Zustand 快速入门
说明:本笔记源于我学习B站up"一线柏拉图"的视频
www.bilibili.com/video/BV1Tr...前置知识:基本掌握React语法,包括useState,useReducer,useContext,状态提升,组件间共享状态等
Zustand(德语"状态")是一个轻量、快速、无模板的 React 状态管理库。它基于 Hook,API 设计极简。
为什么需要 Zustand?
- React 本身已经提供了状态管理,但随着应用变复杂,内置方案会暴露出各种痛点。
- 多个组件需要共享用户信息时Context和Provider繁琐
- 更新逻辑散落各组件,状态修改到处都有,难以追踪
- 无法在组件外部读取/修改,灵活性差。
1. 安装
bash
npm install zustand
2. 创建存储器 Store
- Store 就是一个自定义 Hook,用
create函数创建,用来存储定义好的状态和更新函数。 - 状态和更新函数都定义在一起,方便维护和测试。
- 在实际项目中往往会有一个专门的文件夹用于存放你定义的所有的Stores。
javascript
import { create } from 'zustand'
//例如定义熊bear
const useBearStore = create((set) => ({
bears: 0,//定义状态,可以定义多个状态
owner: "Yue",
//定义更新函数
increase: () => set((state) => ({ bears: state.bears + 1 })),
decrease: () => set((state) => ({ bears: state.bears - 1 })),
reset: () => set({ bears: 0 }),
}))
set是创建 store 时,create 函数自动注入的第一个参数,专门用来更新状态。你可以把它理解为 Zustand 版的 setState。它接收一个对象或函数。函数可接收当前状态state,用于基于旧值更新。
3. 在组件中使用
直接调用自定义 Hook 即可获取状态和操作方法,无需useContext的Provider包裹。
js
import useBearStore from './store/bearStore'//导入定义好的Store, 导入的useBearStore叫做Store选择器
function BearCounter() {
// 订阅多个字段
const bears = useBearStore((state) => state.bears)
const owner = useBearStore((state) => state.owner)
return <h1>🐻 {bears} 只熊</h1>
}
function Controls() {
// 订阅方法(选择器提取多个值)
const increase = useBearStore((state) => state.increase)
const decrease = useBearStore((state) => state.decrease)
const reset = useBearStore((state) => state.reset)
return (
<div>
<button onClick={increase}>+1</button>
<button onClick={decrease}>-1</button>
<button onClick={reset}>重置</button>
</div>
)
}
// 合并在一个组件中
function App() {
return (
<>
<BearCounter />
<Controls />
</>
)
}
解构赋值(简化)
js
const { bears, owner, increase, decrease, reset } = useBearStore(
{
bears: state.bears,
owner: state.owner,
increase: state.increase,
decrease: state.decrease,
reset: state.reset,
})
4. 更新状态的几种方式
- 直接传对象(合并,非覆盖)
- 传入函数 (接收
state,返回部分状态) - 使用
set的第二个参数 :设为true会替换整个状态(慎用)
javascript
set({ bears: 5 }) // 合并
set((state) => ({ bears: state.bears + 1 })) // 函数更新
set({ bears: 0 }, true) // 替换整个状态(其他字段会丢失)
5. 异步操作
直接在 set 中编写异步逻辑即可,就像普通函数一样。
javascript
//例如发送网络请求
const useFishStore = create((set) => ({
fish: [],
loading: false,
fetchFish: async () => {
set({ loading: true })
const res = await fetch('/api/fish')
const fish = await res.json()
set({ fish, loading: false })
},
}))
6. 选择器与性能优化
通过选择器只订阅需要的部分,避免不必要的重渲染。
js
// 只有 bears 变了才重渲染
const bears = useBearStore((state) => state.bears)
// 取多个值用 shallow 比较(需额外导入)
import { useShallow } from 'zustand/shallow'
const { bears, owner, increase, decrease, reset } = useBearStore(
useShallow((state) => ({
bears: state.bears,
owner: state.owner,
increase: state.increase,
decrease: state.decrease,
reset: state.reset,
}))
)
- 当你用选择器返回一个对象或数组时,每次渲染都会创建新的引用,导致组件即使数据没变也会重渲染;
- useShallow 是 Zustand 提供的一个辅助 Hook,用于浅比较选择器的返回值,避免不必要的重渲染。
7. 中间件
- 中间件是包装 store 创建过程的函数,通过中间件函数包裹 store 定义来给 store 添加额外功能(如持久化、日志、调试等),而不修改业务逻辑。
- 可以理解为:在 create 和你的 store 定义之间加一层拦截/增强。
- Zustand 提供了常用中间件,组合使用时注意顺序(一般从外到内:
immer > devtools> subscribeWithSelector > persist > store)。
下面是这四个 Zustand 中间件的详细讲解,从作用、使用场景到如何组合使用,一步步说明。
7.1 immer
作用
让你的状态更新方式更简单: 让你用 "直接修改" 的写法来处理不可变数据,Zustand 内部会自动转为不可变更新。
传统写法 vs immer 写法
javascript
// ❌ 传统 immutable 写法(一堆展开运算符)
set((state) => ({
user: {
...state.user,
profile: {
...state.user.profile,
name: '小明'
}
}
}))
// ✅ immer 写法:直接修改, 本质是传统方式的封装
set((state) => {
state.user.profile.name = '小明'
})
安装与使用
bash
npm install immer
javascript
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useStore = create(
immer((set) => ({
user: { name: '', age: 0 },
updateName: (name) =>
set((state) => {
state.user.name = name // 直接修改
}),
}))
)
适用场景
- 状态嵌套较深(对象/数组)
- 烦透了各种
...展开运算符 - 更新逻辑复杂且集中在某个深层字段
7.2 devtools
作用
连接 Redux DevTools 浏览器插件,可视化调试状态变化(历史回放、时间旅行)。
Redux DevTools 浏览器插件:这是一个专门用于调试状态管理的浏览器扩展,虽然名字带 Redux,但 Zustand、MobX、Recoil 等都能用它。
安装
无需额外安装,Zustand 内置。
基础用法
javascript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools(
(set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 }), false, 'increment'),
}),
{ name: 'CounterStore' } // DevTools 中显示的名字
)
)
set 的第三个参数是 action 名称,方便 DevTools 里查看。
适用场景
- 开发和调试阶段排查状态变更
- 想知道"什么时候、谁(哪个 action)改了状态"
- 需要回放时间、查看历史
7.3 subscribe(原生方法)和 subscribeWithSelector(中间件)
7.3.1 原生 subscribe
Zustand 的每个 store 默认自带 subscribe 方法,用于监听整个 store 的变化。
作用
subscribe 允许你在组件外部(普通 JS 模块、工具函数、WebSocket 连接等)也能监听状态变化,用于副作用管理,而useEffect 只能在组件内使用。
javascript
const useStore = create((set) => ({
count: 0,
name: 'Zustand'
}))
// 原生 subscribe:任何字段变化都会触发
const unsub = useStore.subscribe((state, prevState) => {
console.log('有状态变了')
console.log('旧状态:', prevState)
console.log('新状态:', state)
})
特点:
- 只能监听整个 store,无法指定只监听某个字段
- 回调参数是
(newState, prevState) - 适合"只要变化就执行"的场景
7.3.2 subscribeWithSelector 中间件
作用
原生 subscribe 无法选择性地监听特定字段。加上这个中间件后,subscribe 方法被增强,可以传入选择器函数,只在选定数据变化时才触发。
javascript
import { subscribeWithSelector } from 'zustand/middleware'
const useStore = create(
subscribeWithSelector((set) => ({
count: 0,
name: 'Zustand'
}))
)
// 增强后的 subscribe:只监听 count
useStore.subscribe(
(state) => state.count, // 选择器
(newCount, prevCount) => { // 回调只接收选定的值,新值与旧值
console.log(`count: ${prevCount} → ${newCount}`)
}
)
特点:
- 第一个参数是选择器,第二个是回调函数
- 回调参数是
(selectedValue, prevSelectedValue),更精准 - 使用
Object.is比较选择器返回值,只有真正变化才触发
安装
也是 Zustand 内置。
具体适用场景
- React 组件外部需要监听特定字段变化
- 实现自定义事件/副作用(如播放声音、发送分析)
- WebSocket 状态绑定
7.3.3 核心区别对比
| 特性 | 原生 subscribe |
subscribeWithSelector |
|---|---|---|
| 需要中间件 | ❌ 不需要 | ✅ 需要显式添加 |
| 监听范围 | 整个 store | 可选择特定字段 |
| 回调参数 | (state, prevState) |
(selected, prevSelected) |
| 触发频率 | 任何字段变化都触发 | 只有选择的字段变化才触发 |
| 性能 | 粗粒度 | 精细控制 |
7.3.4 使用场景例举对比
javascript
// 场景1:任何状态变化都要记录(原生 subscribe 就够)
useStore.subscribe((state) => {
console.log('快照日志:', state)
})
// 场景2:只在 token 变化时重连 WebSocket
useStore.subscribe(
(s) => s.token, // 需要 subscribeWithSelector
(token) => {
if (token) connectWS(token)
else disconnectWS()
}
)
// 场景3:只在 theme 变化时操作 DOM
useStore.subscribe(
(s) => s.theme, // 需要 subscribeWithSelector
(theme) => {
document.documentElement.dataset.theme = theme
}
)
7.3.5 联系:作用相同,能力不同(了解即可)
- 本质相同:都是监听状态变化的底层 API,都返回取消订阅函数
- 能力递进 :
subscribeWithSelector是原生subscribe的增强版 - 内部关系 :Zustand 内部实现中,
subscribeWithSelector包装了原生subscribe,用Object.is做比较
javascript
// 概念上的伪代码
function subscribeWithSelector(store) {
return (selector, callback) => {
let prevValue = selector(store.getState())
// 内部还是调用原生 subscribe
store.subscribe((state) => {
const newValue = selector(state)
if (!Object.is(newValue, prevValue)) {
callback(newValue, prevValue)
prevValue = newValue
}
})
}
}
7.3.6 组件外监听 vs 组件内选择器
| 环境 | 用什么 |
|---|---|
| React 组件内 | useStore(selector) |
| 组件外,监听整个 store | 原生 subscribe |
| 组件外,监听特定字段 | subscribeWithSelector |
总结
原生
subscribe是基础款,粗粒度监听全部变化。subscribeWithSelector是通过中间件增强的精准版,让你能选择性监听特定字段,避免无用的回调触发。想要细粒度控制在组件外监听,用后者就对了。
7.4 persist
作用
自动将状态保存到 localStorage / sessionStorage 等,刷新页面后自动恢复。
安装
内置,无需安装。
基础用法
javascript
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}),
{
name: 'my-counter',//localStorage 的 key(键名)
storage: createJSONStorage(() => localStorage), // 默认就是这个,可换sessionStorage等,修改Store存储在浏览器的位置
}
)
)
进阶配置(感兴趣可学)
javascript
persist(store, {
name: 'app-store',
storage: createJSONStorage(() => sessionStorage), // 用 sessionStorage
partialize: (state) => ({ token: state.token }), // 只持久化 token
version: 1,
migrate: (persistedState, version) => {
// 版本迁移逻辑
return persistedState
},
})
适用场景
- 用户偏好(主题、语言)
- 购物车、草稿
- JWT token
7.5 中间件组合顺序
javascript
import { create } from 'zustand'
import { persist, devtools, subscribeWithSelector } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
const useStore = create(
devtools( // 1. 最外:调试
subscribeWithSelector( // 2. 允许选择性订阅
persist( // 3. 持久化
immer((set) => ({ // 4. 深层更新 + store
user: { name: '小明', age: 18 },
incAge: () =>
set((s) => {
s.user.age += 1
}),
})),
{ name: 'my-store' }
)
),
{ name: 'AppStore' }
)
)
// 在组件外选择性监听
useStore.subscribe(
(s) => s.user.age,
(newAge) => console.log('年龄变成:', newAge)
)
8. 在 React 之外使用
Store 返回的 Hook 上挂载了 getState 和 setState,可以直接在 React 外部读取/修改状态。
javascript
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
user: { name: '小明', age: 18 },
inc: () => set((s) => ({ count: s.count + 1 })),
}))
// ✅ 在任何地方获取当前状态,不再只限于组件内
const currentState = useStore.getState()
console.log(currentState.count) // 0
console.log(currentState.user.name) // '小明'
//setState不用受已定义的更新函数的影响,做到随用随定义
// ✅ 在组件外直接更新
useStore.setState({ count: 10 })
// ✅ 传入函数(基于旧状态的更新)
useStore.setState((prev) => ({ count: prev.count + 1 }))
// ✅ 替换整个 state(第二个参数 true)
useStore.setState({ count: 0 }, true)
9. 进阶学习
- zustand官方文档:zustand.nodejs.cn/docs/gettin...
- Redux Toolkit(另一种较大型的状态管理工具):cn.redux.js.org/redux-toolk...