上下文、状态或Redux Toolkit:根据过渡复杂度选择
应根据状态转换的复杂性而非应用规模来选择 React 状态管理工具:Context 会扩散树形依赖,Zustand 能更精准地管理订阅,Redux Toolkit 则用于处理状态转换。
人工智能几乎让前端开发中的某一部分工作变得免费:编写代码。只要提出需要Zustand存储、Redux切片或Context加上useReducer,模型就会生成可编译的代码。语法不再是难点,真正的难题在于空白的架构设计以及状态应在产品中如何传递。
这才是真正的状态管理问题。Context API、Zustand和Redux Toolkit从不同角度来解决它。这里讨论的并非API的表面知识,而是其背后的实现原理。
1. Context API:React树即边界
Context解决的是一个具体问题:如何让某个值无需逐层传递prop就能到达深层组件?
React的文档将useContext描述为读取和监听上下文。组件会从其上层最接近的提供者那里获取值。当该提供者的值发生变化时,读取器会随之更新。
关键不在于钩子的调用,而在于树形结构关系。
提供者定义了环境,而消费者则从该环境中读取数据。
上下文存在于React的层级结构中。
上下文本身并不管理状态
一个常见的误解是认为Context是一个状态管理库。实际上,状态更新通常来自useState、reducer或自定义钩子。Context主要负责传播这些钩子产生的值。
巨大的上下文陷阱
人们说“上下文会导致不必要的重新渲染”。这种说法过于绝对。React会使用Object.is来比较提供者之前的值与当前的值。当值发生变化时,所有读取该值的组件都会更新;而memo机制并不会阻止新上下文的出现。
想象有一组彼此无关的字段:
<AppContext
value={{
user,
cart,
orders,
notifications
}}
>
更深层次的问题在于架构设计:将本不应属于同一范畴的模块放在同一个依赖边界下。
与其这样:
AppContext
不如选择:
AuthContext
ThemeContext
FeatureFlagContext
这样每个上下文都有明确的语义职责。设计原则是:上下文应代表有意义的依赖边界。问题不在于上下文本身“难以扩展”,而往往在于那些过于庞大的上下文。
2. 离开树结构:Zustand
上下文与组件树相关联。Zustand则提出了不同的问题:如何让共享的客户端状态在组件树之外存在,并实现精确的订阅机制?
存储可以保存状态以及用于修改状态的函数:
const useCartStore = create((set) => ({
items: [],
addItem: (item) =>
set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) =>
set((state) => ({
items: state.items.filter(
item => item.id !== id
)
}))
}));
组件负责选择所需的数据片段:
const items = useCartStore(
state => state.items
);
其优势在于订阅边界的明确性。组件无需获取整个存储,只需获取它关心的那部分数据。选择器则将这一约定明确化。
为何Redux仍然存在
如果Zustand如此简洁,那为什么还需要Redux Toolkit呢?因为问题从“该把这个值放在哪里?”转变为“如何处理所有可能影响它的因素?”
想象一下存在重叠的领域:
Checkout
Payment
Inventory
Orders
Refunds
Permissions
工作流程可能如下所示:
Checkout Started
↓
Payment Started
↓
Payment Succeeded
↓
Inventory Reserved
↓
Order Created
如果在支付成功后库存预留失败,问题就不再出在存储上,而是属于状态转换问题。正是在这方面,Redux的架构发挥了作用。
3. 作为明确转换机制的Redux
不应将Redux简单理解为“全局存储”。而应将其视为一套用于描述事件并计算可预测的下一状态的系统:
nextState =
reducer(previousState, action)
Redux的工作流程
相比简短的set(...)语句,这种方式显得更为复杂:
set(...)
每个步骤都有其具体职责:
- 动作 — 发生了什么?
- 还原器 — 状态应如何变化?
- 存储 — 当前状态是什么?
- 选择器 — 该组件需要什么数据?
这种明确性正是其特点。当业务规则变得复杂时,了解状态变化原因比节省十行代码更有价值。
Redux Toolkit 是现代版的 Redux
传统 Redux 需要手动定义动作类型和创建函数,而 Toolkit 通过 createSlice 消除了这一需求:
const cartSlice = createSlice({
name: "cart",
initialState: {
items: []
},
reducers: {
addItem(state, action) {
state.items.push(action.payload);
}
}
});
相关的状态和转换会保持在一起,同时自动生成动作创建函数。Immer 允许使用类似变异的语法,却能生成不可变的更新结果。现代 Redux 更少“编写样板代码”,而是更注重“以结构化的方式处理状态转换,无需繁琐的传统流程”。
中间件与异步操作
中间件位于 dispatch 和 reduce 之间:
那个模块负责处理日志记录、数据分析、异步工作流以及对特定操作的响应。Toolkit提供了诸如createAsyncThunk和监听器中间件之类的辅助工具。Zustand允许灵活的异步结构组织,而Toolkit则提供了更为成熟的模式。
该选哪一个?
避免使用规模判断法:
- 小型应用 → Context
- 中型应用 → Zustand
- 大型应用 → Redux
屏幕数量并不能准确反映状态的复杂性。一个拥有百个屏幕的CRUD应用可能比一个只有十个屏幕的支付与权限处理流程更简单。
最好从关系与转换的角度来考虑:
需牢记的心智模型
不必记住三个品牌的标志,只需记住三种处理方式:
- Context — 通过React树来分发依赖关系
“哪个库最好?”并不是正确的第一个问题。更好的问题是:需要管理何种复杂度?应选择能让该复杂度更易于理解的抽象层,而非那些被标为“现代”的库。
实际上许多代码库会混合使用这些工具:用 Context 处理主题和区域设置,用 Zustand 管理购物车及界面元素,仅在需要审计追踪的多步骤资金和库存操作时才使用 Toolkit。只要每个工具的使用都有明确目的,这种混合方式是可行的。问题在于,当某个大型 Context 或一个未做区分的存储因某些建议而试图涵盖所有功能时,麻烦就来了。