solidjs-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Why this skill exists

该技能的存在意义

OpenWork’s UI is SolidJS: it updates via signals, not React-style rerenders. Most “UI stuck” bugs are actually state coupling bugs (e.g. one global
busy()
disabling an unrelated action), not rerender issues.
This skill captures the patterns we want to consistently use in OpenWork.
OpenWork的UI基于SolidJS构建:它通过signals(信号)进行更新,而非React风格的重新渲染。 大多数“UI卡住”的bug实际上是状态耦合问题(例如,一个全局的
busy()
状态禁用了无关的操作),而非重新渲染问题。
本技能记录了我们希望在OpenWork中统一遵循的开发模式。

Core rules

核心规则

  • Prefer fine-grained signals over shared global flags.
  • Keep async actions scoped (each action gets its own
    pending
    state).
  • Derive UI state via
    createMemo()
    instead of duplicating booleans.
  • Avoid mutating arrays/objects stored in signals; always create new values.
  • 优先使用细粒度signals,而非共享全局标记。
  • 保持异步操作的作用域隔离(每个操作拥有独立的
    pending
    状态)。
  • 通过
    createMemo()
    推导UI状态,而非重复定义布尔值。
  • 不要修改存储在signals中的数组/对象;始终创建新值。

Scoped async actions (recommended)

作用域隔离的异步操作(推荐方案)

When an operation can overlap with others (permissions, installs, background refresh), don’t reuse a global
busy()
.
Use a dedicated signal per action:
ts
const [replying, setReplying] = createSignal(false);

async function respond() {
  if (replying()) return;
  setReplying(true);
  try {
    await doTheThing();
  } finally {
    setReplying(false);
  }
}
当某个操作可能与其他操作(权限验证、安装、后台刷新)重叠时,不要复用全局的
busy()
状态。
应为每个操作使用独立的signal:
ts
const [replying, setReplying] = createSignal(false);

async function respond() {
  if (replying()) return;
  setReplying(true);
  try {
    await doTheThing();
  } finally {
    setReplying(false);
  }
}

Why

原因说明

A single
busy()
boolean creates deadlocks:
  • Long-running task sets
    busy(true)
  • A permission prompt appears and its buttons are disabled by
    busy()
  • The task can’t continue until permission is answered
  • The user can’t answer because buttons are disabled
Fix: permission UI must be disabled only by a permission-specific pending state.
单一的
busy()
布尔值会导致死锁:
  • 长时间运行的任务将
    busy
    设置为
    true
  • 权限提示框弹出,但其按钮被
    busy()
    状态禁用
  • 任务需等待权限确认才能继续
  • 用户因按钮被禁用无法确认权限
解决方法:权限UI应仅由权限专属的pending状态控制。

Signal snapshots in async handlers

异步处理程序中的Signal快照

If you read signals inside an async function and you need stable values, snapshot early:
ts
const request = activePermission();
if (!request) return;
const requestID = request.id;

await respondPermission(requestID, "always");
如果在异步函数中读取signal且需要稳定值,请提前获取快照:
ts
const request = activePermission();
if (!request) return;
const requestID = request.id;

await respondPermission(requestID, "always");

Derived UI state

推导式UI状态

Prefer
createMemo()
for computed disabled states:
ts
const canSend = createMemo(() => prompt().trim().length > 0 && !busy());
优先使用
createMemo()
计算禁用状态:
ts
const canSend = createMemo(() => prompt().trim().length > 0 && !busy());

Lists

列表处理

  • Use setter callbacks for derived updates:
ts
setItems((current) => current.filter((x) => x.id !== id));
  • Don’t mutate
    current
    in-place.
  • 使用setter回调进行衍生更新:
ts
setItems((current) => current.filter((x) => x.id !== id));
  • 不要原地修改
    current

Practical checklist (SolidJS UI changes)

实用检查清单(SolidJS UI变更)

  • Does any button depend on a global flag that could be true during long-running work?
  • Could two async actions overlap and fight over one boolean?
  • Is any UI state duplicated (can be derived instead)?
  • Do event handlers read signals after an
    await
    where values might have changed?
  • If you refactor props/types, did you update all intermediate component signatures and call sites?
  • 是否有按钮依赖可能在长时间运行任务中变为true的全局标记?
  • 是否存在两个异步操作因共用同一个布尔值而产生冲突的情况?
  • 是否有可通过推导而非重复定义的UI状态?
  • 事件处理程序是否在
    await
    之后读取signal,而此时值可能已发生变化?
  • 重构props/类型时,是否更新了所有中间组件的签名和调用位置?

References

参考资料