本节摘要:React 的复用单元是自定义 Hook,其两条铁律(只在顶层调用、依赖数组齐全)都源于"组件反复执行";Solid 的复用单元是普通函数——组合原语,没有调用规则、没有闭包旧值问题。本节以一个本地存储同步 Hook 为例完整平移,并补上迁移绕不开的 props 工具:
mergeProps与splitProps。
别把自定义 Hook 当成需要虔诚遵守仪式的圣物再搬进 Solid。自定义 Hook 的两条铁律——只能在组件顶层调用、不能放进条件分支——不是因为函数式编程的审美,而是因为 Hooks 内部靠调用顺序对位存储位置,组件每次重跑,顺序一乱状态就错位。Solid 组件只跑一次,"顺序错位"的问题从根上不存在:复用单元退化为普通函数,在哪调用、条件调用、循环调用,全都合法。
React 版本,先摆出全部套路:
import { useState, useEffect } from "react"; function useLocalStorage(key, initial) { const [value, setValue] = useState(() => { try { return JSON.parse(localStorage.getItem(key)) ?? initial; } catch { return initial; } }); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); // 依赖数组:誊写要小心 useEffect(() => { const onStorage = e => { if (e.key === key) setValue(JSON.parse(e.newValue)); }; window.addEventListener("storage", onStorage); return () => window.removeEventListener("storage", onStorage); }, [key]); // 又一次誊写 return [value, setValue]; }
Solid 版本:
import { createSignal, createEffect, onCleanup } from "solid-js"; function createLocalStorage(key, initial) { const [value, setValue] = createSignal(() => { try { return JSON.parse(localStorage.getItem(key)) ?? initial; } catch { return initial; } }); createEffect(() => localStorage.setItem(key, JSON.stringify(value()))); if (typeof window !== "undefined") { const onStorage = e => { if (e.key === key) setValue(JSON.parse(e.newValue)); }; window.addEventListener("storage", onStorage); onCleanup(() => window.removeEventListener("storage", onStorage)); } return [value, setValue]; }
平移的省力点逐条标注:依赖数组整段消失,持久化 Effect 里读了 key 与 value(),依赖自动成立;清理不再需要第二个 Effect 来装,onCleanup 写在订阅语句旁边,代码的组织跟着业务走而不是跟着规则走;if (typeof window !== "undefined") 这种条件性注册是 React 里明令禁止的"条件 Hook",Solid 里就是普通判断——这个差别在服务端渲染(第 6 章)里会变成实打实的能力。命名习惯也从 use 前缀换成 create 前缀,社区约定俗成,看到 create 开头的函数就该知道它返回响应式原语。
💡 判断一段 React 逻辑能不能"低成本"平移,有个快速自检:数一数依赖数组。一个依赖数组都没有的逻辑,几乎可以逐行照抄;依赖数组越多,平移后删掉的代码越多,收益越大。
第 1 章埋的坑在这里填。看一段迁移者最容易写出的代码:
// 错误:解构发生在组件执行的瞬间,name 从此是静态快照 function Greeting({ name }) { return <p>你好,{name}</p>; // 父组件更新 name 后,这里纹丝不动 } // 正确:通过 props 访问器按需取值,每次取值都走响应式通道 function Greeting(props) { return <p>你好,{props.name}</p>; }
机制要说透:Solid 的 props 对象不是普通对象,它上面的每个属性都是取值器(getter)——读取 props.name 的动作会被追踪,父组件传来的信号变化时,JSX 里这个表达式重新执行。解构语句 const { name } = props 在组件函数执行的那一刻调用了取值器,拿到当时的值存进局部常量,通路到此断开。这不是 Solid 的缺陷,而是"组件只执行一次"的必然代价:React 靠重渲染把新值重新送进来,Solid 没有重渲染,值必须留在通路上。
默认值的写法也随之下沉到工具函数。React 习惯 function Btn({ type = "primary" }),Solid 用 mergeProps:
import { mergeProps, splitProps } from "solid-js"; function Btn(allProps) { // mergeProps 保留取值器的同时补默认值 const props = mergeProps({ type: "primary", size: "md" }, allProps); // splitProps 按键组拆分,剩余项透传,保持响应 const [local, rest] = splitProps(props, ["type", "size"]); return <button class={`btn ${local.type} ${local.size}`} {...rest} />; }
splitProps 是 React 场景里没有对应物的工具:它把 props 拆成"本组件消费的"与"透传给底层节点的"两份,两份都保持响应性。写组件库封装时(比如把业务按钮包装底层按钮组件),它会成为日常工具。
习惯一:原语放哪,作用域就在哪。在组件里调用 createLocalStorage,它内部的 Effect、清理都登记到该组件的 Owner 下,组件销毁即全部回收——复用单元与生命周期天然对齐,不需要额外的约定。习惯二:原语可以嵌套组合,createLocalStorage 内部再调用别的 create 函数完全合法,组合深度不受限。习惯三:纯计算不要做成原语。返回值不随时间变化的函数就是普通函数,包一层 Signal 反而增加依赖图负担——这个判断和 2.4 节"什么时候不需要 Memo"是同一套思路。
props.xxx 访问或用工具函数。下一章下到架构层——编译器切好了模板,运行时怎么把信号的变化送到正确的节点。