SolidJS 最佳实践
SolidJS 通过细粒度响应式系统提供接近原生的 DOM 性能,但这种模型也引入了一些会让来自 React 或 Vue 的开发者感到困惑的模式。本文介绍在实际应用中最重要的 SolidJS 最佳实践——这些实践可以防止微妙的 bug 并保持代码的可预测性。
核心要点
- SolidJS 组件作为设置函数只执行一次——响应式存在于 signal 层面,而非组件层面
- 将 signal 读取保持在响应式作用域内:JSX 表达式、
createEffect或createMemo - 使用函数或
createMemo派生值,而不是通过 effect 同步状态 - 永远不要解构 props——使用
mergeProps和splitProps来保持 getter 链 - 对嵌套状态使用
createStore,对异步数据获取使用createResource
理解组件只运行一次
这是其他一切所依赖的心智模型转变。在 React 中,组件在状态变化时重新渲染。在 SolidJS 中,组件作为设置函数只执行一次。响应式发生在 signal 层面,而不是组件层面。
这意味着以下代码是错误的:
function Counter() {
const [count, setCount] = createSignal(0)
const doubled = count() * 2 // 只运行一次。永远不会更新。
return <div>{doubled}</div>
}
修复方法是将 signal 读取保持在响应式作用域内——JSX 表达式、createEffect 或 createMemo:
function Counter() {
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2) // 响应式地追踪 count
return <div>{doubled()}</div>
}
SolidJS 响应式模式:Signals、Memos 和 Effects
SolidJS 中的细粒度响应式建立在三个基础原语之上。了解何时使用每一个是编写高效 SolidJS 组件的核心。
createSignal— 用于原始值和简单状态createMemo— 用于依赖 signals 的派生值createEffect— 仅用于副作用(DOM 操作、第三方库)
最常见的错误是使用 createEffect 来同步状态:
// ❌ 反模式:使用 effect 进行派生
createEffect(() => setFullName(`${firstName()} ${lastName()}`))
// ✅ 正确:直接派生
const fullName = () => `${firstName()} ${lastName()}`
像 fullName 这样的普通函数可以工作,因为 SolidJS 会在它出现在读取其底层 signals 的响应式作用域内时重新计算它。只有当派生计算昂贵且你想要缓存结果时才使用 createMemo。
将 createEffect 保留用于 Solid 响应式图之外的工作——比如初始化图表库或命令式地更新 DOM 元素。查看官方文档中关于 memos 和派生值 的解释以获取更深入的细节。
永远不要解构 Props
SolidJS 的 props 由 getters 支持。解构它们会破坏响应式连接:
// ❌ 破坏响应式
function User({ name }: { name: string }) {
return <h1>{name}</h1>
}
// ✅ 保持响应式
function User(props: { name: string }) {
return <h1>{props.name}</h1>
}
当你需要默认值或想要分离你消费的 props 和你转发的 props 时,使用 mergeProps 和 splitProps——两者都保持 getter 链。
Discover how at OpenReplay.com.
使用控制流组件进行条件和列表渲染
SolidJS 性能模式依赖于使用正确的渲染原语。对于响应式条件和列表,优先使用 Solid 的控制流组件,而不是在 JSX 内依赖原始 JavaScript 逻辑。
// ✅ 条件渲染
<Show when={isLoggedIn()} fallback={<LoginPage />}>
<Dashboard />
</Show>
// ✅ 列表渲染 — 在更新过程中保持项目身份
<For each={posts()}>{(post) => <PostCard post={post} />}</For>
当列表位置保持稳定但这些位置的值可能改变时,使用 <Index> 而不是 <For>。<For> 通过引用追踪项目,最适合具有稳定身份的对象数组,而 <Index> 通过位置追踪项目。
你可以在官方文档中阅读更多关于 Solid 的 列表渲染原语。
对复杂状态使用 Stores
Signals 追踪单个值的变化。当该值是一个大型嵌套对象时,更新会替换整个值,这意味着该 signal 的所有消费者都会对变化做出反应。createStore 提供属性级响应式,更适合嵌套状态:
const [state, setState] = createStore({ user: { name: "Alice" }, posts: [] })
// 只有读取 state.posts 的组件会对此做出反应
setState("posts", (p) => [...p, newPost])
对于复杂的变更使用 produce,在将服务器数据合并到现有 store 时使用 reconcile。
正确处理异步数据
在 createEffect 内获取数据可能导致竞态条件,并且不能与 Suspense 干净地集成。改用 createResource:
const [posts] = createResource(() => fetch("/api/posts").then((r) => r.json()))
createResource 接受一个可选的 source signal 作为其第一个参数。当提供时,每当该 source 变化时 fetcher 会重新运行,并且 resource 会自动与 Solid 的 <Suspense> 和 <ErrorBoundary> 组件集成。
在 SolidStart 应用中,将 query 和 createAsync 与 preload 函数一起使用。Preload 函数可以在导航意图期间(如链接悬停)运行,并在导航期间再次运行,允许数据在组件渲染时准备就绪。
结论
SolidJS 奖励那些顺应其响应式模型而非对抗它的开发者。将 signal 读取保持在响应式作用域内,派生值而不是通过 effects 同步它们,永远不要解构 props,并在状态具有嵌套结构时使用 createStore。这些 SolidJS 响应式模式不是任意规则——它们是细粒度响应式工作方式的直接结果,遵循它们可以产生既正确又快速的组件。
常见问题
SolidJS 组件只运行一次,因此在响应式作用域外分配给普通变量的任何 signal 读取只会捕获初始值。将派生包装在 createMemo 中或将 signal 读取直接放在 JSX 内。两者都是响应式作用域,会在底层 signal 变化时重新计算。
普通派生函数对于轻量级计算效果很好,因为 SolidJS 会在每次消费响应式作用域运行时重新计算它。当派生计算昂贵或被多个消费者读取时使用 createMemo。createMemo 会缓存结果,只在其依赖项变化时重新计算。
For 通过引用追踪每个项目,因此它非常适合具有稳定身份的对象数组。Index 通过项目在数组中的位置追踪项目,使其更适合位置保持稳定但该位置的值可能改变的列表。
你可以,但你不应该这样做。使用 createEffect 同步派生状态会创建不必要的中间更新,并可能引入故障。相反,使用函数或 createMemo 派生值。将 createEffect 保留用于真正的副作用,如 DOM 操作、日志记录或与第三方库交互。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.