SolidJS 中如何管理复杂状态?有哪些状态管理方案?
SolidJS 的响应式系统与其他框架有本质区别:它不依赖虚拟 DOM,而是通过细粒度响应式追踪实现精确更新。理解这一点,是选择正确状态管理方案的前提。本文从实际场景出发,逐一拆解 SolidJS 内置的状态管理原语,帮你建立清晰的选择思路。
一、基础原语:createSignal 与 createStore
createSignal:简单值的首选
createSignal 是 SolidJS 最基础的响应式原语,返回一个 getter 和一个 setter。适用于管理原始类型或简单对象状态。
javascriptimport { createSignal } from 'solid-js'; function Counter() { const [count, setCount] = createSignal(0); // getter 调用触发依赖追踪 // 注意:必须调用 count() 而非 count return ( <button onClick={() => setCount(prev => prev + 1)}> 点击了 {count()} 次 </button> ); }
关键点:getter 是函数调用,这是 SolidJS 响应式追踪的入口。只有执行 count() 时,SolidJS 才知道当前代码依赖了这个信号。
setter 支持直接赋值和函数式更新两种方式:
javascript// 直接赋值 setCount(5); // 基于前值更新(推荐) setCount(prev => prev + 1);
适用场景:计数器、开关状态、表单输入值等简单状态。
createStore:嵌套对象的精确更新
当状态是深层嵌套对象时,createSignal 会引发问题——替换整个对象会导致所有依赖该对象的组件重新渲染,即使只改了一个字段。
createStore 解决了这个问题:它对对象的每个属性都建立独立的响应式追踪,修改 state.user.name 只会触发依赖 name 的代码更新。
javascriptimport { createStore } from 'solid-js/store'; const [state, setState] = createStore({ user: { name: '张三', age: 25 }, settings: { theme: 'dark', lang: 'zh' } }); // 精确更新:只触发依赖 user.name 的响应 setState('user', 'name', '李四'); // 函数式更新 setState('user', 'age', prev => prev + 1); // 批量更新同一层级 setState('user', { name: '王五', age: 30 });
createStore 的 setter 使用路径语法,逐层指定要更新的属性位置,实现了对象级别的细粒度响应式。
适用场景:用户信息、配置对象、表单状态、列表数据等结构化状态。
二、派生状态:createMemo 与 createComputed
createMemo:缓存计算结果
createMemo 创建一个只读的派生信号,只在依赖变化时重新计算。适合将计算逻辑从模板中抽离,避免每次渲染都重复执行。
javascriptimport { createMemo } from 'solid-js'; function ShoppingCart() { const [items, setItems] = createStore({ list: [] }); // 只在 items.list 变化时重新计算 const totalPrice = createMemo(() => items.list.reduce((sum, item) => sum + item.price * item.quantity, 0) ); const itemCount = createMemo(() => items.list.reduce((sum, item) => sum + item.quantity, 0) ); return ( <div> <p>共 {itemCount()} 件商品</p> <p>合计 ¥{totalPrice()}</p> </div> ); }
createMemo 的值会被缓存,多个消费者读取同一个 memo 不会触发重复计算。
createComputed:立即执行的副作用
createComputed 类似 createMemo,但不缓存返回值,用于需要在依赖变化时立即执行副作用的场景(如同步 DOM 操作、日志记录)。它在响应式系统中属于同步执行的观察者。
javascriptimport { createComputed } from 'solid-js'; // 当路由变化时自动更新页面标题 createComputed(() => { document.title = `${currentRoute().name} - 我的应用`; });
日常开发中 createMemo 更常用,createComputed 主要用于需要同步响应的场景。
三、异步状态:createResource
createResource 是 SolidJS 处理异步数据的核心方案,内置 loading/error 状态管理,与 Suspense 深度集成。
javascriptimport { createResource } from 'solid-js'; async function fetchUser(id) { const res = await fetch(`/api/users/${id}`); return res.json(); } function UserProfile(props) { const [user, { mutate, refetch }] = createResource( () => props.userId, // source signal:当 userId 变化时自动重新请求 fetchUser ); return ( <div> {/* 内置 loading 和 error 状态 */} <Show when={user.loading}> <p>加载中...</p> </Show> <Show when={user.error}> <p>加载失败:{user.error.message}</p> </Show> <Show when={user()}> <h2>{user().name}</h2> <p>{user().email}</p> </Show> <button onClick={() => refetch()}>刷新</button> </div> ); }
createResource 返回的第二个对象包含两个实用方法:
- mutate:手动设置数据,跳过请求(乐观更新的关键)
- refetch:重新触发请求
乐观更新示例:
javascriptconst [todos, { mutate }] = createResource(fetchTodos); async function addTodo(text) { const newTodo = { id: Date.now(), text, done: false }; // 先在本地更新 mutate(prev => [...prev, newTodo]); // 再发请求,失败则回滚 try { await api.addTodo(newTodo); refetch(); // 用服务端数据同步 } catch { mutate(prev => prev.filter(t => t.id !== newTodo.id)); } }
适用场景:API 数据获取、需要 loading/error 状态的异步操作。
四、跨组件状态共享
Context API:作用域内的全局状态
SolidJS 的 Context API 与 React 类似,但利用了细粒度响应式,Provider 值的变化只会更新实际使用该值的组件。
javascriptimport { createContext, useContext } from 'solid-js'; const ThemeContext = createContext(); function ThemeProvider(props) { const [theme, setTheme] = createSignal('light'); const toggle = () => setTheme(prev => prev === 'light' ? 'dark' : 'light'); return ( <ThemeContext.Provider value={{ theme, toggle }}> {props.children} </ThemeContext.Provider> ); } // 子组件中使用 function ThemedButton() { const { theme, toggle } = useContext(ThemeContext); return ( <button style={{ background: theme() === 'dark' ? '#333' : '#fff' }} onClick={toggle} > 切换主题 </button> ); }
模块级 Signal:最简单的全局状态
对于不需要作用域隔离的简单全局状态,直接在模块顶层创建信号即可:
javascript// store.js import { createSignal } from 'solid-js'; const [currentUser, setCurrentUser] = createSignal(null); export { currentUser, setCurrentUser }; // 任何组件直接 import 使用 import { currentUser, setCurrentUser } from './store';
这种方式比 Context 更简洁,但缺乏作用域隔离,适合小型应用或真正全局的状态(如登录用户信息)。
五、高级工具:produce、reconcile 与 unwrap
createStore 的配套工具函数在复杂场景下必不可少。
produce:类 Immer 的可变写法
produce 允许在 store 更新中使用可变写法,底层仍然是不可变更新:
javascriptimport { produce } from 'solid-js/store'; // 不用 produce:路径语法 setState('items', items => items.map(item => item.id === id ? { ...item, done: true } : item )); // 使用 produce:可变写法,更直观 setState(produce(state => { const item = state.items.find(i => i.id === id); if (item) item.done = true; }));
reconcile:高效对比更新
当服务端返回完整数据需要覆盖本地 store 时,reconcile 会做精细对比,只更新变化的属性,避免不必要的响应触发:
javascriptimport { reconcile } from 'solid-js/store'; // 服务端返回新数据 const serverData = await fetchFullState(); // reconcile 对比后只更新变化的部分 setState(reconcile(serverData)); // 对比:直接赋值会替换整个对象,触发所有依赖响应 // setState(serverData); // 不推荐
unwrap:读取原始数据
createStore 返回的 state 是 Proxy 对象,在某些场景(序列化、传给非 Solid 代码)需要拿到原始对象:
javascriptimport { unwrap } from 'solid-js/store'; const rawState = unwrap(state); console.log(JSON.stringify(rawState)); // 可以正常序列化
六、方案选择指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单原始值(开关、计数) | createSignal | 最轻量,getter/setter 足够 |
| 嵌套对象、列表 | createStore | 细粒度追踪,避免整体替换 |
| 派生计算值 | createMemo | 缓存计算,依赖追踪 |
| 异步数据获取 | createResource | 内置 loading/error,Suspense 集成 |
| 同步副作用 | createComputed | 立即执行,不缓存 |
| 组件树共享状态 | Context API | 作用域隔离,避免 prop drilling |
| 真正全局的状态 | 模块级 Signal | 最简洁,无需 Provider |
| Store 可变更新 | produce | 直观写法,底层不可变 |
| 服务端数据覆盖 | reconcile | 精细对比,最小化更新 |
选择的核心原则:从简单方案开始,按需升级。大部分场景下 createSignal + createStore + createResource 已经足够,不需要引入额外状态管理库。
七、常见误区
误区一:给 createStore 套 React 思维。SolidJS 的 Store 不需要 reducer/action,直接用 setState 路径语法或 produce 即可更新。不需要模仿 Redux 的模式。
误区二:过度使用 Context。Context 适合主题、国际化等需要作用域隔离的状态。如果是全局唯一的登录信息,模块级 Signal 更简洁。
误区三:忽略 getter 调用。createSignal 返回的是 getter 函数,必须在响应式上下文(组件函数体、createMemo/createEffect 内)中调用才能建立依赖追踪。在 setTimeout 或事件回调中读取值不会建立追踪关系——这本身没问题,但需要理解其中的区别。
误区四:createResource 不需要手动管理 loading。createResource 与 <Suspense> 配合时可以自动处理 loading 状态,但你仍然可以在组件内通过 resource.loading 做更细粒度的控制。