掘金文章归档 · 第 63 篇 React 生态 2/5 zustand · 状态管理

zustand 入门

基础类型 → 引用类型 → 监听 → useShallow → 异步 → immer → persist 全流程

原文作者:随意_(掘金) | juejin.cn/post/7350700368407756826 | 发布于 2024-03-27 | 阅读 308 · 约 7 分钟

0背景

最近公司某些项目需要 vue 迁移到 react 技术栈,技术调研 react 的状态管理工具 zustand,相较于 redux 简便不少,具体的计较可以查看 zustand 官方文档,下面主要讲解下 zustand 的基本用法。以下例子采用的是 zustand@4.5.2 版本。

1安装

npm install zustand

2基础类型

前提:在 src 目录下创建 store 文件夹用于统一管理状态,store 创建 useTest.ts 文件,内容如下:

import { create } from "zustand";

interface Istore {
  testName: string;
  setTestName: (str: string) => void;
}

export const useTestStore = create<Istore>()((set) => ({
  testName: "",
  setTestName: (str: string) => set(() => ({ testName: str })),
}));

创建 test.tsx 主页面,和组件 testA、testB 组件:

import TestA from "./a";
import TestB from "./b";

const Test: React.FC = () => {
  return (
    <div>
      <TestA></TestA>
      <hr></hr>
      <TestB></TestB>
    </div>
  );
};

export default Test;
import { useTestStore } from "@/store/useTest";

const TestA: React.FC = () => {
  const { testName, setTestName } = useTestStore();

  return (
    <div>
      <span>testName:{testName}</span>
      <button onClick={() => setTestName("testA")}>更改testName</button>
    </div>
  );
};

export default TestA;
import { useTestStore } from "@/store/useTest";

const TestB: React.FC = () => {
  const { testName, setTestName } = useTestStore();

  return (
    <div>
      <span>testName:{testName}</span>
      <button onClick={() => setTestName("testB")}>更改testName</button>
    </div>
  );
};

export default TestB;

此时可见两组间共享状态,一个变动另一个自动刷新。

截图占位:TestA / TestB 两组组件共享 testName 状态

3引用类型

useTest.ts:

import { create } from "zustand";

interface Iinfo {
  name: string;
  age: number;
}

interface Istore {
  testName: string;
  userInfo: Iinfo;
  setTestName: (str: string) => void;
  setUserInfo: (data: Iinfo) => void;
  setUserInfoName: (str: string) => void;
  setUserInfoAge: (str: number) => void;
}

export const useTestStore = create<Istore>()((set) => ({
  testName: "默认值",
  setTestName: (str: string) => set(() => ({ testName: str })),
  userInfo: {
    name: "张三",
    age: 18,
  },
  setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
  setUserInfoName: (name: string) =>
    set((state) => ({ userInfo: { ...state.userInfo, name } })),
  setUserInfoAge: (age: number) =>
    set((state) => ({ userInfo: { ...state.userInfo, age } })),
}));

testA 组件:

import { useTestStore } from "@/store/useTest";

const TestA: React.FC = () => {
  const {
    userInfo,
    setUserInfo,
    setUserInfoName,
    setUserInfoAge,
    testName,
    setTestName,
  } = useTestStore();

  return (
    <div>
      <div>testName:{testName}</div>
      <div>userInfoName:{userInfo.name}</div>
      <div>userInfoAge:{userInfo.age}</div>
      <button onClick={() => setTestName("testA")}>更改testName</button>
      <button onClick={() => setUserInfo({ name: "更改了", age: 30 })}>
        更改userInfo
      </button>
      <button onClick={() => setUserInfoName("李四")}>更改userInfoName</button>
      <button onClick={() => setUserInfoAge(90)}>更改userInfoAge</button>
    </div>
  );
};

export default TestA;

testB:

import { useTestStore } from "@/store/useTest";

const TestB: React.FC = () => {
  const { userInfo, testName, setTestName } = useTestStore();

  return (
    <div>
      <div>testName:{testName}</div>
      <div>userInfoName:{userInfo.name}</div>
      <div>userInfoAge:{userInfo.age}</div>
      <button onClick={() => setTestName("testB")}>更改testName</button>
    </div>
  );
};

export default TestB;

此时可见 AB 组件引用类型更新。

📌 局部改对象里某个字段时用 set((state) => ({ userInfo: { ...state.userInfo, name } })),保证引用类型不可变更新。

4action 中获取 state

加一个计数器,每次点击 store 加一,需要在 action 获取 state,用法没变化:

import { create } from "zustand";

interface Iinfo {
  name: string;
  age: number;
}

interface Istore {
  count: number;
  testName: string;
  userInfo: Iinfo;
  setTestName: (str: string) => void;
  setUserInfo: (data: Iinfo) => void;
  setUserInfoName: (str: string) => void;
  setUserInfoAge: (str: number) => void;
  setCount: () => void;
}

export const useTestStore = create<Istore>()((set) => ({
  testName: "默认值",
  setTestName: (str: string) => set(() => ({ testName: str })),
  userInfo: {
    name: "张三",
    age: 18,
  },
  setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
  setUserInfoName: (name: string) =>
    set((state) => ({ userInfo: { ...state.userInfo, name } })),
  setUserInfoAge: (age: number) =>
    set((state) => ({ userInfo: { ...state.userInfo, age } })),
  count: 0,
  setCount: () => set((state) => ({ count: state.count + 1 })),
}));

5监听变动

A 组件变更了 count 计数器,B 组件需要监听 count 变动,回调函数,监听全部的 store 变化和特定的 count 变化。B 组件代码如下,组件卸载调用取消监听。useTestStore.subscribe 返回值为取消监听,调用即可取消监听。

import { useEffect } from "react";

import type { Istore } from "@/store/useTest";
import { useTestStore } from "@/store/useTest";

const TestB: React.FC = () => {
  const { userInfo, testName, setTestName } = useTestStore();

  // 在组件挂载时订阅 store 变化 只要 store 变化均可触发
  useEffect(() => {
    // 监听所有更改,每次更改时都会同步触发
    const unsubscribeALL = useTestStore.subscribe(
      (state: Istore, prevState: Istore) => {
        console.log(state, prevState);
      }
    );
    // 在组件卸载时取消订阅
    return () => {
      unsubscribeALL();
    };
  }, []);

  // 在组件挂载时订阅 store 变化 仅仅监听 count 变化
  useEffect(() => {
    // 监听所有更改,每次更改时都会同步触发
    const unsubscribeCount = useTestStore.subscribe(
      (state: Istore, prevState: Istore) => {
        if (state.count !== prevState.count) {
          console.log(state, prevState);
        }
      }
    );
    // 在组件卸载时取消订阅
    return () => {
      unsubscribeCount();
    };
  }, []);

  return (
    <div>
      <div>testName:{testName}</div>
      <div>userInfoName:{userInfo.name}</div>
      <div>userInfoAge:{userInfo.age}</div>
      <button onClick={() => setTestName("testB")}>更改testName</button>
    </div>
  );
};

export default TestB;
💡 subscribe 的返回值就是取消监听函数,useEffect 的 cleanup 里直接调用即可,避免内存泄漏。

6选择性订阅 useShallow

在 Zustand 中,useShallow 是一个用于选择性地订阅 store 中状态变化的自定义钩子。可以选择拼装自己需要的状态,有点类似于 vue 的 computed,注意两种写法。

import { useShallow } from "zustand/react/shallow";

const TestB: React.FC = () => {
  const { userInfo, testName, setTestName, count } = useTestStore();

  // 自定义组装数据 1:对象写法
  const { count1, userInfo1 } = useTestStore(
    useShallow((state) => ({ count1: state.count, userInfo1: state.userInfo }))
  );
  // 自定义组装数据 2:数组写法
  // const [count1, testName1] = useTestStore(
  //   useShallow((state) => [state.count, state.testName])
  // )

  return (
    <div>
      <div>testName:{testName}</div>
      <div>count:{count}</div>
      <div>userInfoName:{userInfo.name}</div>
      <div>userInfoAge:{userInfo.age}</div>
      <div>count1:{count1}</div>
      <div>userInfo1.name:{userInfo1.name}</div>
      <button onClick={() => setTestName("testB")}>更改testName</button>
    </div>
  );
};

export default TestB;
📌 useShallow 做浅比较:组装对象/数组时只有所选字段变了才触发重渲染,避免每次都刷新。

7action 异步设置

注意:setUserInfoNameAsync 异步设置 state。

import { create } from "zustand";

interface Iinfo {
  name: string;
  age: number;
}

function delayedPromise(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("异步处理名称");
    }, 300);
  });
}

export interface Istore {
  count: number;
  testName: string;
  userInfo: Iinfo;
  setTestName: (str: string) => void;
  setUserInfo: (data: Iinfo) => void;
  setUserInfoName: (str: string) => void;
  setUserInfoNameAsync: () => void;
  setUserInfoAge: (str: number) => void;
  setCount: () => void;
}

export const useTestStore = create<Istore>()((set) => ({
  testName: "默认值",
  setTestName: (str: string) => set(() => ({ testName: str })),
  userInfo: {
    name: "张三",
    age: 18,
  },
  setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
  setUserInfoName: (name: string) =>
    set((state) => ({ userInfo: { ...state.userInfo, name } })),
  setUserInfoNameAsync: async () => {
    const nameStr = await delayedPromise();
    set((state) => ({
      userInfo: { ...state.userInfo, name: nameStr },
    }));
  },
  setUserInfoAge: (age: number) =>
    set((state) => ({ userInfo: { ...state.userInfo, age } })),
  count: 0,
  setCount: () => set((state) => ({ count: state.count + 1 })),
}));
📌 异步 action 没有特殊 API:async 函数里 await 完之后再调 set 即可,写法与同步 action 一致。

8引入 immer 简化复杂对象操作

对用惯 vue 的来说,react 每次更改 state 需要先创建副本,再赋值 setState,操作起来麻烦,zustand 借助 immer 来简化操作。

npm i immer -S

修改 useTest.ts:

import { produce } from "immer";
import { create } from "zustand";

interface Iinfo {
  name: string;
  age: number;
}

interface ComplexObjectType {
  nested: {
    value: string;
  };
  array: number[];
}

function delayedPromise(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("异步处理名称");
    }, 300);
  });
}

export interface Istore {
  count: number;
  testName: string;
  userInfo: Iinfo;
  setTestName: (str: string) => void;
  setUserInfo: (data: Iinfo) => void;
  setUserInfoName: (str: string) => void;
  setUserInfoNameAsync: () => void;
  setUserInfoAge: (str: number) => void;
  setCount: () => void;
  ImmerObject: ComplexObjectType;
  updateImmerObject: (updater: (draft: ComplexObjectType) => void) => void;
}

export const useTestStore = create<Istore>()((set) => ({
  testName: "默认值",
  setTestName: (str: string) => set(() => ({ testName: str })),
  userInfo: {
    name: "张三",
    age: 18,
  },
  setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
  setUserInfoName: (name: string) =>
    set((state) => ({ userInfo: { ...state.userInfo, name } })),
  setUserInfoNameAsync: async () => {
    const nameStr = await delayedPromise();
    set((state) => ({
      userInfo: { ...state.userInfo, name: nameStr },
    }));
  },
  setUserInfoAge: (age: number) =>
    set((state) => ({ userInfo: { ...state.userInfo, age } })),
  count: 0,
  setCount: () => set((state) => ({ count: state.count + 1 })),
  // immer 初始状态
  ImmerObject: {
    nested: {
      value: "Immer",
    },
    array: [1, 2, 3],
  },

  // 使用 immer 的 produce 函数来更新复杂对象
  updateImmerObject: (updater: (draft: ComplexObjectType) => void) =>
    set(
      produce((draft) => {
        updater(draft.ImmerObject);
      })
    ),
}));

A 组件:

import { useTestStore } from "@/store/useTest";

const TestA: React.FC = () => {
  const {
    userInfo,
    count,
    setUserInfo,
    setUserInfoName,
    setUserInfoAge,
    testName,
    setTestName,
    setCount,
    setUserInfoNameAsync,
    ImmerObject,
    updateImmerObject,
  } = useTestStore();

  const handleUpdate = () => {
    updateImmerObject((draft) => {
      draft.nested.value = "ImmerObject change";
      draft.array.push(4);
    });
  };

  return (
    <div>
      <div>testName:{testName}</div>
      <div>userInfoName:{userInfo.name}</div>
      <div>userInfoAge:{userInfo.age}</div>
      <div>count:{count}</div>

      <p>ImmerObject Value: {ImmerObject.nested.value}</p>
      <p>ImmerObject Array: {ImmerObject.array.join(", ")}</p>

      <button onClick={() => setTestName("testA")}>更改testName</button>
      <button onClick={() => setUserInfo({ name: "更改了", age: 30 })}>
        更改userInfo
      </button>
      <button onClick={() => setUserInfoName("李四")}>更改userInfoName</button>
      <button onClick={() => setUserInfoAge(90)}>更改userInfoAge</button>
      <button onClick={setCount}>更改count</button>
      <button onClick={setUserInfoNameAsync}>setUserInfoNameAsync</button>
      <button onClick={handleUpdate}>Update Immer Object</button>
    </div>
  );
};

export default TestA;
📌 produce 里直接 draft.nested.value = xxx、draft.array.push(4),像 vue 一样"改原对象",immer 自动产出不可变副本。

9实现永久缓存 persist

对于用户信息等状态,需要永久存储,否则一刷新就没了。新建一个 useUser.ts 的 store,devtools、persist 实现 local 缓存。

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

interface Iinfo {
  id: string;
  name: string;
  token: string;
}

interface BearState {
  userInfo: Iinfo;
  setUserInfo: (data: Iinfo) => void;
}

export const useUserStore = create<BearState>()(
  devtools(
    persist(
      (set) => ({
        userInfo: {
          id: "",
          name: "",
          token: "",
        },
        setUserInfo: (data: Iinfo) => set(() => ({ userInfo: data })),
      }),
      {
        name: "userInfo",
      }
    )
  )
);
📌 persist 包一层 + name 参数,状态自动写入 localStorage,刷新页面不丢;devtools 提供 Redux DevTools 调试支持。

10结尾

以上就是 zustand 基础用法,入门而已。全套示例代码已在上文各节完整给出(基础类型 → 引用类型 → 计数器 → 监听 → useShallow → 异步 → immer → persist 递进组合),可直接组合成完整 store 与 TestA/TestB 页面运行。