Zustand状态管理实战:轻量高效替代Redux的前端方案
先看效果,再看代码
我之前在项目里用 Redux,写个状态管理要配 reducer、action、selector,改个字段得跨三个文件,烦死了。后来试了 Zustand,真香——核心逻辑就一个文件,几行代码搞定。下面这个例子,你直接复制就能跑:
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}))
// 组件里直接用
function Counter() {
const { count, inc, dec } = useStore()
return (
<div>
<span>{count}</span>
<button onClick={inc}>+</button>
<button onClick={dec}>-</button>
</div>
)
}
是不是比 Context + useReducer 简洁多了?亲测有效,中小型项目直接上 Zustand,省下时间去改需求。
这个场景最好用:异步数据 + loading 状态
很多人以为 Zustand 只能存简单状态,其实它处理异步完全没问题。我常用这种结构:把 loading、error、data 全塞进 store,避免组件里写一堆 useState。比如拉用户列表:
import { create } from 'zustand'
const useUserStore = create((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null })
try {
const res = await fetch('https://jztheme.com/api/users')
const data = await res.json()
set({ users: data, loading: false })
} catch (err) {
set({ error: err.message, loading: false })
}
},
}))
组件里直接调 fetchUsers(),不用管中间状态切换。这里注意下,我踩过坑:**不要在 set 里直接解构旧状态**,比如 set({ ...state, loading: true }) 是错的!Zustand 的 set 默认是 shallow merge,但如果你传函数,它会自动合并。所以正确写法是上面那样,或者显式传入函数:
set((state) => ({ ...state, loading: true }))
不过一般没必要,直接对象就行,Zustand 会帮你 merge。
踩坑提醒:这三点一定注意
- 别在 store 里存大对象引用:Zustand 默认是浅比较,如果 store 里有个大数组,每次更新都返回新引用,所有订阅组件都会重渲染。解决办法:用
subscribeWithSelector中间件,或者拆分 store。比如我把用户信息和 UI 状态分开两个 store,互不影响。 - 中间件顺序很重要:比如你同时用 persist(持久化)和 devtools,必须把 persist 放最外层,否则 devtools 看不到持久化的初始值。我折腾了半天才发现:
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
const useStore = create(
devtools(
persist(
(set) => ({
// your state
}),
{ name: 'my-store' }
)
)
)
- 服务端渲染(SSR)要小心:Zustand 在服务端不会初始化状态,如果你在 getServerSideProps 里用 store,会报错。解决方案很简单:只在客户端用。或者用官方推荐的
createWithEqualityFn配合shallow比较,但多数情况直接加个typeof window !== 'undefined'判断更省事。
高级技巧:拆分 store 还是合并?
新手常纠结:该搞一个大 store 还是多个小 store?我的经验是:按业务模块拆,别按数据类型拆。比如电商项目,user、cart、product 各一个 store,而不是把所有 loading 放一起。
好处很明显:组件只订阅自己需要的部分。Zustand 的订阅机制很智能,如果你只取 useStore(state => state.user),那 cart 变更时这个组件不会重渲染。亲测有效,性能比 Redux 好太多。
但注意:如果两个状态经常一起变,比如搜索页的 keyword 和 results,那就放同一个 store。避免频繁跨 store 调用,否则逻辑分散难维护。
再分享个骚操作:动态 store
有时候你需要根据 ID 动态生成 store,比如聊天应用里每个会话独立状态。Zustand 官方文档没细说,但其实可以这样搞:
const createStoreMap = new Map()
function getChatStore(chatId) {
if (!createStoreMap.has(chatId)) {
const store = create((set) => ({
messages: [],
addMessage: (msg) => set((state) => ({
messages: [...state.messages, msg]
})),
}))
createStoreMap.set(chatId, store)
}
return createStoreMap.get(chatId)
}
// 组件里
function Chat({ chatId }) {
const { messages, addMessage } = getChatStore(chatId)()
// ...
}
这里注意内存泄漏问题!如果会话关闭了,记得从 map 里 delete 掉。我加了个 cleanup 函数:
function destroyChatStore(chatId) {
createStoreMap.delete(chatId)
}
虽然有点 hack,但在复杂场景下真能解决问题。当然,如果项目不大,直接用一个 store 存 { [chatId]: messages } 更简单。
结尾碎碎念
Zustand 真的让我少写了 70% 的样板代码。现在新项目基本默认选它,除非团队强制用 Redux Toolkit。它的核心思想就一点:**状态即函数,简单直接**。不用学一堆概念,上手就能干活。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如结合 Immer 写 immutable 更新),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流——毕竟前端方案没有银弹,只有更适合当前项目的解法。

暂无评论