Zustand轻量状态管理框架

在前两章中我们介绍了Redux及其进化版Redux Toolkit(RTK),虽然RTK简化了许多Redux的繁琐操作,但Redux的整个体系仍然过于笨重,除非是数据流极端复杂的超大型项目,一般来说RTK可能都不是最好的选择。Zustand(德语“状态”的意思)是一个基于React Hooks的轻量级状态管理框架,相比RTK它有以下优势:

API设计人性化:Zustand的API采用了极简设计,不像Redux那样需要严格按照Reducer、Action、Dispatch等概念编写代码,心智负担极低。

无需Provider:Zustand的Store不需要用Provider组件包裹应用,任何组件都可以直接引入并使用。

基于React Hooks:Zustand完全基于React Hooks设计,与函数式组件天然契合。

代码精简:Zustand的核心代码规模不大,对构建打包非常友好,而且代码清晰值得学习。

安装Zustand

执行以下命令安装Zustand。

npm i zustand

Zustand的基本使用

创建Store

Zustand中创建Store非常简单,只需要调用create函数并传入一个回调函数即可。回调函数接收set参数,用于更新Store中的状态数据,函数返回值就是Store的初始状态和操作方法。

store/useCounterStore.js

import { create } from "zustand";

const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

export default useCounterStore;

上面代码中,我们通过create函数创建了一个名为useCounterStore的Store。注意create的返回值本身就是一个Hooks函数,因此我们按照Hooks的命名规范以use开头命名。Store中包含了一个状态数据count和三个操作方法incrementdecrementreset

set函数用于更新状态数据,它支持两种用法:

  • 传入一个对象,例如set({ count: 0 }),直接设置状态值
  • 传入一个函数,例如set((state) => ({ count: state.count + 1 })),基于当前状态计算新状态

注意set函数默认执行的是浅合并(shallow merge),即只会合并顶层属性不会覆盖整个状态对象,如果要设置的某个属性是一个对象或数组,那么它就会被整体替换,这和React的useState行为一致。

补充说明:浅合并如何理解?浅合并是指set({ a: 1 })不会影响state.b,但如果state.user = { name: "Alice" },然后set({ user: { age: 25 } }),整个user对象会被替换,而不是合并。

在组件中使用Store

使用Zustand的Store非常直观,直接在组件中调用Store的Hooks函数即可,不需要任何Provider包裹。

App.jsx

import useCounterStore from "@/store/useCounterStore";

const App = () => {
  const { count, increment, decrement, reset } = useCounterStore();

  return (
    <div>
      <div>{count}</div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

export default App;

对比Redux Toolkit的写法,Zustand的代码量明显减少了很多,我们不需要useSelectoruseDispatch,也不需要Provider,直接从Store中解构出数据和方法就可以使用了。

Store部分订阅

上面代码中我们通过useCounterStore()获取了Store中的所有数据和方法,这意味着Store中任何数据发生变化都会触发组件的重新渲染。在实际开发中,如果一个Store中包含较多数据,而某个组件只关心其中一部分,我们可以仅部分订阅需要的数据,避免不必要的重新渲染。

App.jsx

import useCounterStore from "@/store/useCounterStore";

const DisplayComponent = () => {
  const count = useCounterStore((state) => state.count);

  return <div>{count}</div>;
};

const ControlComponent = () => {
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);

  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

const App = () => {
  return (
    <div>
      <DisplayComponent />
      <ControlComponent />
    </div>
  );
};

export default App;

上面代码中,DisplayComponent只订阅了count数据,而ControlComponent只订阅了操作方法。此时,当count发生变化时,只有DisplayComponent会重新渲染,而ControlComponent不会受到影响,避免了不必要的渲染开销。

此外,如果需要从Store中选取多个值,这可以通过返回对象的方式实现,但此时需要配合useShallow使用,否则每次都会返回一个新的对象引用从而导致重新渲染。

App.jsx

import { useShallow } from "zustand/react/shallow";
import useCounterStore from "@/store/useCounterStore";

const App = () => {
  const { count, increment } = useCounterStore(
    useShallow((state) => ({
      count: state.count,
      increment: state.increment,
    })),
  );

  return (
    <div>
      <div>{count}</div>
      <button onClick={increment}>+</button>
    </div>
  );
};

export default App;

处理异步操作

在Redux中,处理异步操作需要引入Redux Thunk或Redux Saga等中间件,理解这些库的逻辑相对复杂,心智负担极高。而在Zustand中,异步操作就像写普通的JavaScript函数一样简单,你只需要把函数标记为async异步函数即可,下面是一个例子。

store/useUserStore.js

import { create } from "zustand";

const useUserStore = create((set) => ({
  userList: [],
  loading: false,
  error: null,

  fetchUserList: async () => {
    set({ loading: true, error: null });
    try {
      const response = await fetch("/api/getUserList");
      const data = await response.json();
      set({ userList: data, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },

  createUser: async (userData) => {
    set({ loading: true, error: null });
    try {
      const response = await fetch("/api/createUser", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(userData),
      });
      const newUser = await response.json();
      set((state) => ({
        userList: [...state.userList, newUser],
        loading: false,
      }));
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
}));

export default useUserStore;

App.jsx

import { useEffect } from "react";
import useUserStore from "@/store/useUserStore";

const App = () => {
  const { userList, loading, error, fetchUserList } = useUserStore();

  useEffect(() => {
    fetchUserList();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      {userList.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
};

export default App;

使用中间件

Zustand提供了中间件机制用于扩展Store的功能,常用的内置中间件包括devtools调试工具集成、persist状态持久化等。

devtools中间件

devtools中间件可以将Zustand的Store接入浏览器的Redux DevTools扩展,方便我们进行状态调试。

import { create } from "zustand";
import { devtools } from "zustand/middleware";

const useCounterStore = create(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }),
    { name: "CounterStore" },
  ),
);

export default useCounterStore;

配置devtools中间件后,打开浏览器的Redux DevTools扩展即可看到Zustand Store的状态变化记录。

persist中间件

persist中间件可以将Store的状态持久化到localStoragesessionStorage中,实现页面刷新后状态不丢失的效果,省去了我们自己手动操作这些Storage的麻烦。

import { create } from "zustand";
import { persist } from "zustand/middleware";

const useSettingsStore = create(
  persist(
    (set) => ({
      theme: "light",
      language: "zh-cn",
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: "app-settings",
    },
  ),
);

export default useSettingsStore;

上面代码中,persist中间件的第2个参数是配置对象,name指定了在localStorage中存储的键名。当用户修改主题或语言设置后,刷新页面这些设置仍然会保留。除此之外,persist中间件还支持更多配置项,下面是一些例子。

persist(
  (set) => ({
    // ...
  }),
  {
    name: "app-settings",
    storage: createJSONStorage(() => sessionStorage), // 使用sessionStorage
    partialize: (state) => ({ theme: state.theme }), // 只持久化部分状态
  }
);

组合使用多个中间件

多个中间件可以嵌套组合使用。

import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

const useSettingsStore = create(
  devtools(
    persist(
      (set) => ({
        theme: "light",
        setTheme: (theme) => set({ theme }),
      }),
      { name: "app-settings" },
    ),
    { name: "SettingsStore" },
  ),
);

export default useSettingsStore;

Store拆分

随着应用规模的增长,将所有状态放在一个Store中会导致代码臃肿、难以维护。Store维护的最佳实践是按照业务领域将状态拆分到多个独立的Store中,每个Store管理一个特定领域的状态,这也是Zustand推荐的做法。

src
  |_ store
    |_ useCounterStore.js
    |_ useUserStore.js
    |_ useSettingsStore.js
    |_ ...
  |_ App.jsx
  |_ main.jsx

这种组织方式和Redux中将所有Slice合并到一个Store不同,Zustand中每个Store都是独立的,组件按需引入即可。这种方式的好处是各个Store之间天然解耦,代码组织清晰。

Store拆分后,一般来说是不需要互相访问的,如果有这种需求通常意味着拆分不合理。不过如果确实需要在一个Store中访问另一个Store的数据,Zustand也支持直接在Store外部调用另一个Store的getState()方法,该方法用于在React组件外部获取状态,它不会建立订阅关系,仅用于一次性读取。

import useUserStore from "./useUserStore";

const useOrderStore = create((set) => ({
  orders: [],
  createOrder: (orderData) => {
    const currentUser = useUserStore.getState().currentUser;
    // ...
  },
}));
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。