本节摘要:共享状态在 React 里是一道架构题——Context 会引发消费组件集体重渲染,于是社区造出 Redux、Zustand 一整条外部 store 赛道;在 Solid 里它退化为一道放置题——信号天生跨组件,模块级原语管全局,Context 只负责"往树里注入"。本节给出双轨制的判断标准,并写出两条路的对照代码。
一家内容平台的控制台有个典型格局:当前登录用户被顶栏、侧边菜单、权限守卫、个人主页四处读取;主题色影响全部界面;购物车数据在几十个组件间流动。React 团队面对这个格局的 standard answer 是 Context 加 useMemo 再加选择器库;Solid 团队的答案短得多——先问"它需要跟组件树走,还是全局唯一"。这一问就是本节的主线。
全局唯一的状态(当前用户、主题、购物车),直接放在模块作用域:
// session.js:模块级信号就是全局状态,没有 Provider 包裹 import { createSignal } from "solid-js"; const [user, setUser] = createSignal(null); export { user, setUser };
任何组件导入即用,读写都走信号通道,订阅自动建立。React 里这么写等于绕开框架自建旁路,Solid 里这是官方认可的一等公民——原因回到老话题:信号更新不走组件重渲染,全局信号的变化只会触达真正订阅它的绑定,不存在"全局状态导致全树刷新"的恐惧。
需要"随组件树实例化"的状态(多标签页各自的主题、路由参数派生的数据、测试时需要隔离的状态),用 Context 注入:
import { createContext, useContext, createSignal } from "solid-js"; const ThemeContext = createContext(); export function ThemeProvider(props) { const [theme, setTheme] = createSignal(props.initial || "light"); const value = { theme, setTheme }; return <ThemeContext.Provider value={value}>{props.children}</ThemeContext.Provider>; } export function useTheme() { return useContext(ThemeContext); } // 组件里 const { theme, setTheme } = useTheme();
形状与 React 几乎一样,语义差异藏在一行里:value 是个普通对象,里面的 theme 是信号。消费组件拿到的是"信号的引用"而不是"信号的值"——主题变化时没有任何组件重渲染,只有 JSX 里 theme() 出现的位置被改写。React 的 Context 是值广播:Provider 的 value 一变,所有消费组件重渲染,于是大家学着用 memo 与选择器堵漏;Solid 的 Context 是引用注入,广播这回事不存在。同一个 API,React 里用来传值,Solid 里用来传线路。

| 维度 | React Context | Solid Context |
|---|---|---|
| 传递内容 | 值本身 | 引用(信号、方法、组件) |
| value 变化的影响 | 消费组件全部重渲染 | 无重渲染,订阅点各自更新 |
| 防止过度渲染的手段 | memo、拆分 Provider、选择器库 | 不需要 |
| 配套外部 store 的动机 | 绕开广播语义 | 几乎没有动机 |
| 典型用法 | 主题、语言、登录态的值 | 主题、语言、登录态的线路 |
表里"配套外部 store 的动机"一行解释了一个迁移现象:React 项目里为绕开 Context 而引入的 Zustand、Jotai,在 Solid 里多数直接删除——它们解决的问题不复存在。真正需要保留的是服务端缓存类库(TanStack Query 等),因为缓存失效、重试、窗口聚焦刷新这套逻辑与渲染模型无关,Solid 官方生态也提供了对应的绑定层。
⚠️ 模块级信号虽好,别什么都往里放。全局状态的可测试性与热更新都弱于 Context 注入:测试之间会互相污染,改代码后模块状态不会重置。经验法则是"默认模块级,需要隔离时包 Context",把 Context 当成注入手段而非状态容器。
用一个贯穿案例把决策走完。需求:购物车在顶栏徽标、商品列表按钮、结算页三处消费,还要在多标签页间同步(3.3 的存储原语正好复用)。
第一步判断实例化粒度:购物车全站唯一、不随路由或布局分支变化——模块级 store 胜出,Context 在这里没有收益。第二步判断数据形态:条目列表加数量、会被深层修改——store 而非 Signal。第三步接多标签同步:存储事件写回 store 的对应路径,reconcile 对齐。落子结果:
// cart.js:模块级 store,三处消费、零 Provider import { createStore, reconcile } from "solid-js/store"; const [cart, setCart] = createStore({ items: [], version: 0 }); export function addToCart(item) { setCart("items", list => [...list, item]); } export function syncFromOtherTab(json) { setCart(reconcile(JSON.parse(json))); setCart("version", v => v + 1); // 供需要"整体刷新感知"的界面订阅 } export { cart };
注意 version 字段的用意:多数消费点只关心条目(订阅 cart.items 的路径),个别界面(结算页倒计时文案)想感知"哪怕内容没变、同步发生过"——给它们一个独立的小信号,而不是让全部消费方陪跑。这个案例的完整决策链——实例化粒度、数据形态、同步通道、派生信号——就是 5.1 的全部内容在工作台上的一次实操。
下一节把异步数据接入这张格局图——createResource 与 Suspense 如何把"加载中"从命令式三态变成声明。