用Zustand实现组件级状态管理的最佳实践

2024-08-26
16886 分钟
...

在前文中,我们介绍了Zustand这个简单、易用、轻量的状态管理框架。

通常情况下,状态管理通常都是全局的,可以在应用的任意地方访问。然而,这样的做法是否真的符合最佳实践呢?如果从马克思的角度来看,任何片面的观点都是不全面的。事实上,有些时候我们只想创建页面级别或者组件级别的状态,而不是把所有状态都挂在全局。

全局状态的弊端

无效渲染

全局状态管理的一个明显弊端是它可能导致无效的渲染。全局状态通常是在React组件生命周期之外创建的,这意味着我们无法利用组件的props值来设置初始状态。我们只能通过一个默认值来创建状态,然后再利用useEffectprops中的值同步到store中:

const useBearStore = create((set) => ({
  
  bears: 0,
  actions: {
    increasePopulation: (by) =>
      set((state) => ({ bears: state.bears + by })),
    removeAllBears: () => set({ bears: 0 }),
  },
}));

const App = ({ initialBears }) => {
  
  React.useEffect(() => {
    useBearStore.set((prev) => ({ ...prev, bears: initialBears }));
  }, [initialBears]);

  return (
    <main>
      <RestOfTheApp />
    </main>
  );
};

在上面的例子中,组件在useEffect触发之前会使用初始的bears: 0渲染一次,然后在正确的initialBears赋值后再次渲染。我们只是在同步initialBears值,而不是用它来初始化状态,但这依然会导致多次渲染。

难以管理

全局状态的另一个弊端是难以管理。在应用的任意部分,全局状态都可能被意外访问或修改,这使得在项目后续迭代中难以保证状态的安全隔离,甚至可能导致状态混乱。

假设你有一个应用,包含用户信息和购物车信息的全局状态。初始设计时,这两个状态是分离的,但在某次迭代中,开发者意外地修改了购物车状态中的用户信息,从而导致状态混乱。

import create from 'zustand';


const useUserStore = create(set => ({
  user: { name: 'John Doe', loggedIn: true },
  updateUser: (newUser) => set({ user: newUser }),
}));


const useCartStore = create(set => ({
  cart: [],
  addItem: (item) => set(state => ({ cart: [...state.cart, item] })),
}));

在这个初始设计中,用户信息和购物车信息是分开的,全局状态管理看起来较为清晰。

但在项目的后续迭代中,假如开发者需要在购物车中添加一些用户相关的信息,错误地修改了useCartStore,直接访问和修改了用户信息:

const useCartStore = create(set => ({
  cart: [],
  user: { name: 'John Doe', loggedIn: true }, 
  addItem: (item) => set(state => ({ cart: [...state.cart, item] })),
  updateUserInCart: (newUser) => set({ user: newUser }), 
}));

这个设计有两个主要问题:

  • 问题 1: 用户状态现在同时存在于useUserStoreuseCartStore中,导致了状态的重复和混乱。
  • 问题 2: 其他开发者可能在后续开发中未意识到状态已经被重复定义,并可能会意外地通过useCartStore修改user,导致状态的不一致和难以追踪的bug。

虽然这个例子看起来容易发现问题,但在更复杂的真实场景中,类似问题可能会更加隐蔽。如果一个框架无法提供安全的实践方案,人为的错误在所难免,尤其是在大型项目中。因此,我们需要在架构设计时提供良好的规范,以减少错误的发生。

如何处理

因此,在一个应用中,状态应该被分为全局状态和局部状态。那么,Zustand如何实现局部状态呢?

我们可以通过React Context来注入局部状态。这个概念类似于React Query中的<QueryClientProvider>,以及Redux中的单一状态仓库。因为状态仓库的实例是静态的、单例的,不会频繁改变,所以将它们放到React Context中非常容易,并且不会导致不必要的重新渲染。然后,我们仍然可以为状态仓库创建订阅者,这些订阅者将通过Zustand进行优化。以下是具体的实现:

import { createStore, useStore } from 'zustand';

const BearStoreContext = React.createContext(null);

const BearStoreProvider = ({ children, initialBears }) => {
  const [store] = React.useState(() =>
    createStore((set) => ({
      bears: initialBears,
      actions: {
        increasePopulation: (by) =>
          set((state) => ({ bears: state.bears + by })),
        removeAllBears: () => set({ bears: 0 }),
      },
    }))
  );

  return (
    <BearStoreContext.Provider value={store}>
      {children}
    </BearStoreContext.Provider>
  );
};

在这里,我们没有使用开箱即用的create函数来创建实例,因为它返回的是一个React hook(useStore),而通过createStore可以返回一个独立的store对象,这是Zustand的新API。

我们使用了React.useState来创建store,虽然也可以使用React.useRef,但前者对TypeScript更加友好。使用useState的初始化方法只会调用一次,因此props的更新将不会传递到状态仓库中。

如果我们想要从状态仓库中取出一些值进行消费,可以使用这个上下文。为此,我们需要将store和selector传递给从Zustand中拿到的useStore钩子。以下是一个最佳实践的抽象:

const useBearStore = (selector) => {
  const store = React.useContext(BearStoreContext);
  if (!store) {
    throw new Error('Missing BearStoreProvider');
  }
  return useStore(store, selector);
};

相较于创建一个全局状态而言,这种方式虽然多了一些代码,但它解决了三个关键问题:

  1. 可以利用props初始化状态仓库:因为我们是在React组件树内部创建的store。
  2. 自动化测试更为容易:我们可以选择渲染一个包含BearStoreProvider的组件,或者为测试场景渲染一个独立的组件。这样,已创建的状态仓库能够完全隔离测试,无需在测试间重置状态仓库。
  3. 组件复用性增强:现在,一个组件可以渲染一个BearStoreProvider,为其子组件提供封装好的Zustand状态仓库。我们可以在一个页面中任意渲染这个组件,每个实例将拥有它独立的状态仓库,从而实现状态的隔离和复用。

即便Zustand文档中声称无需Context Provider也能访问状态仓库,但了解如何整合状态仓库的创建和React Context仍然是必要的,这样可以更好地处理一些需要封装和复用的场景。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
react2025-12-02
useEffect与useLayoutEffect对比
在 React Hooks 中, 和 都用于处理副作用逻辑,例如订阅、事件监听、DOM 操作等。但它们有着本质性的执行时机差异,这影响到页面渲染、布局抖动、性能表现等关键点。 1\. 执行时机的核心区别 在 React 的渲染流程中: 1. 渲染(Render phase) React 根据 state/props 计算 UI,生成虚拟 DOM,不访问真实 DOM。 2. 提交(Commit phase) React 将虚拟 DOM 变...
学习
react2025-08-03
在 React 中实现倒计时功能会有什么坑
倒计时 倒计时是一个非常常见的业务场景,但是在 React 中实现起来,却不算简单。 首先我们来看这段倒计时代码,它能否正常执行? 来看看实际表现效果 可以看到计时器在不断执行,但是 的值却没有变。 这是一个很经典的问题:React 闭包陷阱 。 我们来详细分析下: 的依赖数组 是空的,这意味着 effect 只会在组件挂载时执行一次,而不会在 状态更新时重新执行。所以 回调函数中捕获的 值始终是初始值 。 这个问题解决起来也很简单,有...
学习面试
react2025-07-30
React性能优化三剑客:memo、useMemo和useCallback详解
前言 在React开发中,性能优化是一个永恒的话题。今天我们就来深入探讨React提供的三个重要性能优化工具: 、 和 ,它们如何帮助我们构建更高效的React应用。 1\. 为什么需要性能优化? 🤔 React的核心机制是当组件的state或props发生变化时,组件会重新渲染。但有时这种重新渲染是不必要的: 父组件更新导致所有子组件重新渲染 ,即使子组件的props没有变化 复杂计算在每次渲染时重复执行 ,消耗大量资源 函数引用在...
学习面试
react2025-06-05
React 中 useDeferredValue 和 startTransition 的核心区别与使用场景
React 中 和 ,两者都用于优化性能,但适用场景和实现方式不同。 核心概念解析 1\. useDeferredValue:延迟值更新 作用 :告诉 React 延迟更新某个值 ,直到所有高优先级渲染任务完成。 适用场景 :当某个值的更新会触发计算密集型渲染(如复杂列表、图表),但需要优先保证其他高优先级 UI(如输入框)的响应。 机制 : 保持旧值显示,后台计算新值。 计算完成后,用新值更新 UI,期间允许用户继续交互。 示例 :...
学习
react2024-12-18
Antd 样式覆盖
&nbsp; 目前作者所在的业务正在升级 Antd5.0,不得不说 Antd5.0 真的太香了,但是由于团队的 UE 规范进行了大改版,Antd5.0 所有的 必须对齐最新的规范,因此就需要对 Antd5.0 组件做主题定制。 现状 目前针对项目中使用的 antd 高频组件进行了梳理,并且根据规范基于 design token 对这些高频组件进行样式定制,对于主题色、圆角、边框、字体等用户 高的主题都已经满足团队规范。 虽然 Desig...
学习
react2024-12-06
React 19 终于发布,一大波新功能正式升级
这篇文章主要介绍了 React 19 的新功能,包括 Actions 自动处理数据突变相关状态,新增的 useActionState、useFormStatus、useOptimistic 等 hook,新的 use API 读取渲染资源,新的 React DOM 静态 API 生成静态站点,以及 React 服务器组件中的 RSC 和 RSA 等内容。同时提到后续会分享针对旧版功能的优化和改良。 关联问题: React 19 性能如何...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录