5月27日 15:52

SolidJS 中如何管理复杂状态?有哪些状态管理方案?

SolidJS 的响应式系统与其他框架有本质区别:它不依赖虚拟 DOM,而是通过细粒度响应式追踪实现精确更新。理解这一点,是选择正确状态管理方案的前提。本文从实际场景出发,逐一拆解 SolidJS 内置的状态管理原语,帮你建立清晰的选择思路。

一、基础原语:createSignal 与 createStore

createSignal:简单值的首选

createSignal 是 SolidJS 最基础的响应式原语,返回一个 getter 和一个 setter。适用于管理原始类型或简单对象状态。

javascript
import { 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 的代码更新。

javascript
import { 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 创建一个只读的派生信号,只在依赖变化时重新计算。适合将计算逻辑从模板中抽离,避免每次渲染都重复执行。

javascript
import { 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 操作、日志记录)。它在响应式系统中属于同步执行的观察者。

javascript
import { createComputed } from 'solid-js'; // 当路由变化时自动更新页面标题 createComputed(() => { document.title = `${currentRoute().name} - 我的应用`; });

日常开发中 createMemo 更常用,createComputed 主要用于需要同步响应的场景。

三、异步状态:createResource

createResource 是 SolidJS 处理异步数据的核心方案,内置 loading/error 状态管理,与 Suspense 深度集成。

javascript
import { 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:重新触发请求

乐观更新示例:

javascript
const [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 值的变化只会更新实际使用该值的组件。

javascript
import { 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 更新中使用可变写法,底层仍然是不可变更新:

javascript
import { 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 会做精细对比,只更新变化的属性,避免不必要的响应触发:

javascript
import { reconcile } from 'solid-js/store'; // 服务端返回新数据 const serverData = await fetchFullState(); // reconcile 对比后只更新变化的部分 setState(reconcile(serverData)); // 对比:直接赋值会替换整个对象,触发所有依赖响应 // setState(serverData); // 不推荐

unwrap:读取原始数据

createStore 返回的 state 是 Proxy 对象,在某些场景(序列化、传给非 Solid 代码)需要拿到原始对象:

javascript
import { 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 不需要手动管理 loadingcreateResource<Suspense> 配合时可以自动处理 loading 状态,但你仍然可以在组件内通过 resource.loading 做更细粒度的控制。

标签:SolidJS