用途
solidjs-patterns 是 OpenWork 项目中的一项技能,用于统一 SolidJS 的响应式与 UI 状态写法。OpenWork 的 UI 使用 SolidJS,它通过 signals 更新,而不是 React 风格的重渲染。该技能指出,大多数“UI 卡住”的 bug 实际上是状态耦合 bug(例如一个全局 busy() 禁用了不相关的操作),而不是重渲染问题。
核心规则
- 优先使用细粒度 signals,而不是共享的全局标志。
- 保持异步操作作用域化(每个操作拥有自己的 pending 状态)。
- 通过 createMemo() 派生 UI 状态,而不是重复定义布尔值。
- 避免修改存放在 signals 中的数组/对象;始终创建新值。
作用域化异步操作(推荐)
当一个操作可能与其他操作重叠时(权限、安装、后台刷新),不要复用全局 busy()。为每个操作使用专用 signal:
const [replying, setReplying] = createSignal(false);
async function respond() {
if (replying()) return;
setReplying(true);
try {
await doTheThing();
} finally {
setReplying(false);
}
}
原因:单个 busy() 布尔值会造成死锁:
- 长时间运行的任务设置 busy(true)
- 出现权限提示,其按钮被 busy() 禁用
- 任务在权限被应答前无法继续
- 用户因为按钮被禁用而无法应答
修复方式:权限 UI 只能由权限专用的 pending 状态禁用。
异步处理器中的 signal 快照
如果在异步函数内部读取 signals,并且需要稳定的值,应尽早快照:
const request = activePermission();
if (!request) return;
const requestID = request.id;
await respondPermission(requestID, "always");
派生 UI 状态
对于计算出的禁用状态,优先使用 createMemo():
const canSend = createMemo(() => prompt().trim().length > 0 && !busy());
列表
对派生更新使用 setter 回调:
setItems((current) => current.filter((x) => x.id !== id));
不要就地修改 current。
实用检查清单(SolidJS UI 改动)
- 是否有任何按钮依赖一个在长时间运行工作期间可能为 true 的全局标志?
- 两个异步操作是否可能重叠并争抢同一个布尔值?
- 是否有任何 UI 状态被重复定义(本可以派生)?
- 事件处理器是否在 await 之后读取 signals,而值可能已经改变?
- 如果重构了 props/types,是否更新了所有中间组件签名和调用点?
参考
- SolidJS: https://www.solidjs.com/docs/latest
- SolidJS signals: https://www.solidjs.com/docs/latest/api#createsignal
- SolidJS memos: https://www.solidjs.com/docs/latest/api#creatememo