5.1 内置状态方案:Context、store与全局状态对照


5.1 内置状态方案:Context、store 与全局状态对照

本节摘要:共享状态在 React 里是一道架构题——Context 会引发消费组件集体重渲染,于是社区造出 Redux、Zustand 一整条外部 store 赛道;在 Solid 里它退化为一道放置题——信号天生跨组件,模块级原语管全局,Context 只负责"往树里注入"。本节给出双轨制的判断标准,并写出两条路的对照代码。

一家内容平台的控制台有个典型格局:当前登录用户被顶栏、侧边菜单、权限守卫、个人主页四处读取;主题色影响全部界面;购物车数据在几十个组件间流动。React 团队面对这个格局的 standard answer 是 Context 加 useMemo 再加选择器库;Solid 团队的答案短得多——先问"它需要跟组件树走,还是全局唯一"。这一问就是本节的主线。

双轨制:模块级原语与 Context 注入

全局唯一的状态(当前用户、主题、购物车),直接放在模块作用域:

// 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 里用来传线路。

图:共享状态方案光谱——从组件内到外部库

图:共享状态方案光谱——从组件内到外部库

对照表:两条 Context 的语义分野

维度 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 的全部内容在工作台上的一次实操。

本节要点回顾

  • 放置题取代架构题:先问"随树实例化还是全局唯一",再选 Context 或模块级。
  • React 传值,Solid 传线路:Context 语义的分野决定了配套生态的去留。
  • 外部 store 库大幅退役:为绕开广播语义而生的库,问题域消失即退役。
  • 默认模块级,隔离用 Context:可测试性是唯一的权衡项。

下一节把异步数据接入这张格局图——createResource 与 Suspense 如何把"加载中"从命令式三态变成声明。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U