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 列表展示区。
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
组件读取todos
和filter
状态。- 它根据
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)
返回 true
,Display
组件就不会再进行不必要的重渲染了。
最佳实践: 始终使用 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 函数返回一个包含 todos
和 setFilter
的新对象 { todos: ..., setFilter: ... }
。当 Store 中 任何 状态(比如 filter
)发生变化时,这个 selector 函数会重新执行,即使 state.todos
和 state.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.todos
或 state.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。- 在请求开始时,通过
set
将isLoading
设为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 中的状态,有几种方式:
- 在 Action 中多次调用
set
(不推荐,可能触发多次更新)。 - 在 Action 中一次性
set
多个状态 (如果状态都在一个 Slice 控制)。 - 定义一个专门用于交互的 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 创建过程中包装 set
和 get
方法,添加额外的功能,如持久化、日志记录、开发者工具集成等。
中间件本质上是一个函数,它接收 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 }), // 可选:只持久化部分状态
}
)
);
现在,bears
和 fishes
的状态会在每次更新后自动保存到 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 进行状态管理。