首页 / 文章

上下文、状态或Redux Toolkit:根据过渡复杂度选择

应根据状态转换的复杂性而非应用规模来选择 React 状态管理工具:Context 会扩散树形依赖,Zustand 能更精准地管理订阅,Redux Toolkit 则用于处理状态转换。

发布者:REACTAPP.TOP阅读约 4 分钟1017 词

来源: Vibe Coding Can Build the Store. Can You Design It?

人工智能几乎让前端开发中的某一部分工作变得免费:编写代码。只要提出需要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树来分发依赖关系
  • Zustand — 在树结构之外提供细粒度的订阅功能的共享客户端存储
  • Redux Toolkit — 在结构化架构中实现明确的事件处理与可预测的状态转换
  • “哪个库最好?”并不是正确的第一个问题。更好的问题是:需要管理何种复杂度?应选择能让该复杂度更易于理解的抽象层,而非那些被标为“现代”的库。

    实际上许多代码库会混合使用这些工具:用 Context 处理主题和区域设置,用 Zustand 管理购物车及界面元素,仅在需要审计追踪的多步骤资金和库存操作时才使用 Toolkit。只要每个工具的使用都有明确目的,这种混合方式是可行的。问题在于,当某个大型 Context 或一个未做区分的存储因某些建议而试图涵盖所有功能时,麻烦就来了。