Zustand 状态管理:从入门到实践

news2025/3/31 1:18:55

Zustand 状态管理:从入门到实践

Zustand 是一个轻量、快速且灵活的 React 状态管理库。它基于 Hooks API,提供了简洁的接口来创建和使用状态,同时易于扩展和优化。本文将通过一个 TODO 应用实例带你快速入门 Zustand,并探讨其核心概念、性能优化技巧以及一些高级用法。

快速入门:构建一个 TODO 应用

让我们通过实现一个经典的 TODO 应用来熟悉 Zustand 的基本用法。

1. 安装

首先,你需要安装 Zustand:

# 使用 npm
npm install zustand

# 或者使用 yarn
yarn add zustand

2. 创建 Store

接下来,我们使用 Zustand 的 create 函数来定义我们的状态存储(Store)。Store 包含了应用所需的状态以及更新这些状态的方法。

import { create } from 'zustand';

// 定义 Todo 项的类型 (可选,但推荐使用 TypeScript)
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

// 定义 Store 的状态和操作类型 (可选)
interface TodoState {
  filter: 'all' | 'completed' | 'incompleted';
  todos: Todo[];
  setFilter: (filter: TodoState['filter']) => void;
  setTodos: (fn: (prevTodos: Todo[]) => Todo[]) => void;
  // 如果需要添加 Todo 的方法,也可以在这里定义
  // addTodo: (title: string) => void;
}

// 用于生成唯一 ID (简化示例)
let keyCount = 0;

const useStore = create<TodoState>((set) => ({
  // --- 状态 (State) ---
  filter: 'all', // 当前筛选条件,默认为 'all'
  todos: [],     // Todo 列表,初始为空数组

  // --- 操作 (Actions) ---
  // 设置筛选条件
  setFilter(filter) {
    // set 函数用于更新状态,它接收一个对象,包含要更新的部分状态
    set({ filter });
  },

  // 更新 Todo 列表
  setTodos(fn) {
    // set 函数也可以接收一个函数,参数是当前状态 (prev)
    // 这对于基于先前状态进行更新非常有用,可以避免竞态条件
    set((prev) => ({ todos: fn(prev.todos) }));
  },

  // 示例:添加 Todo 的 Action (可以直接在 Store 中定义)
  // addTodo(title) {
  //   set((prev) => ({
  //     todos: [
  //       ...prev.todos,
  //       { title, completed: false, id: keyCount++ },
  //     ],
  //   }));
  // },
}));

export default useStore;

在这个 Store 中,我们定义了:

  • filter: 代表当前的筛选选项(all - 全部, completed - 已完成, incompleted - 未完成)。
  • todos: 一个数组,存储所有的代办事项对象。每个对象包含 id (唯一标识), title (事项名称) 和 completed (是否完成)。
  • setFilter: 一个 Action (操作),用于修改 filter 状态。
  • setTodos: 一个 Action,用于修改 todos 状态。它接收一个函数作为参数,该函数接收当前的 todos 数组并返回新的 todos 数组,确保状态更新的原子性和安全性。

3. 构建 React 组件

现在我们来构建构成 TODO 应用界面的 React 组件。整个应用大致分为三块:输入和过滤控制区、Todo 列表展示区。

[图片:TODO 应用界面截图 - 展示输入框、过滤按钮和 Todo 列表]

App 组件 (入口与表单处理)

App 组件作为应用的根组件,包含添加新 Todo 的表单逻辑。

import React from 'react';
import useStore from './store'; // 引入我们创建的 Store Hook
import Filter from './Filter';
import Filtered from './Filtered';

// 用于生成唯一 ID (简化示例,应与 store 中的 keyCount 保持一致或使用更健壮的 ID 生成方式)
let keyCount = 0;

const App = () => {
  // 从 Store 中获取更新 todos 的方法
  const { setTodos } = useStore();

  // 处理表单提交事件,用于添加新的 Todo
  const add = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // 阻止表单默认提交行为
    const form = e.currentTarget;
    const inputElement = form.elements.namedItem('inputTitle') as HTMLInputElement;
    const title = inputElement.value.trim(); // 获取输入框的值并去除首尾空格

    if (title) { // 确保标题不为空
      inputElement.value = ''; // 清空输入框
      // 调用 setTodos 更新状态,添加新的 Todo 项
      setTodos((prevTodos) => [
        ...prevTodos,
        { title, completed: false, id: keyCount++ }, // 创建新的 Todo 对象
      ]);
    }
  };

  return (
    <div className="todo-app">
      <h1>My Todos</h1>
      <form onSubmit={add}>
        {/* 过滤组件 */}
        <Filter />
        {/* 输入框 */}
        <input name="inputTitle" placeholder="Add a new todo..." autoComplete="off" />
        {/* 列表展示组件 */}
        <Filtered />
        {/* 隐藏的提交按钮或依赖 Enter 键提交 */}
        <button type="submit" style={{ display: 'none' }}>Add</button>
      </form>
    </div>
  );
};

export default App;

App 组件中:

  • 我们使用 useStore() 获取 setTodos Action。
  • add 函数在表单提交时触发(通常是按下 Enter 键),它获取输入框的值,创建一个新的 Todo 对象,并使用 setTodos 将其添加到 Store 的 todos 数组中,最后清空输入框。
Filter 组件 (过滤选项)

Filter 组件提供单选按钮,让用户可以选择查看所有、已完成或未完成的 Todo。

import React from 'react';
import { Radio } from 'antd'; // 假设使用 antd UI 库
import useStore from './store';

const Filter = () => {
  // 从 Store 中获取 filter 状态和 setFilter Action
  const { filter, setFilter } = useStore();

  return (
    <div className="filter-controls">
      <Radio.Group onChange={(e) => setFilter(e.target.value)} value={filter}>
        <Radio value="all">All</Radio>
        <Radio value="completed">Completed</Radio>
        <Radio value="incompleted">Incompleted</Radio>
      </Radio.Group>
    </div>
  );
};

export default Filter;
  • Filter 组件从 useStore 获取当前的 filter 值和 setFilter Action。
  • 当用户点击不同的 Radio 按钮时,onChange 事件触发,调用 setFilter 更新 Store 中的 filter 状态。
Filtered 组件 (展示过滤后的列表)

Filtered 组件负责根据当前的 filter 状态,从 Store 中获取 todos 列表,进行筛选,并渲染最终的 Todo 列表。这里还使用了 react-spring 来添加简单的动画效果。

import React from 'react';
import { useTransition, animated as a } from '@react-spring/web'; // 引入动画库
import useStore from './store';
import TodoItem from './TodoItem'; // 引入单个 Todo 项组件

const Filtered = () => {
  // 从 Store 中获取 todos 列表和 filter 状态
  const { todos, filter } = useStore();

  // 根据 filter 状态筛选 todos
  const filteredTodos = todos.filter((todo) => {
    if (filter === 'all') return true; // 显示全部
    if (filter === 'completed') return todo.completed; // 显示已完成
    return !todo.completed; // 显示未完成 (filter === 'incompleted')
  });

  // 使用 react-spring 创建列表项的进入/离开动画
  const transitions = useTransition(filteredTodos, {
    keys: (todo) => todo.id, // 使用 Todo 的唯一 ID 作为 key
    from: { opacity: 0, height: 0 },
    enter: { opacity: 1, height: 40 }, // 假设每个 Todo 项高度为 40px
    leave: { opacity: 0, height: 0 },
    config: { tension: 280, friction: 25 } // 动画物理效果配置
  });

  return (
    <div className="todo-list">
      {transitions((style, item) => (
        // 使用 animated.div 应用动画样式
        <a.div className="item" style={style}>
          {/* 渲染单个 Todo 项 */}
          <TodoItem item={item} />
        </a.div>
      ))}
      {filteredTodos.length === 0 && <p className="empty-message">No todos here!</p>}
    </div>
  );
};

export default Filtered;
  • Filtered 组件读取 todosfilter 状态。
  • 它根据 filter 的值计算出 filteredTodos
  • useTransition (来自 react-spring) 用于为列表项添加平滑的进入和离开动画。
  • 每个筛选后的 Todo 项被传递给 TodoItem 组件进行渲染。
TodoItem 组件 (单个 Todo 项)

TodoItem 组件负责渲染单个 Todo 项,并处理完成状态切换和删除操作。

import React from 'react';
import { CloseOutlined } from '@ant-design/icons'; // 假设使用 antd 图标
import useStore from './store';
import { Todo } from './store'; // 引入 Todo 类型定义

interface TodoItemProps {
  item: Todo;
}

const TodoItem = ({ item }: TodoItemProps) => {
  // 获取 setTodos Action
  const { setTodos } = useStore();
  const { title, completed, id } = item;

  // 切换 Todo 的完成状态
  const toggleCompleted = () =>
    setTodos((prevTodos) =>
      prevTodos.map((prevItem) =>
        prevItem.id === id ? { ...prevItem, completed: !completed } : prevItem,
      ),
    );

  // 删除 Todo 项
  const remove = () => {
    setTodos((prevTodos) => prevTodos.filter((prevItem) => prevItem.id !== id));
  };

  return (
    <>
      <input
        type="checkbox"
        checked={completed}
        onChange={toggleCompleted}
        aria-label={`Mark ${title} as ${completed ? 'incomplete' : 'complete'}`}
      />
      <span style={{ textDecoration: completed ? 'line-through' : 'none' }}>
        {title}
      </span>
      <CloseOutlined
        onClick={remove}
        style={{ cursor: 'pointer', marginLeft: '8px' }}
        aria-label={`Remove ${title}`}
      />
    </>
  );
};

export default TodoItem;
  • TodoItem 接收一个 item (Todo 对象) 作为 prop。
  • toggleCompleted 函数通过 setTodos 更新对应 ID 的 Todo 项的 completed 状态。
  • remove 函数通过 setTodos 过滤掉当前 ID 的 Todo 项,实现删除。

至此,一个基本的 TODO 应用就完成了。我们通过 create 定义了状态和操作,并通过 useStore Hook 在组件中访问和更新状态。

使用 Immer 简化嵌套状态更新

当状态结构变得复杂,例如包含多层嵌套对象时,直接使用展开运算符(...)进行不可变更新会变得非常冗长且容易出错。

考虑以下嵌套状态:

const nestedObject = {
  deep: {
    nested: {
      obj: {
        count: 0,
      },
    },
  },
};

如果想更新 count,传统的 set 写法会是:

const useStore = create((set) => ({
  nestedObject,
  updateState() {
    set(prevState => ({
      nestedObject: {
        ...prevState.nestedObject,
        deep: {
          ...prevState.nestedObject.deep,
          nested: {
            ...prevState.nestedObject.deep.nested,
            obj: {
              ...prevState.nestedObject.deep.nested.obj,
              count: prevState.nestedObject.deep.nested.obj.count + 1, // 更新 count
            },
          },
        },
      },
    }));
  },
}));

这种写法非常繁琐。幸运的是,Zustand 可以与 Immer 库无缝集成,极大地简化深层嵌套状态的更新。

首先,安装 Immer:

npm install immer
# or
yarn add immer

然后,在 set 函数中使用 Immer 的 produce

import { create } from 'zustand';
import { produce } from 'immer'; // 引入 produce

const useStore = create((set) => ({
  nestedObject,
  updateState() {
    // 使用 produce 包装更新逻辑
    set(produce((state) => {
      // 在 produce 回调中,可以直接修改 state (Immer 会处理不可变性)
      state.nestedObject.deep.nested.obj.count += 1;
    }));
  },
}));

使用 Immer 后,代码变得清晰简洁,就像直接修改对象一样。

注意: 虽然 Immer 提高了开发效率,但它在内部执行了额外的操作来保证不可变性。对于服务端渲染 (SSR) 或性能极其敏感的场景,相比于手动解构,Immer 可能会带来微小的 CPU 开销。请根据实际情况权衡。

优化性能:状态选取 (Selectors)

默认情况下,当你在组件中使用 useStore() 获取状态时,例如 const { todos, filter } = useStore();,只要 Store 中的 任何 状态发生变化,该组件都会重新渲染 (re-render)。

考虑以下场景:

let renderCount = 0;

// Display 组件只依赖 todos
const Display = () => {
  renderCount++;
  console.log('Display re-rendered:', renderCount);
  const { todos } = useStore(); // 获取整个 store 或多个属性
  return (
    <div>
      {/* ... 渲染 todos ... */}
    </div>
  );
};

// Control 组件只调用 setFilter
const Control = () => {
  const { setFilter } = useStore();
  return <button onClick={() => setFilter('completed')}>Set Filter to Completed</button>;
};

const App = () => (
  <>
    <Display />
    <Control />
  </>
);

当点击 Control 组件的按钮时,filter 状态更新了。尽管 Display 组件根本不使用 filter,它仍然会重新渲染。这是因为 Zustand 在状态更新后,默认使用 Object.is 比较 useStore() 返回的对象。由于每次状态更新都会产生一个新的 Store 状态对象,即使 todos 本身没变,useStore() 返回的对象引用也变了,导致 Object.is 比较结果为 false,从而触发 Display 组件的重渲染。

解决方案:使用 Selector 函数

为了解决这个问题,useStore Hook 接受一个可选的 selector 函数 作为第一个参数。这个函数接收整个 state 对象,并返回你真正需要的部分。Zustand 会比较 selector 函数 返回的值 是否发生变化,而不是比较整个 state 对象。

Display 组件修改为:

const Display = () => {
  renderCount++;
  console.log('Display re-rendered:', renderCount);
  // 使用 selector 函数只选取 todos
  const todos = useStore((state) => state.todos);
  return (
    <div>
      {/* ... 渲染 todos ... */}
    </div>
  );
};

现在,当 filter 变化时,state.todos 的引用没有改变,Object.is(prevTodos, currentTodos) 返回 trueDisplay 组件就不会再进行不必要的重渲染了。

最佳实践: 始终使用 selector 函数来精确选取组件所需的状态片段,避免不必要的渲染。

辅助工具:createSelectors

为了简化为每个状态属性编写 selector 的过程,并提供类似 store.use.propertyName() 的便捷语法,社区提供了一个 createSelectors 辅助函数(注意:这不是 Zustand 核心库的一部分,需要自行实现或引入)。

import { StoreApi, UseBoundStore } from 'zustand';

// 类型定义,为 Store 添加 use 属性
type WithSelectors<S> = S extends { getState: () => infer T }
  ? S & { use: { [K in keyof T]: () => T[K] } }
  : never;

// createSelectors 函数实现
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
  _store: S,
) => {
  const store = _store as WithSelectors<typeof _store>;
  store.use = {}; // 初始化 use 对象
  // 遍历 store 的所有 state keys
  for (const k of Object.keys(store.getState())) {
    // 为每个 key 创建一个 selector hook 并挂载到 use 对象上
    (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
  }
  return store;
};

// --- 使用 createSelectors ---

// 1. 先创建基础 store
const useStoreBase = create<TodoState>((set) => ({
  filter: 'all',
  todos: [],
  setFilter(filter) {
    set({ filter });
  },
  setTodos(fn) {
    set((prev) => ({ todos: fn(prev.todos) }));
  },
}));

// 2. 使用 createSelectors 包装基础 store
const useStore = createSelectors(useStoreBase);

// --- 在组件中使用 ---
// 获取状态属性
const todos = useStore.use.todos();
const filter = useStore.use.filter();

// 获取 Action (方法)
const setTodos = useStore.use.setTodos();
const setFilter = useStore.use.setFilter();

使用 createSelectors 后,可以通过 useStore.use.propertyName() 的方式获取状态,内部自动应用了 selector,减少了手动编写 selector 的模板代码,并降低了忘记使用 selector 的风险。

优化性能:浅层比较 (Shallow Comparison)

当你需要在一个组件中选取 多个 状态片段时,即使使用了 selector,也可能遇到不必要的重渲染。

考虑以下情况:

const MyComponent = () => {
  // 同时选取 todos 和 setFilter
  const { todos, setFilter } = useStore((state) => ({
    todos: state.todos,
    setFilter: state.setFilter,
  }));
  // ...
}

在这个例子中,selector 函数返回一个包含 todossetFilter 的新对象 { todos: ..., setFilter: ... }。当 Store 中 任何 状态(比如 filter)发生变化时,这个 selector 函数会重新执行,即使 state.todosstate.setFilter 本身没有改变,它也会返回一个 新的对象引用。由于 Object.is 比较的是对象引用,它会认为状态发生了变化,导致组件重渲染。

解决方案:使用浅层比较

Zustand 提供了 shallow 函数来进行浅层比较。它会比较 selector 返回对象的第一层属性值是否发生变化,而不是比较对象本身的引用。

shallow 作为 useStore 的第二个参数传入:

import { shallow } from 'zustand/shallow'; // 引入 shallow

const MyComponent = () => {
  const { todos, setFilter } = useStore(
    (state) => ({
      todos: state.todos,
      setFilter: state.setFilter,
    }),
    shallow, // 使用 shallow 进行比较
  );
  // ...
}

现在,只有当 state.todosstate.setFilter 的值(或引用,对于非原始类型)真正发生变化时,组件才会重渲染。

推荐用法:useShallow Hook

Zustand 还提供了一个更简洁的 useShallow Hook (来自 zustand/react/shallow),它是目前推荐的进行浅层比较的方式:

import { useShallow } from 'zustand/react/shallow'; // 引入 useShallow

const MyComponent = () => {
  // 使用 useShallow 包装 selector
  const { todos, setFilter } = useStore(
    useShallow((state) => ({
      todos: state.todos,
      setFilter: state.setFilter,
    })),
  );
  // ...
}

useShallow 内部封装了 selector 和 shallow 比较逻辑,使得代码更易读。

总结:

  • 选取单个状态片段时,直接使用 selector 函数:const todos = useStore(state => state.todos);
  • 选取多个状态片段时,使用 useShallow 包装 selector:const { todos, filter } = useStore(useShallow(state => ({ todos: state.todos, filter: state.filter })));

处理异步操作

Zustand 的 Action 天然支持异步操作。你可以直接在 Action 中编写 async/await 代码来处理数据获取、API 调用等异步任务,并在完成后使用 set 更新状态。

import { create } from 'zustand';

interface AsyncState {
  todos: Todo[] | null; // 初始可能为 null
  error: Error | null;
  isLoading: boolean;
  fetchData: () => Promise<void>;
}

const useAsyncStore = create<AsyncState>((set) => ({
  todos: null,
  error: null,
  isLoading: false,

  fetchData: async () => {
    set({ isLoading: true, error: null }); // 开始加载,清除旧错误
    try {
      const res = await fetch(`https://jsonplaceholder.typicode.com/todos`);
      if (!res.ok) {
        throw new Error(`HTTP error! status: ${res.status}`);
      }
      const todos = await res.json();
      set({ todos, isLoading: false }); // 成功,更新 todos,结束加载
    } catch (error) {
      console.error("Failed to fetch todos:", error);
      set({ error: error as Error, isLoading: false }); // 失败,记录错误,结束加载
    }
  },
}));

// --- 在 React 组件中使用 ---
import React, { useEffect } from 'react';
import useAsyncStore from './asyncStore';

function App() {
  // 从 store 获取状态和 action
  const { todos, error, isLoading, fetchData } = useAsyncStore();

  // 组件挂载时触发数据获取
  useEffect(() => {
    fetchData();
  }, [fetchData]); // fetchData 通常是稳定的,但作为依赖项是好习惯

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!todos) return <div>No data yet.</div>; // 可能在加载完成但无数据时

  return (
    <div>
      <h1>Fetched Todos</h1>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

在这个例子中:

  • fetchData 是一个 async 函数,它负责调用 API。
  • 在请求开始时,通过 setisLoading 设为 true
  • 请求成功后,通过 set 更新 todos 并将 isLoading 设为 false
  • 请求失败后,通过 set 记录 error 并将 isLoading 设为 false
  • React 组件根据 isLoading, error, todos 的状态来决定渲染哪个视图(加载中、错误信息、Todo 列表)。

模块化:拆分 Store (Slices)

随着应用功能的增加,单个 Store 文件可能会变得庞大而难以维护。Zustand 支持将 Store 拆分成多个逻辑相关的 “切片” (Slices),然后在主 Store 中将它们组合起来。每个 Slice 本质上是一个函数,它接收 set, get 等参数,并返回该切片的状态和操作。

示例:创建 Bear 和 Fish 切片

// bearSlice.js
export const createBearSlice = (set, get) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => {
    // 可以在一个 slice 中调用 set 来影响其他 slice 的状态 (如果它们被合并在同一个 store 中)
    // 注意:这里假设 fish slice 也在同一个 store 中且有 fishes 属性
    // 更好的方式可能是通过 get() 获取其他 slice 的 action 来调用
    // 或者定义跨 slice 的 action (见下文)
    // set((state) => ({ fishes: state.fishes - 1 })) // 这种直接修改其他 slice 状态的方式需要谨慎
  },
});

// fishSlice.js
export const createFishSlice = (set, get) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
});

合并 Slices

在主 Store 文件中,使用 create 并将各个 Slice 的结果合并。

// useBoundStore.js
import { create } from 'zustand';
import { createBearSlice } from './bearSlice';
import { createFishSlice } from './fishSlice';

// 合并 slices,将它们的属性直接展开到根级别
export const useBoundStore = create((...a) => ({
  ...createBearSlice(...a), // a 包含了 set, get, api 等参数
  ...createFishSlice(...a),
}));

/*
// 另一种合并方式:创建命名空间 (如果希望按模块访问)
export const useBoundStoreWithNamespaces = create((...a) => ({
  bearSlice: { ...createBearSlice(...a) },
  fishSlice: { ...createFishSlice(...a) },
}));
// 使用时:useBoundStoreWithNamespaces(state => state.bearSlice.bears)
*/

在 React 组件中使用合并后的 Store

使用方式与普通 Store 完全相同,因为所有状态和操作都被合并到了顶层。

import React from 'react';
import { useBoundStore } from './stores/useBoundStore';

function App() {
  // 使用 selector 精确选取所需状态和操作
  const bears = useBoundStore((state) => state.bears);
  const fishes = useBoundStore((state) => state.fishes);
  const addBear = useBoundStore((state) => state.addBear);
  const addFish = useBoundStore((state) => state.addFish); // 假设需要 addFish

  return (
    <div>
      <h2>Number of bears: {bears}</h2>
      <h2>Number of fishes: {fishes}</h2>
      <button onClick={addBear}>Add a bear</button>
      <button onClick={addFish}>Add a fish</button>
    </div>
  );
}

export default App;

更新跨 Slice 的状态

如果一个操作需要同时更新多个 Slice 中的状态,有几种方式:

  1. 在 Action 中多次调用 set (不推荐,可能触发多次更新)。
  2. 在 Action 中一次性 set 多个状态 (如果状态都在一个 Slice 控制)。
  3. 定义一个专门用于交互的 Action:可以在合并 Store 时定义,或者创建一个新的 “交互 Slice”。
// interactionSlice.js
// 这个 slice 依赖于其他 slice 的 action 存在于最终合并的 store 中
export const createInteractionSlice = (set, get) => ({
  addBearAndFish: () => {
    // 通过 get() 获取其他 slice 的 action 并调用
    // 这是更推荐的方式,因为它不直接依赖 set 的内部实现细节
    get().addBear();
    get().addFish();
    // 或者,如果 action 只是简单的 set,也可以直接 set
    // set(state => ({ bears: state.bears + 1, fishes: state.fishes + 1 }));
  },
});

// useBoundStore.js (合并所有 slices)
import { create } from 'zustand';
import { createBearSlice } from './bearSlice';
import { createFishSlice } from './fishSlice';
import { createInteractionSlice } from './interactionSlice';

export const useBoundStore = create((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
  ...createInteractionSlice(...a), // 合并交互 slice
}));

// 组件中使用
const addBearAndFish = useBoundStore((state) => state.addBearAndFish);
// <button onClick={addBearAndFish}>Add Bear and Fish</button>

使用中间件 (Middleware)

Zustand 支持中间件,允许你在 Store 创建过程中包装 setget 方法,添加额外的功能,如持久化、日志记录、开发者工具集成等。

中间件本质上是一个函数,它接收 Store 的创建函数 ((set, get, api) => ({...})) 作为参数,并返回一个新的创建函数。

示例:使用 persist 中间件进行状态持久化

Zustand 提供了官方的 persist 中间件,可以将 Store 的状态保存到 localStorage (或其他存储)。

首先,安装中间件(如果尚未包含在主 zustand 包中,根据版本可能需要单独安装):

npm install zustand # 通常已包含 middleware

然后,在 create 时应用中间件:

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'; // 引入 persist
import { createBearSlice } from './bearSlice';
import { createFishSlice } from './fishSlice';

export const useBoundStore = create(
  // 1. 调用 persist 中间件
  persist(
    // 2. 将原始的 store 创建逻辑 (包括 slice 合并) 作为参数传入 persist
    (...a) => ({
      ...createBearSlice(...a),
      ...createFishSlice(...a),
    }),
    // 3. 提供 persist 的配置对象
    {
      name: 'bound-store-storage', // localStorage 中的 key 名称
      storage: createJSONStorage(() => localStorage), // 指定存储引擎 (默认 localStorage)
      // partialize: (state) => ({ bears: state.bears }), // 可选:只持久化部分状态
    }
  )
);

现在,bearsfishes 的状态会在每次更新后自动保存到 localStorage,并在页面加载时恢复。

其他常用的中间件包括 devtools (用于集成 Redux DevTools 浏览器扩展) 等。你可以组合使用多个中间件。

处理计算属性 (例如 MobX 迁移)

如果你从 MobX 等具有计算属性(Computed Properties)概念的库迁移过来,可能会想知道如何在 Zustand 中实现类似的功能。计算属性是根据其他状态派生出来的值。

MobX 示例:

// MobX Store
class UserStore {
  mobilePhone = null; // { countryCode: string, caller: string } | null

  @computed
  get mobileDisplay() {
    if (!this.mobilePhone) {
      return '';
    }
    const { countryCode = '0086', caller } = this.mobilePhone;
    return `+${parseInt(countryCode)} ${caller}`;
  }
}

Zustand 实现方案:

方案 1:在组件中使用 Selector 函数计算

这是最常见且推荐的方式。直接在组件的 selector 函数中进行计算。Zustand 的 selector 具有记忆化特性,只有当依赖的状态(如此处的 state.mobilePhone)发生变化时,selector 才会重新计算。

import React from 'react';
import useUserStore from '../../store/zustandUserStore';

const UserProfile = () => {
  // 在 selector 中直接计算 mobileDisplay
  const mobileDisplay = useUserStore(state => {
    if (!state.mobilePhone) {
      return '';
    }
    const { countryCode = '0086', caller } = state.mobilePhone;
    // 注意:parseInt 可能不是必须的,取决于 countryCode 的格式
    return `+${countryCode} ${caller}`;
  });

  return (
    <div>
      <p>Mobile: {mobileDisplay || 'Not provided'}</p>
    </div>
  );
};

方案 2:在 Store 中定义一个普通函数

你也可以在 Store 中定义一个普通函数,该函数使用 get() 来访问当前状态并执行计算。

import { create } from 'zustand';

interface UserState {
  mobilePhone: { countryCode?: string; caller: string } | null;
  getMobileDisplay: () => string;
  // ... 其他状态和操作
}

const useUserStore = create<UserState>((set, get) => ({
  mobilePhone: null,
  // 将计算属性改为一个普通函数,使用 get() 获取最新状态
  getMobileDisplay: () => {
    const state = get(); // 获取当前完整状态
    if (!state.mobilePhone) {
      return '';
    }
    const { countryCode = '0086', caller } = state.mobilePhone;
    return `+${countryCode} ${caller}`;
  },
  // ... 其他属性和方法
}));

// --- 在组件中使用 ---
const UserProfile = () => {
  // 获取计算函数
  const getMobileDisplay = useUserStore((state) => state.getMobileDisplay);
  // 调用函数获取计算结果
  const mobileDisplay = getMobileDisplay();

  // 或者,如果希望在状态变化时自动更新,仍然需要 selector
  // const mobileDisplay = useUserStore(state => state.getMobileDisplay()); // 这样每次渲染都会调用

  // 更好的方式是结合 selector,仅在 mobilePhone 变化时重新获取并调用
  const mobileDisplayResult = useUserStore(state => {
      // 依赖 mobilePhone
      if (!state.mobilePhone) return '';
      // 调用 store 内的函数
      return state.getMobileDisplay();
  }, shallow); // 如果 getMobileDisplay 函数本身是稳定的,可以不用 shallow

  return (
    <div>
      <p>Mobile: {mobileDisplayResult || 'Not provided'}</p>
    </div>
  );
};

对比:

  • 方案 1 (Selector) 更符合 Zustand 的理念,利用了 selector 的自动重计算和优化机制,通常是首选。
  • 方案 2 (Store 函数) 将计算逻辑保留在 Store 内部,但需要在组件中正确调用(通常还是结合 selector 来触发更新)。当计算逻辑非常复杂或需要在 Store 内部多处复用时可以考虑。

这篇文章涵盖了 Zustand 的核心概念和常见用法,从基础的 Store 创建、组件交互,到性能优化(Selectors, Shallow Comparison)、异步处理、代码组织(Slices)、功能扩展(Middleware)以及特定场景(计算属性)的解决方案。希望这能帮助你更好地理解和运用 Zustand 进行状态管理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2323224.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PGP实现简单加密教程

模拟情景&#xff1a; 假设001和002两位同学的电脑上都安装了PGP&#xff0c;现在两人需要进行加密通讯。 一、创建密钥 1.新建密钥&#xff0c;输入名称和邮箱&#xff0c;输入8位口令&#xff0c;根据指示完成。 2.将其添加到主密钥&#xff0c;鼠标右击出现选项。 这里出…

7.8 窗体间传递数据

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的 当项目中有多个窗体时&#xff08;在本节中为两个窗体&#xff1a;Form1和Form2&#xff09;&#xff0c;窗体间传递数据有以下几种方…

【redis】集群 数据分片算法:哈希求余、一致性哈希、哈希槽分区算法

文章目录 什么是集群数据分片算法哈希求余分片搬运 一致性哈希扩容 哈希槽分区算法扩容相关问题 什么是集群 广义的集群&#xff0c;只要你是多个机器&#xff0c;构成了分布式系统&#xff0c;都可以称为是一个“集群” 前面的“主从结构”和“哨兵模式”可以称为是“广义的…

基于Springboot的网上订餐系统 【源码】+【PPT】+【开题报告】+【论文】

网上订餐系统是一个基于Java语言和Spring Boot框架开发的Web应用&#xff0c;旨在为用户和管理员提供一个便捷的订餐平台。该系统通过简化餐饮订购和管理流程&#xff0c;为用户提供快速、高效的在线订餐体验&#xff0c;同时也为管理员提供完善的后台管理功能&#xff0c;帮助…

【redis】集群 如何搭建集群详解

文章目录 集群搭建1. 创建目录和配置2. 编写 docker-compose.yml完整配置文件 3. 启动容器4. 构建集群超时 集群搭建 基于 docker 在我们云服务器上搭建出一个 redis 集群出来 当前节点&#xff0c;主要是因为我们只有一个云服务器&#xff0c;搞分布式系统&#xff0c;就比较…

飞牛NAS本地部署小雅Alist结合内网穿透实现跨地域远程在线访问观影

文章目录 前言1. VMware安装飞牛云&#xff08;fnOS&#xff09;1.1 打开VMware创建虚拟机1.3 初始化系统 2. 飞牛云搭建小雅Alist3. 公网远程访问小雅Alist3.1 安装Cpolar内网穿透3.2 创建远程连接公网地址 4. 固定Alist小雅公网地址 前言 嘿&#xff0c;小伙伴们&#xff0c…

Linux版本控制器Git【Ubuntu系统】

文章目录 **前言**一、版本控制器二、Git 简史三、安装 Git四、 在 Gitee/Github 创建项目五、三板斧1、git add 命令2、git commit 命令3、git push 命令 六、其他1、git pull 命令2、git log 命令3、git reflog 命令4、git stash 命令 七、.ignore 文件1、为什么使用 .gitign…

browser-use 库网页元素点击测试工具

目录 代码代码解释输出结果 代码 import asyncio import jsonfrom browser_use.browser.browser import Browser, BrowserConfig from browser_use.dom.views import DOMBaseNode, DOMElementNode, DOMTextNode from browser_use.utils import time_execution_syncclass Eleme…

解决GitLab无法拉取项目

1、验证 SSH 密钥是否已生成 ls ~/.ssh/ 如果看到类似 id_rsa 和 id_rsa.pub 的文件&#xff0c;则说明已存在 SSH 密钥。 避免麻烦&#xff0c;铲掉重来最方便。 如果没有&#xff0c;请生成新的 SSH 密钥&#xff1a; ssh-keygen -t rsa -b 4096 -C "your_emailexam…

FPGA学习篇——Verilog学习之寄存器的实现

1 寄存器理论 这里在常见的寄存器种加了一个复位信号sys_rst_n。&#xff08;_n后缀表示复位信号低电平有效&#xff0c;无这个后缀的则表示高电平有效&#xff09; 这里规定在时钟的上升沿有效&#xff0c;只有当时钟的上升沿来临时&#xff0c;输出out 才会改变&#xff0c;…

【VUE】ant design vue实现表格table上下拖拽排序

适合版本&#xff1a;ant design vue 1.7.8 实现效果&#xff1a; 代码&#xff1a; <template><div class"table-container"><a-table:columns"columns":dataSource"tableData":rowKey"record > record.id":row…

Vue实现动态数据透视表(交叉表)

需求:需要根据前端选择的横维度、竖维度、值去生成一个动态的表格&#xff0c;然后把交叉的值放入到对应的横维度和竖维度之下&#xff0c;其实就是excel里面的数据透视表功能&#xff0c;查询交叉语句为sql语句。 实现页面&#xff1a; 选择一下横维度、竖维度、值之后点击查…

推荐《人工智能算法》卷1、卷2和卷3 合集3本书(附pdf电子书下载)

今天&#xff0c;咱们就一同深入探讨人工智能算法的卷1、卷2和卷3&#xff0c;看看它们各自蕴含着怎样的奥秘&#xff0c;并且附上各自的pdf电子版免费下载地址。 《人工智能算法&#xff08;卷1&#xff09;&#xff1a;基础算法》 下载地址&#xff1a;https://www.panziye…

元宇宙浪潮下,数字孪生如何“乘风破浪”?

在当今科技飞速发展的时代&#xff0c;元宇宙的概念如同一颗璀璨的新星&#xff0c;吸引了全球的目光。元宇宙被描绘为一个平行于现实世界、又与现实世界相互影响且始终在线的虚拟空间&#xff0c;它整合了多种前沿技术&#xff0c;为人们带来沉浸式的交互体验。而数字孪生&…

数据分析 之 怎么看懂图 一

韦恩图怎么看 ①颜色:不同颜色代表不同的集合 ②)颜色重叠部分:表示相交集合共有的元素 ③颜色不重叠的部分:表示改集合独有的元素 ④数字:表示集合独有或共有的元素数量 ⑤百分比:表示该区域元素数占整体的比例 PCA图怎么看 ① 第一主成分坐标轴及主成分贡献率主成分贡献…

手写数据库MYDB(一):项目启动效果展示和环境配置问题说明

1.项目概况 这个项目实际上就是一个轮子项目&#xff0c;现在我看到的这个市面上面比较火的就是这个首先RPC&#xff0c;好多的机构都在搞这个&#xff0c;还有这个消息队列之类的&#xff0c;但是这个是基于MYSQL的&#xff0c;我们知道这个MYSQL在八股盛宴里面是重点考察对象…

深入理解椭圆曲线密码学(ECC)与区块链加密

椭圆曲线密码学&#xff08;ECC&#xff09;在现代加密技术中扮演着至关重要的角色&#xff0c;广泛应用于区块链、数字货币、数字签名等领域。由于其在提供高安全性和高效率上的优势&#xff0c;椭圆曲线密码学成为了数字加密的核心技术之一。本文将详细介绍椭圆曲线的基本原理…

Intellij IDEA2023 创建java web项目

Intellij IDEA2023 创建java web项目 零基础搭建web项目1、创建java项目2、创建web项目3、创建测试页面4、配置tomcat5、遇到的问题 零基础搭建web项目 小白一枚&#xff0c;零基础学习基于springMVC的web项目开发&#xff0c;记录开发过程以及中间遇到的问题。已经安装了Inte…

Scrapy结合Selenium实现滚动翻页数据采集

引言 在当今的互联网数据采集领域&#xff0c;许多网站采用动态加载技术&#xff08;如AJAX、无限滚动&#xff09;来优化用户体验。传统的基于Requests或Scrapy的爬虫难以直接获取动态渲染的数据&#xff0c;而Selenium可以模拟浏览器行为&#xff0c;实现滚动翻页和动态内容…

sqlmap 源码阅读与流程分析

0x01 前言 还是代码功底太差&#xff0c;所以想尝试阅读 sqlmap 源码一下&#xff0c;并且自己用 golang 重构&#xff0c;到后面会进行 ysoserial 的改写&#xff1b;以及 xray 的重构&#xff0c;当然那个应该会很多参考 cel-go 项目 0x02 环境准备 sqlmap 的项目地址&…