第九章:Zustand:轻量高效的 React 状态管理
第九章:Zustand:轻量高效的 React 状态管理
Prorise第九章:Zustand:轻量高效的 React 状态管理
—— 现代 React 项目的状态管理最佳实践
摘要: 本章是为有经验的 Vue (Pinia) 开发者量身定制的 Zustand 实战指南。我们将跳过繁琐的理论铺垫,直击核心,将 Zustand 定位为 Pinia 在 React 生态中的“战略级对等物”。您将学习如何利用 Zustand 简洁的 API 实现状态管理、性能优化、异步处理,并解决从 Vue 的“可变”心智模型到 React “不可变”模型的平滑过渡。最终,您将掌握一套在企业级应用中组织、测试和扩展 Zustand Store 的现代化工程实践。
在本章中,我们将遵循一条为“转译者”精心设计的路线图,逐步攻克 React 状态管理:
- 首先,我们将从 定位与心智模型 出发,清晰界定 Zustand 的生态位,并建立它与 Pinia 的核心概念映射,彻底打消您“为什么选它”的疑虑。
- 接着,我们将进入 基础实践,以最快的速度创建并使用您的第一个 Store,并集成 TypeScript。
- 然后,我们将立即深入 React 性能的命脉——Selector 与渲染优化,这是从 Vue 自动追踪依赖到 React 手动优化的关键转变。
- 为了解决您最大的心智模型障碍,我们专门开辟一章讲解 不可变性,并引入
immer
作为平滑过渡的终极解决方案。 - 掌握核心后,我们将探索 派生状态 (Getters)、异步流程 和 核心中间件 等高级特性,让您具备应对复杂业务的能力。
- 最后,我们将聚焦于 企业级应用,学习如何通过切片模式(Slice Pattern)组织大型状态,并为其编写健壮的测试。
9.1. 核心定位与概念映射
9.1.1 核心定位:为什么选择 Zustand 而不是 Redux?
对于一位经验丰富的开发者而言,技术选型的核心在于理解其设计哲学与权衡。在 React 状态管理的“丛林”中,Redux 曾是事实上的标准,但它的时代背景和设计也带来了相应的“历史包袱”。
根据官方文档的描述,Zustand 是一个 小型、快速且可扩展的极简状态管理解决方案。它的核心优势在于提供了一个基于 Hooks 的舒适 API,并且 既没有繁重的模板代码,也没有强制性的设计范式。
这与 Redux 形成了鲜明对比。为了更直观地理解这一点,让我们模拟一段您在进行技术选型时可能会有的内心对话。
我看很多 React 的招聘要求和老项目里都提到了 Redux。为什么我们的新项目要选择 Zustand 这个看起来更“小众”的库?它足够稳定和强大吗?
问得好。这正是关键所在。Redux 的核心是 Flux 架构,要求严格的单向数据流,这在大型、复杂的项目中非常稳健,但也带来了大量的“模板代码”——你需要定义 Actions, Reducers, Dispatchers, 甚至使用 Redux Toolkit (RTK) 来简化这个过程。
确实,我在 Vuex 的早期版本里也体会过类似的繁琐。
而 Zustand 的哲学完全不同。它认为对于大多数现代应用来说,这种严格的束缚是不必要的。它让你像使用一个普通的 JavaScript 对象一样管理状态,但又通过 Hooks 实现了与 React 组件的精确绑定。
听起来更像是 Pinia 的感觉,很直接。
完全正确!更重要的是,Zustand 在底层解决了很多棘手的并发问题,比如官方提到的“僵尸子节点问题”、“React 并发”和“上下文丢失”。它可能是目前唯一一个在这些方面都处理得非常完善的库。所以,它不是“小众”,而是“现代”和“精准”。我们选择它,是因为它在提供强大功能的同时,极大地提升了我们的开发体验。
总结来说,Zustand 与 Redux 在概念上都遵循 不可变状态模型,但在实践上却有天壤之别:
- 依赖与上下文: Redux 要求整个应用被一个
<Provider>
包裹,而 Zustand 无需任何 Provider,真正做到了即用即走。 - 代码简洁度: Zustand 极大地减少了模板代码。一个简单的状态,Redux Toolkit 可能需要一个
createSlice
的配置对象,而 Zustand 只需要一个create
函数。
决策依据: 选择 Zustand,意味着我们选择了与 Redux 相似的稳定内核(不可变状态),但抛弃了其繁重的流程和模板代码,换来了更接近 Pinia 的简洁、直观的开发体验。
9.1.2 心智模型映射:Pinia 与 Zustand 的核心概念对比
对于您这位“决策驱动的转译者”而言,最快的学习方式就是建立新旧知识之间的映射。下面的表格就是您从 Pinia 到 Zustand 的“翻译词典”。
Pinia 概念 | Zustand 对等物 | 核心翻译与注意点 |
---|---|---|
defineStore() | create() | (核心) 两者都是创建 Store 的入口函数。 |
state | state (在 create 的回调函数中) | 两者都是用于定义基础状态数据的对象。 |
actions | actions (在 create 的回调函数中) | 定义修改状态的方法。主要区别是 Zustand 的 action 必须通过 set() 函数来完成状态更新。 |
getters | 无直接对等物 | 这是最大的区别之一。Zustand 推荐在组件内使用 useMemo 或创建可复用的 Selector 函数来处理派生状态。 |
plugins | middleware | 用于增强 Store 功能,如集成 DevTools、数据持久化等,概念高度一致。 |
storeToRefs() | 无直接对等物 | React 生态通过 Selector 机制 (useStore(state => state.someValue) ) 来实现对状态的精确订阅和解构,从而保证渲染性能。 |
9.1.3 现代化工程实践:搭建集成 Tailwind & Antd 的开发环境
在深入 Zustand 的 API 之前,我们必须搭建一个符合 2025 年标准的、具备最佳开发体验的工程环境。本节将指导您从零开始,为一个 Vite + React + TS 项目,正确集成 Tailwind CSS v4 和 Ant Design 5,并配置好路径别名。
第一步:初始化 Vite + React + TS 项目
首先,我们使用 pnpm
创建一个纯净的 Vite 项目。
1 | # 创建一个名为 zustand-practice 的项目 |
第二步:集成 Tailwind CSS v4
我们将采用最新的 CSS-First 配置方式来集成 Tailwind。
安装核心依赖
在项目根目录,安装 Tailwind CSS v4 和官方 Vite 插件。
1
pnpm add -D tailwindcss@next @tailwindcss/vite
配置 Vite 插件
编辑
vite.config.ts
文件,引入并使用@tailwindcss/vite
插件。文件路径:
vite.config.ts
1
2
3
4
5
6
7
8
9
10import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite' // 1. 导入插件
export default defineConfig({
plugins: [
react(),
tailwindcss(), // 2. 将插件添加到 plugins 数组
],
})在主 CSS 文件中引入 Tailwind
清空
src/index.css
的所有内容,然后只添加以下一行。文件路径:
src/index.css
1
@import "tailwindcss";
核心变化: 这一行
@import "tailwindcss";
是 v4 Rust 引擎的唯一入口,它会智能处理base
,components
,utilities
层的注入,无需任何额外配置。
第三步:配置 Vite 路径别名 (@/
)
为了提升代码可维护性,我们配置 @
别名指向 src
目录。
修改 Vite 配置
再次编辑
vite.config.ts
,增加resolve.alias
配置。文件路径:
vite.config.ts
(修改后)1
2
3
4
5
6
7
8
9
10
11
12
13
14import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path' // 1. 引入 Node.js 的 'path' 模块
export default defineConfig({
plugins: [react(), tailwindcss()],
// 2. 新增 resolve 配置
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})同步 TypeScript 配置
为确保 VSCode 和 TypeScript 编译器能识别此别名,需同步修改
tsconfig.json
。文件路径:
tsconfig.app.json
(复制粘贴)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* Path alias configuration */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}关键一步: 修改
tsconfig.app.json
后,您需要重启 TypeScript 服务。在 VSCode 中,按下Ctrl+Shift+P
(或Cmd+Shift+P
),然后选择TypeScript: Restart TS Server
。
第四步:正确集成 Ant Design
我们将采用 Ant Design 官方推荐的、基于 <App>
组件的现代化集成方案。
安装 Ant Design
1
pnpm add antd
配置顶层
App
组件这是最关键的一步。我们将使用 antd 的
<App>
组件包裹整个应用,它能同时解决 静态方法上下文消费 和 全局样式重置 两大问题,无需手动引入任何 CSS 文件。文件路径:
src/main.tsx
(修改)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import React from 'react'
import ReactDOM from 'react-dom/client'
import RootApp from './App.tsx'
import './index.css'
import { ConfigProvider, App as AntdApp } from 'antd'
import zhCN from 'antd/locale/zh_CN';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ConfigProvider locale={zhCN}>
{/* 用 antd 的 <App> 组件包裹整个应用。
这是官方推荐的终极方案,用于解决 message, notification, Modal 等静态方法
无法消费 ConfigProvider 上下文的问题。
*/}
<AntdApp>
<RootApp />
</AntdApp>
</ConfigProvider>
</React.StrictMode>,
)
第五步:安装 Zustand 并创建第一个 Store
现在,环境就绪,我们引入 Zustand。
安装 Zustand
1
pnpm install zustand
创建 Store 文件
文件路径:
src/stores/bearStore.ts
1
2
3
4
5
6
7
8
9
10
11import { create } from "zustand";
interface BearState {
bears: number;
increase: () => void;
}
export const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}));
第六步:验证所有集成
最后,我们修改 App.tsx
,创建一个简洁的组件来同时使用 Zustand、Ant Design 和 Tailwind CSS,以验证所有配置都已生效。
文件路径: src/App.tsx
(修改)
1 | import { useBearStore } from '@/stores/bearStore' // 1. 导入 Zustand store (使用路径别名) |
现在,运行 pnpm run dev
。如果您的浏览器中显示了一个 Ant Design 卡片,卡片中的数字可以通过点击按钮增加,并且每次点击都会在页面顶部弹出一个 Ant Design 的成功提示,同时整个页面具有 Tailwind CSS 设置的灰色背景和居中布局,那么恭喜您!我们已经成功搭建了一个纯净、高效的现代化 React 开发环境。
9.2. 实战演练:构建结构化、可维护的 Store
我们将在 9.1
搭建好的项目中,通过一系列的功能迭代,将理论知识应用到实践中。我们将 一边讲解核心 API,一边创建独立的、遵循最佳规范的 React 组件 来消费这些 API,确保您能看到每个知识点带来的实际效果。
9.2.1 深入 set
:掌握状态更新的艺术
set
函数是您与 Zustand Store 交互的 唯一写操作入口。理解它的行为模式,是精通 Zustand 的基石。
核心心智模型:不可变性
首先,我们必须再次强调:Zustand 和 React 一样,遵循 不可变 的状态更新模式。
在 Pinia 中,我可以很直观地 store.count++
或者 store.user.name = 'new name'
来修改状态。这种“可变”操作非常符合直觉。
这是 Vue 响应式系统(基于 Proxy)和 React 状态系统(基于 Immutability)的根本区别。在 React 的世界里,我们从不“修改”原始状态,而是用一个“全新的”状态对象来替换它。
听起来效率很低,每次都要创建新对象?
恰恰相反,这正是 React 高效更新机制(Virtual DOM diffing)的基础。通过比较新旧两个状态对象的引用(地址),React 可以瞬间知道状态是否变更,从而决定是否需要更新 UI。Zustand 的 set
函数正是为这种模式而生,并且它还提供了一些便利的特性。
特性一:自动的浅层合并
为了简化操作,Zustand 的 set
函数默认会执行 浅层合并。这意味着您只需要提供要更新的字段,Zustand 会自动将它与现有状态合并,无需手动扩展 (...
) 其他属性。
实战应用:创建熊计数器组件
更新 Store (
bearStore.ts
)我们为 Store 添加
lastUpdated
状态,用于演示浅层合并。文件路径:
src/stores/bearStore.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { create } from "zustand";
interface BearState {
bears: number;
lastUpdated: number | null;
increase: () => void;
}
export const useBearStore = create<BearState>((set) => ({
bears: 0,
lastUpdated: null,
// 一般来说,要覆盖一个不可变对象需要使用如下的语法:
// set((state) => ({ ...state, bears: state.bears + 1, lastUpdated: Date.now() }))
// 我们永远都需要将 ...state 作为返回对象的第一项解构,而 zustand 帮我们优化了这一点
increase: () =>
set((state) => ({ bears: state.bears + 1, lastUpdated: Date.now() })),
}));创建消费组件 (
BearCounter.tsx
)文件路径:
src/components/BearCounter.tsx
(新建文件)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import { useBearStore } from "@/stores/bearStore";
import { Button, Card, Descriptions, Statistic } from "antd";
import React from "react";
export const BearCounter: React.FC = () => {
// 最佳实践:只订阅本组件需要的最小状态集,避免不必要的重渲染
const bears = useBearStore((state) => state.bears);
// const lastUpdated = useBearStore((state) => state.lastUpdated);
const increase = useBearStore((state) => state.increase);
return (
<Card title="熊的数量控制器 (浅层合并)">
<Descriptions bordered column={1}>
<Descriptions.Item label="当前数量">
<Statistic value={bears} />
</Descriptions.Item>
{/* <Descriptions.Item label="最后更新">{lastUpdated ? new Date(lastUpdated).toLocaleString() : "N/A"}</Descriptions.Item> */}
</Descriptions>
<Button type="primary" onClick={increase} className="mt-4">
增加一只熊
</Button>
</Card>
);
};
特性二:处理深度嵌套对象
set
函数的自动合并 只在一层深度有效。当您需要更新嵌套对象时,必须手动处理深层对象的不可变更新。
更新 Store (
bearStore.ts
)文件路径:
src/stores/bearStore.ts
(修改)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import { create } from "zustand";
interface BearState {
bears: number;
increase: () => void;
status: {
// 新增嵌套对象
hungry: number;
};
feed: (amount: number) => void;
}
export const useBearStore = create<BearState>((set) => ({
bears: 0,
status: { hungry: 100 },
increase: () =>
set((state) => ({ bears: state.bears + 1})),
feed: (amount: number) =>
set((state) => ({
status: {
...state.status, // 👈 必须手动展开旧的 stats 对象
hungry: state.status.hungry - amount,
},
})),
}));感受痛点: 正如您所见,手动展开 (
...state.stats
) 非常繁琐。在后续讲解 中间件 的章节,我们将引入 Immer 作为解决此问题的终极方案。但现在,请务必理解这个手动过程。创建消费组件 (
BearFeeder.tsx
)文件路径:
src/components/BearFeeder.tsx
(新建文件)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import { useBearStore } from '@/stores/bearStore'
import { Button, Card, Statistic, message } from 'antd'
export const BearFeeder = () => {
const hungry = useBearStore((state) => state.stats.hungry)
const feed = useBearStore((state) => state.feed)
const handleFeed = () => {
if (hungry > 0) feed(10)
else message.warning('熊已经吃饱了!')
}
return (
<Card title="喂食器 (嵌套更新)">
<Statistic title="饥饿度" value={hungry} suffix="/ 100" />
<Button onClick={handleFeed} className="mt-4">喂食 (-10)</Button>
</Card>
)
}
9.2.2 使用 get
函数:在 Actions 内部读取最新状态
create
回调函数提供了第二个参数 get
,允许 Action 在执行时,安全地 读取 到 Store 的最新状态,无需订阅。这对于实现 Action 之间的联动至关重要。
实战应用:实现一个更智能的 increase
Action
我们重构 increase
Action,让它在增加熊的数量后,还能更新时间戳。
文件路径: src/stores/bearStore.ts
(修改)
1 | // ... interface BearState { ... } |
效果验证: 您无需修改 BearCounter.tsx
组件。现在当您点击“增加一只熊”按钮时(按钮文字可改回),会发现数量和时间戳同时被更新了。
9.2.3 组件外部的 API:getState
与 setState
useBearStore
不仅是一个 Hook,也是一个 Store 实例,允许我们在 React 组件外部与之交互,这在调试或与非 React 库集成时非常有用。
实战应用:创建外部调试与重置工具
更新 Store (
bearStore.ts
)我们添加
reset
功能,并使用set
的replace
标志来完全替换状态。文件路径:
src/stores/bearStore.ts
(修改)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45import { create } from "zustand";
const initialState = {
// 定义初始状态, 便于重置
bears: 0,
lastUpdated: null,
status: { hungry: 100 },
};
interface BearState {
bears: number;
lastUpdated: number | null;
increase: () => void;
status: {
// 新增嵌套对象
hungry: number;
};
feed: (amount: number) => void;
reset: () => void;
}
export const useBearStore = create<BearState>((set, get) => ({
...initialState,
increase: () => {
const newBears = get().bears + 1;
set({
bears: newBears,
lastUpdated: Date.now(),
});
},
feed: (amount: number) =>
set((state) => ({
status: {
...state.status, // 👈 必须手动展开旧的 stats 对象
hungry: state.status.hungry - amount,
},
})),
// 在复杂的情况下,我们需要将所有的状态作为一个对象,解构初始化对象并传入我们的函数,类似语法相当于是这样的:
// reset: () =>
// set({ ...initialState, increase: get().increase, feed: get().feed }, true),
// 这里的 True 标志代表 replace,替换整个对象,我们需要把整个对象进行解构并合并
// 但是 zustand 帮我们优化了这一点,我们只需要传入整个 initialState 即可
reset: () => set(initialState),
}));
为什么它能工作? 因为它巧妙地利用了我们前面提到的 浅层合并 特性。当您调用 set(initialState)
时,Zustand 在内部执行的逻辑是这样的:
- 获取当前 Store 的完整状态,它看起来像:
{ bears, lastUpdated, status, increase, feed, reset }
。 - 获取您传入的
initialState
对象:{ bears: 0, lastUpdated: null, status: { hungry: 100 } }
。 - 执行一次
Object.assign({}, currentState, initialState)
操作。 initialState
里的bears
,lastUpdated
,status
字段会覆盖掉currentState
中对应的旧值。- 而
currentState
中有、但initialState
中没有的字段(也就是increase
,feed
,reset
这些 Action)则 完全不受影响,被保留了下来。
9.3. 性能之钥:Selector 的工作原理与优化
到目前为止,我们已经掌握了如何读写 Store 的状态。但要真正精通 Zustand,我们必须理解它与 React 渲染周期之间的关系,这其中的关键就是 Selector (选择器)。可以说,是否能正确使用 Selector,是区分 Zustand 新手和专家的分水岭。
9.3.1 Selector 的核心机制:===
严格引用比较
我们回顾一下在组件中读取状态的代码:
1 | const bears = useBearStore((state) => state.bears) |
这里的 (state) => state.bears
就是一个 Selector。Zustand 的工作机制非常纯粹:
它会在每次 Store 发生变化时,重新执行这个 Selector 函数,然后将上一次的返回结果 prevResult
与这一次的返回结果 newResult
进行一次严格的比较,在 JavaScript 中,这等价于 Object.is(prevResult, newResult)
,对于大多数情况,您可以简单理解为 prevResult === newResult
。
- 如果比较结果为
true
(结果未变),组件不会重新渲染。 - 如果比较结果为
false
(结果已变),组件会重新渲染。
这就是为什么我们之前的 BearCounter
组件是性能良好的:当 bears
(一个数字) 的值没有变化时,prevResult === newResult
始终为 true
,组件就不会重渲染。
9.3.2 性能陷阱:订阅非原始值导致的多余渲染
理解了上述核心机制后,一个最常见的性能陷阱就浮出水面了:当 Selector 的返回值是一个每次都新创建的对象或数组时,会发生什么?
实战场景:创建一个只显示所有熊名字的组件
更新 Store (
bearStore.ts
)让我们为 Store 增加一个更复杂的状态,用来存储每只熊的名字和它的食物偏好。
文件路径:
src/stores/bearStore.ts
(修改)1
2
3
4
5
6
7
8
9
10// ... imports
interface BearState {
// ... a bunch of other state
bearHabits: { // 新增一个对象
'Smokey': { favoriteFood: 'Honey' },
'Paddington': { favoriteFood: 'Marmalade' },
}
updateTimestamp: () => void // 复用之前的 action 用于触发不相关的更新
}创建有性能问题的组件 (
BearNames.tsx
)这个组件的职责很简单:只显示所有熊的名字。
文件路径:
src/components/BearNames.tsx
(新建文件)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { useBearStore } from '@/stores/bearStore'
import { Card, Tag } from 'antd'
import React from 'react'
export const BearNames = () => {
console.log('BearNames component is rendering...')
// 这个 Selector 每次执行都会返回一个新的数组
const names = useBearStore((state) => Object.keys(state.bearHabits))
return (
<Card title="所有熊的名字 (有性能问题)">
<div className="flex gap-2">
{names.map((name) => (
<Tag key={name} color="blue">{name}</Tag>
))}
</div>
</Card>
)
}在
App.tsx
中验证问题我们将这个新组件加入到
App.tsx
中,并复用BearCounter
组件里的“更新时间戳”按钮来触发一次与bearHabits
无关的 Store 更新。文件路径:
src/App.tsx
(修改)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// ... imports
import { BearNames } from './components/BearNames' // 导入新组件
function App() {
return (
<div className="flex justify-center items-center h-screen bg-slate-100">
<Space align="start" size="large">
<BearCounter />
<BearFeeder />
<BearNames /> {/* 添加新组件 */}
<DebugTools />
</Space>
</div>
)
}现在,打开浏览器的控制台,然后点击
BearCounter
组件中的“更新时间戳”按钮。您会发现,即使熊的名字列表names
的内容完全没有变化,控制台依然打印出了BearNames component is rendering...
。原因:
Object.keys()
函数在每次被调用时,都会返回一个全新的数组实例。因此,prevResult === newResult
的比较结果永远是false
,导致了组件在任何 Store 状态变更时都会不必要地重渲染。
9.3.3 解决方案:使用 useShallow
Hook 优化订阅
为了解决这个陷阱,Zustand 提供了一个专门的 Hook:useShallow
。
useShallow
的作用是将默认的严格引用比较 (===
),替换为对数组或对象内部元素的“浅层比较”。它会逐一比较新旧两个数组(或对象)的成员,只有当成员发生变化时,才认为结果已改变。
实战应用:修复 BearNames
组件
修复过程极其简单,只需两步:
文件路径: src/components/BearNames.tsx
(修改)
1 | import { useBearStore } from '@/stores/bearStore' |
现在,再次点击“更新时间戳”按钮,您会发现控制台不再打印 BearNames component is rendering...
。不必要的重渲染被成功阻止了!
最佳实践: 当您的 Selector 需要返回一个非原始值(数组或对象)时,请始终使用 useShallow
来包裹它,这是 Zustand 性能优化的第一道防线。
9.3.4 (可选) 便捷技巧:自动生成 Selectors
虽然 (state) => state.bears
写起来不复杂,但在大型项目中,重复编写这些简单的选择器也有些繁琐。官方文档提供了一个便捷的辅助函数,可以为 Store 的每个顶级属性自动生成一个专用的 Selector Hook。
创建辅助函数 (
createSelectors.ts
)文件路径:
src/stores/createSelectors.ts
(新建文件)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import { StoreApi, UseBoundStore } from 'zustand'
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never
export const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
_store: S,
) => {
const store = _store as WithSelectors<typeof _store>
store.use = {}
for (const k of Object.keys(store.getState())) {
;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
}
return store
}应用到我们的 Store (
bearStore.ts
)文件路径:
src/stores/bearStore.ts
(修改)1
2
3
4
5
6
7
8
9
10
11
12// ... imports
import { createSelectors } from './createSelectors' // 导入辅助函数
// ... BearState 接口和 initialState
// 1. 先创建一个基础的 store
const useBearStoreBase = create<BearState>((set, get) => ({
// ... store 实现
}))
// 2. 使用辅助函数包裹基础 store,导出最终版本
export const useBearStore = createSelectors(useBearStoreBase)在组件中使用
现在,我们可以用更简洁的语法来订阅状态。
文件路径:
src/components/BearCounter.tsx
(修改)1
2
3
4
5
6
7
8
9
10
11
12
13
14// ... imports
export const BearCounter = () => {
// Before: const bears = useBearStore((state) => state.bears)
const bears = useBearStore.use.bears() // ✨ After: 语法更简洁
// Before: const lastUpdated = useBearStore((state) => state.lastUpdated)
const lastUpdated = useBearStore.use.lastUpdated() // ✨ After
// Before: const increase = useBearStore((state) => state.increase)
const increase = useBearStore.use.increase() // ✨ After
// ... 组件 UI 不变
}
权衡: 自动生成 Selectors 是一种提升开发体验的“语法糖”。它非常适合订阅顶层的、简单的状态。但对于需要计算、组合或订阅深层嵌套属性的复杂场景,手写 Selector 依然是更灵活、更强大的选择。请务必先掌握手写 Selector 的核心原理。