Refine 生态系列:Zustand 状态管理实战指南——用 Hooks 驱动轻量级全局状态
2026/9/10 9:52:06 网站建设 项目流程

Refine 生态系列:Zustand 状态管理实战指南——用 Hooks 驱动轻量级全局状态

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

本篇技术指南以 Refine 文档站点中的 Ecosystem / Integrations 博客文章 为骨架,系统讲解 Zustand 这一基于 Hooks 的轻量级 React 全局状态管理库:从核心设计理念、安装配置、create/set等基础 API,到防重复渲染的 memoization 技巧、异步 Action、嵌套状态结构、中间件与自定义 Hooks 等进阶用法,并辅以一个完整可运行的 To-do 应用实战。读完本文,你将掌握在不引入 Provider、零样板代码的前提下,用 Zustand 为 React(含 Refine 构建的内部工具、Admin 面板)应用组织可预测、可维护的全局状态。

为什么关注 Zustand:从 Redux 说起

在全局状态管理领域,Redux 曾长期占据主导地位:它强大的可预测性、便捷的调试体验以及广泛应用的最佳实践,使其成为几乎所有开发者接触全局状态的第一站。与此同时,Redux 的上手成本与样板代码也一直为人诟病——创建 store、包裹 Context Provider、在组件中引入useSelector/useDispatch等步骤缺一不可。

Zustand 正是在这一背景下出现的"新面孔"。它被定位为一个小型、快速、可扩展的极简状态管理方案,基于简化的 Flux 原则实现,提供基于 Hooks 的舒适 API,且不强制约定、不裹挟样板代码。它由 Jotai 和 React-spring 背后的团队构建,开源且拥有活跃的社区维护。

与本仓库的生态系列文章(React 生态系统 2024)中的描述一致:Zustand 是一款非常轻量的 React 状态管理库,遵循 Flux 原则,打包体积小,内置 TypeScript 支持,可与 Immer.js 等第三方库配合创建不可变状态。需要注意的是,当前 refine 仓库根目录的 package.json 与 pnpm-lock.yaml 中并未把 zustand 列为直接依赖,它属于文档生态中介绍的第三方集成方案,可自由选装。

Zustand 的核心设计理念

要真正用好 Zustand,需要先理解它与 Redux 的几个关键差异:

  • 无需 Provider 包裹:Zustand 不像 React-Redux 那样要求你用 Content Provider 包裹整个应用,而是主要通过 Hooks 与状态通信,结构上更加扁平。
  • 单一数据源与状态切片(State Slices):Zustand 拥抱"单一数据源(single source of truth)"理念,整个应用状态集中存储在一个 store 中。store 由多个状态切片构成——每个切片代表应用的不同部分,可拆分为独立的 store 定义,从而实现模块化与相关状态/动作的封装。
  • 默认不可变(Immutability by Default):更新状态时创建新的状态对象而非直接修改旧对象,这简化了状态管理、避免了常见的变更类 bug,也让 React 的变更检测与重渲染更加高效。
  • 订阅与选择性响应(Subscriptions & Selective Reactivity):组件可以订阅特定的状态切片,仅在切片变化时自动重渲染。Zustand 基于代理(proxy)实现了细粒度的依赖追踪,从而高效更新、最小化不必要的重渲染。
  • 更少样板代码:同样的计数器状态,用 Redux 需要创建 store、包裹 Provider、引入useSelector/useDispatch等一整套流程;而 Zustand 只需一个create调用。

环境准备与安装

Zustand 是运行在 Node.js 环境下的 JavaScript 库,使用前需确保本机具备以下基础工具:

  • Node.js:提供运行时与npm(npm 随 Node.js 二进制包一起安装,无需单独安装)。
  • npm 或 yarn:包管理器,用于维护和管理项目依赖。

创建一个新的 Node.js 项目并安装 Zustand:

mkdir zustand-prj cd zustand-prj npm init --y
  • mkdir zustand-prj:在当前目录创建名为zustand-prj的新目录。
  • cd zustand-prj:将当前工作目录切换到zustand-prj
  • npm init --y:初始化新的 Node.js 项目并生成package.json清单文件,--y表示自动接受全部默认选项。

安装 Zustand 库:

npm install zustand # 或使用 yarn: yarn add zustand

创建第一个 Store:认识 create 与 set

使用 Zustand 的第一步是从主入口导入create函数:

import { create } from "zustand";

create接收一个回调函数并返回一个自定义 Hook。在这个回调函数里,我们定义状态以及操作状态的方法——状态与方法都以对象形式从回调中返回。create会把一个set函数传给回调,set正是用来修改 store 中状态的核心工具:

const useCounter = create((set) => { return { counter: 0, incrCounter: () => set((state) => ({ counter: state.counter + 1 })), }; });

在上述例子中,store 里有两个成员:状态counter和动作incrCounteruseCounter是一个自定义 Hook,可在任意组件中使用并获取最新状态。假如组件 A、B、C 都使用了该 Hook,B 中对状态所做的任何修改都会反映到 A 和 C 中,它们会重新渲染以呈现最新变化。

Zustand 中的状态可以是原始值(primitive)、对象或函数。这一点从源码结构的组织方式也能印证:状态与动作被统一放进 store 对象中,动作本质上就是"状态的一部分"。

在组件中使用 Store:选择切片与读取全量状态

create返回的自定义 Hook 用法与 React-Redux 中的useAppSelector类似——你调用 Hook 并传入一个回调函数,Hook 内部会调用该回调并把当前状态传给它,你在回调中返回想要的那部分状态(即"切片"):

const counter = useCounter((state) => state.counter);

将切片选择与展示组件结合:

const DisplayCounter = () => { const counter = useCounter((state) => state.counter); return <div>Counter: {counter}</div>; };

再创建一个负责递增状态的组件——注意它与展示组件彼此独立,仅仅通过 store 共享状态:

const CounterControl = () => { const incrCounter = useCounter((state) => state.incrCounter); return ( <div> <button onClick={incrCounter}>Incr. Counter</button> </div> ); };

把两个组件组装进App

const App = () => { return ( <> <DisplayCounter /> <CounterControl /> </> ); };

每当点击CounterControl中的按钮,DisplayCounter就会重新渲染出最新的counter值——两个组件零耦合,却由 Zustand 神奇地连接在一起。这正是 Zustand 相对 React-Redux 的优势所在:同样的状态逻辑,Redux 需要创建 store、包裹 Provider、逐一引入useSelector/useDispatch,而 Zustand 将其大大简化。

返回整个状态

当调用create返回的 Hook 时不传任何回调,Hook 将返回 store 的整个状态:

const state = useCounter();

此时可以像访问普通对象一样读取与调用:

state.counter; // 0 state.incrCounter(); // 1

防止不必要的重渲染:memoization、shallow 与自定义比较器

memoization 是一种优化技术:通过缓存耗时函数调用的结果,当相同参数再次调用时直接返回缓存值,从而避免重复计算、提升性能。Zustand 允许我们为自定义 Hook 添加 memoization——它导出了一个shallow函数,用于对状态切片做浅比较:

import { shallow } from "zustand/shallow";

仍以useCounter为例:假设counter初始值为 0,当通过incrCounter更新后DisplayCounter会重渲染;但如果更新后的值仍为 0(例如并发场景下值未实际变化),这次重渲染就是不必要的

如何阻止这种"前后值相等却仍然重渲染"的情况?Zustand 的做法是给自定义 Hook 传入第二个参数——一个比较器函数(comparator)。它比较上一次的状态切片与下一次的状态切片:相同则不重渲染,不同则重渲染。这与useEffectuseMemouseCallback等 React Hooks 的依赖比较思想一脉相承。

shallow就是 Zustand 内置的比较器,它使用==(浅相等)比较两个状态切片:

const counter = useCounter((state) => state.counter, shallow);

如果你不信任shallow的判定,也可以编写自定义比较器。比较器接收两个参数——上一次的切片值、下一次的切片值:

(previousState, nextStateSlice) =>

在函数体内完成比较并返回结果:返回true让 Hook 跳过重渲染,返回false则触发重渲染。为counter切片编写一个基于===的比较器:

(previousCounter, nextCounter) => previousCounter === nextCounter;

把它接入useCounter

const counter = useCounter( (state) => state.counter, (previousCounter, nextCounter) => previousCounter === nextCounter, );

至此useCounter已具备 memoization 能力:不再有无谓的重渲染,应用运行更高效。补充一点版本提示:较新版本(v5+)也支持直接从主入口import { create, shallow } from "zustand"引入shallow,两种写法按你所用的版本选择即可。

更新状态:set 的两种形态与清空状态

此前只讨论了从 store 读取状态,这一节聚焦如何更新状态。set函数支持两种调用形态。

形态一:传入回调函数(推荐用于依赖当前值)

const useCounter = create((set) => { return { counter: 0, incrCounter: () => set((state) => ({ counter: state.counter + 1 })), }; });

set会调用这个回调并把当前状态作为参数传入,然后用回调的返回值更新状态。注意回调返回的对象中携带counter属性,set据此知道要更新 state 中的哪些属性——只更新返回对象中出现的属性,其余属性保持不变

形态二:直接传入对象

set({ counter: 9, });

这会将counter状态直接更新为 9。

清空整个状态:向set传入空对象并带上第二个参数true

set({}, true);

这会清除 store 中的状态与动作。

认识 Action:同步与异步

Action 是什么?Action 是 store 状态中那些用于改变 store 的函数,类似于 React-Redux 中的 dispatch action。例如上面的incrCounter就是一个 Action——它在内部调用set来更新counter状态。

异步 Action:Zustand 并不关心你的 Action 是同步还是异步——异步函数天然受支持。例如可以在 Action 中发起 HTTP 请求,并用返回结果更新状态:

const useCounter = create((set) => { return { counter: 0, incrCounter: async () => { const { data } = await axios.get("/counter"); set({ counter: data.counter, }); }, }; });

这里用async关键字把incrCounter声明为异步函数,内部请求/counter接口后通过set更新counter。需要说明的是:示例中的axios并非 Zustand 的一部分,只是演示"异步取数 + 更新状态"的常见组合,实际项目中可替换为你偏好的请求库。

实战:用 Zustand 构建 To-do 应用

掌握了基础 API 之后,我们动手构建一个由 Zustand 驱动状态管理的 To-do 应用。首先用create-react-app脚手架创建 React 项目并安装 Zustand:

create-react-app todo-app cd todo-app npm install zustand

1. 创建 Store

import { create } from "zustand"; const useStore = create((set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [ ...state.todos, { id: Date.now(), text, completed: false, }, ], })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo, ), })), deleteTodo: (id) => set((state) => ({ todos: state.todos.filter((todo) => todo.id !== id), })), })); export default useStore;

Store 中有一个todos数组状态,以及三个 Action:addTodo追加新待办(用Date.now()生成 id)、toggleTodo切换某条待办的completed标记、deleteTodo从数组中移除指定 id 的待办。注意这里全部通过"展开旧数组/旧对象生成新引用"的方式更新,保持了 Zustand 的不可变更新约定。

2. 列表展示组件 DisplayTodos

const DisplayTodos = () => { const { todos, toggleTodo, deleteTodo } = useStore((state) => { return { todos: state.todos, toggleTodo: state.toggleTodo, deleteTodo: state.deleteTodo }; }); return ( <ul> {todos.map((todo) => ( <li key={todo.id} style={{ textDecoration: todo.completed ? "line-through" : "none", }} onClick={() => toggleTodo(todo.id)} > {todo.text} <button onClick={() => deleteTodo(todo.id)}>Delete</button> </li> ))} </ul> ); }; export default DisplayTodos;

这里从状态中切出todos数组,用Array#map渲染列表;同时切出toggleTodo(点击待办切换完成态,已完成项显示删除线)与deleteTodo(点击 Delete 按钮按 id 移除该项)。实用提示:原文档示例中 onClick 使用了未从 store 切出的toggleTodo,这里已补充切出,使代码可直接运行。

3. 新增待办组件 TodosControl

const TodosControl = () => { const addTodo = useStore((state) => state.addTodo); const [text, setText] = useState(""); function handleSubmit(e) { e.preventDefault(); addTodo(text); setText(""); } return ( <form onSubmit={handleSubmit}> <input type="text" value={text} onChange={(e) => setText(e.target.value)} /> <button type="submit">Add</button> </form> ); }; export default TodosControl;

组件通过useState维护输入框文本text;表单提交时触发handleSubmit,调用addTodo(text)创建新待办并清空输入框。

4. 组装应用

const App = () => { return ( <> <DisplayTodos /> <TodosControl /> </> ); }; export default App;

至此,一个增、删、勾选完整的 To-do 应用仅靠一个 store 与几个组件即告完成——没有 Provider、没有 reducer、没有 dispatch,这正是 Zustand "极简" 风格的最佳体现。

进阶:管理嵌套状态结构

面对复杂的状态结构,Zustand 支持创建嵌套的状态切片,使状态组织更模块化,更新某一小块状态时不波及其他部分:

const useStore = create((set) => ({ user: { name: "", age: 0, address: { street: "", city: "", }, }, updateUser: (newUser) => set((state) => ({ user: { ...state.user, ...newUser } })), updateAddress: (newAddress) => set((state) => ({ user: { ...state.user, address: { ...state.user.address, ...newAddress }, }, })), }));

user下嵌套了address子结构,因此分别提供updateUserupdateAddress两个方法:前者浅合并用户字段,后者只浅合并地址字段,其余层级原样保留——通过逐层展开(spread)保证不可变更新。

使用中间件:devtools 与更多能力

Zustand 可以通过中间件(middleware)增强功能,例如:记录状态变更日志、在 localStorage 中保存/恢复状态、处理异步动作等。以日志/调试最常用的devtools为例,只需用中间件包裹 store 定义:

import { create } from "zustand"; import { devtools } from "zustand/middleware"; const useStore = create( devtools((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })), );

包裹devtools后,状态变更便可在 Redux DevTools 扩展中观察,这对调试大有裨益。除devtools外,Zustand 还提供persist(持久化到 localStorage 等存储介质)、immer(以可变写法生成不可变更新)等中间件,它们都从zustand/middleware子路径导入。

编写自定义 Hooks 与工具函数

封装状态逻辑为自定义 Hook

自定义 Hook 可以将 Zustand 的状态逻辑封装起来,让复用与复杂状态交互的管理更轻松。以用户认证状态为例:

import { create } from "zustand"; const useAuthStore = create((set) => ({ user: null, login: (userData) => set({ user: userData }), logout: () => set({ user: null }), })); const useAuth = () => { const { user, login, logout } = useAuthStore(); return { user, login, logout }; }; export default useAuth;

之后任何组件只需调用useAuth()即可获得userloginlogout,认证状态逻辑被收敛到一处,代码更整洁、更易复用。

构造工具函数简化深层更新

工具函数可以简化状态更新逻辑、提升代码可读性。下面用"点路径"(dot path)定位并更新深层嵌套状态:

const updateNestedState = (set, keyPath, value) => { set((state) => { const keys = keyPath.split("."); let nestedState = state; keys.slice(0, -1).forEach((key) => { nestedState = nestedState[key]; }); nestedState[keys[keys.length - 1]] = value; return { ...state }; }); }; const useNestedStateStore = create((set) => ({ data: { user: { profile: { name: "", }, }, }, updateProfileName: (name) => updateNestedState(set, "data.user.profile.name", name), }));

通过updateNestedState(set, "data.user.profile.name", name),一行即可更新任意深度的状态属性,该工具函数可在应用各处复用。值得注意的是:这里为了演示"点路径遍历"直接修改了nestedState的叶子属性,最终通过返回{ ...state }生成新引用——在生产代码中,推荐仍以不可变方式(逐层 spread)完成深更新,避免共享引用引发副作用。

与 Refine 项目结合的建议

Refine 作为构建内部工具、Admin 面板与 B2B 应用的数据驱动框架,其自身的 Hooks(如useListuseFormuseShow等)已接管了大部分与数据提供者(data provider)相关的服务端状态;而 Zustand 这类轻量客户端状态库,天然适合管理与服务端无关的 UI 状态——例如主题偏好、侧边栏折叠状态、筛选条件的临时草稿、全局通知配置等。将两者结合时,建议遵循"各司其职"的分工:服务端数据交给 Refine 的 Hooks 与缓存机制,纯客户端瞬时状态交给 Zustand,避免两套方案在职责上重叠。相关的基础背景也可参考仓库中的 React 生态系统 2024 系列文章 中对 Zustand 特性的概括介绍。

总结

回顾本文的探索路径:我们先从 Redux 的痛点切入,认识了 Zustand 的设计理念与差异化优势;随后完成环境准备与安装,掌握了create/set基础 API,学会在组件中选择状态切片、读取全量状态;接着通过shallow与自定义比较器实现了防重复渲染的 memoization,深入理解了set的对象/函数两种更新形态、清空状态、Action 与异步 Action;再以一个完整的 To-do 应用串联全部知识点;最后进阶到嵌套状态结构、devtools中间件、自定义 Hooks 与工具函数。正如原文档所强调的,这些只是 Zustand 能力的冰山一角——它极简、基础、有趣,而更深层的能力(持久化、Immer 集成、暂存状态、跨 store 通信等)依然有待探索。在 Refine 这样的数据驱动框架中,将服务端数据交给 Refine、客户端瞬时状态交给 Zustand,是一条清晰且低成本的集成路径。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询