在React项目中,你是否受够了Redux的繁琐配置?是否厌倦了Context带来的不必要的渲染?Zustand作为一个轻量级的状态管理库,仅用一個create()函数就能搞定状态管理,无需Provider嵌套,无需编写冗余的Action和Reducer。
本文将从状态管理方案选型讲起,深入讲解Zustand的核心API、精准订阅机制、持久化与中间件扩展等实战技巧,帮助你在项目中快速落地。无论你是想简化现有状态管理架构,还是寻找更高效的解决方案,这篇文章都能给你答案。
为什么选择Zustand
现状痛点
在React项目中管理状态一直是开发者面临的难题。传统的Context API虽然使用简单,但存在严重的性能问题——当Provider的value变化时,所有消费该Context的组件都会无条件重新渲染。Redux虽然功能强大,但配置繁琐,样板代码过多,学习曲线陡峭。对于中小型项目来说,这些方案显得过于笨重。
Zustand正是为解决这些痛点而生的。它由React Three Fiber团队开发,设计理念是”用最小的API,实现最大的效率”。你不需要Provider,不需要定义Action和Reducer,只需要一个create()函数就能搞定状态管理。
Zustand核心优势
- 极简API:一行代码创建store,没有任何繁琐配置
- 精准订阅:基于Selector的订阅机制,只订阅需要的状态片段,避免不必要的渲染
- 无需Provider:不依赖Context,避免组件树嵌套地狱
- TypeScript友好:完美支持类型推导,IDE提示完善
- 中间件扩展:内置persist、devtools、immer等常用中间件
- 性能优异:初始化速度极快,内存占用极低
主流状态管理方案对比
| 单向数据流 |
原子化 |
Proxy |
| redux |
Recoil |
Mobx |
| zustand |
jotai |
valtio |
在选择时,通常是选择 zustand、jotai、valtio,他们都是对应类型前者的优化版本。
单向数据流的优势是数据本身比较干净。负担轻。但是当数据结构非常复杂的时候,通常需要结合不可变数据集 immutable.js、immer.js 等才能做到最佳的性能表现。
原子化方式与 proxy 方式都是一致的,都是收集数据与 UI 的绑定关系,当数据发生变化时,UI 会自动更新。他们的区别就是,原子化在写法上,是先定义原子,然后通过原子来管理数据。
而 Proxy 是先定义一个大一点的对象,然后通过 Proxy 来劫持对象的属性,然后再将属性与 UI 进行绑定。因此在性能表象上,原子化的性能会略微好一些,他少了劫持的过程。但是当数据开始变得复杂时,原子化的写法可能也会比较繁琐。
在处理复杂数据时,初始化创建 Atom 对象的开销比较大,但是 Proxy 的包装开销也会比较大。他们在更新时的开销都比较小。
在处理大型复杂列表数据时,他们的表现如下所示
| 库 |
初始化速度 |
更新速度 |
内存占用 |
开发复杂度 |
| Zustand |
极快 |
快 |
极低,原生对象 |
较高 |
| jotai |
较慢 |
精准,快 |
最高,原子实例多 |
偏高 |
| valtio |
中等偏慢 |
精准,快 |
偏高,Proxy 开销大 |
低 |
在更新上的具体细节表现如下
| 库 |
通知复杂度 |
渲染复杂度 |
原理 |
| Zustand |
O(N) |
O(1) |
线性遍历订阅列表 + Selector 比对 |
| Jotai |
O(1) |
O(1) |
依赖图。原子 A 变了,直接找到订阅了 A 的那一个组件。 |
| Valtio |
O(1) |
O(1) |
Proxy 追踪。属性 A 变了,直接精准通知访问过属性 A 的组件。 |
快速上手:创建第一个Store
基础用法
Zustand的使用非常简单,只需要导入create函数并定义状态和操作方法:
import { create } from 'zustand'
interface TabItem { id: string title: string }
interface TabState { tabs: TabItem[] currentTabId: string addTab: (tab: TabItem) => void removeTab: (id: string) => void setCurrentTab: (id: string) => void }
export const useTabStore = create<TabState>((set) => ({ tabs: [], currentTabId: '', addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab] })), removeTab: (id) => set((state) => ({ tabs: state.tabs.filter((t) => t.id !== id), currentTabId: state.currentTabId === id ? '' : state.currentTabId })), setCurrentTab: (id) => set({ currentTabId: id }) }))
|
在组件中使用同样直观:
import { useTabStore } from './store/tabStore'
export default function TabView() { const { tabs, currentTabId, addTab, setCurrentTab } = useTabStore()
return ( <div> <button onClick={() => addTab({ id: 'settings', title: '设置' })}> 新增 Tab </button> <div style={{ display: 'flex', gap: 8 }}> {tabs.map((tab) => ( <div key={tab.id} onClick={() => setCurrentTab(tab.id)} style={{ padding: 4, borderBottom: tab.id === currentTabId ? '2px solid blue' : 'none', cursor: 'pointer' }} > {tab.title} </div> ))} </div> </div> ) }
|
这就是Zustand的全部——创建store,在组件中引入,直接使用。所有状态都由Zustand管理,任何组件都能随时访问。
处理异步操作
异步处理在Zust中同样简单,直接在set回调中执行async/await即可:
import { create } from 'zustand'
interface Todo { id: number title: string completed: boolean }
interface TodoState { todos: Todo[] error: string | null isLoading: boolean fetchTodos: () => Promise<void> toggleTodo: (id: number) => void }
export const useTodoStore = create<TodoState>((set) => ({ todos: [], error: null, isLoading: false, fetchTodos: async () => { set({ isLoading: true, error: null }) try { const res = await fetch('https://jsonplaceholder.typicode.com/todos') const data = await res.json() set({ todos: data.slice(0, 10), isLoading: false }) } catch (error) { set({ error: (error as Error).message, isLoading: false }) } }, toggleTodo: (id) => set((state) => ({ todos: state.todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) })) }))
|
精准订阅:避免无效渲染
问题分析
假设我们有这样一个store,包含用户信息和主题设置:
import { create } from 'zustand'
const useAppStore = create((set) => ({ user: { name: '张三', age: 25, isLogin: true }, theme: 'light', setTheme: (newTheme: string) => set({ theme: newTheme }), setUserAge: (age: number) => set((state) => ({ user: { ...state.user, age } })) }))
|
如果组件直接订阅整个状态对象:
function UserName() { const { user } = useAppStore() console.log('UserName 渲染了') return <div>用户名:{user.name}</div> }
function ThemeDisplay() { const { theme } = useAppStore() console.log('ThemeDisplay 渲染了') return <div>当前主题:{theme}</div> }
|
当调用setUserAge(26)时,即使ThemeDisplay只用到theme字段(未变化),也会被迫重新渲染。这就是Context和普通订阅的典型问题。
解决方案:Selector精准订阅
Zustand的精髓在于Selector——通过函数精确指定需要订阅的状态片段:
function UserName() { const userName = useAppStore((state) => state.user.name) console.log('UserName 渲染了') return <div>用户名:{userName}</div> }
function ThemeDisplay() { const theme = useAppStore((state) => state.theme) console.log('ThemeDisplay 渲染了') return <div>当前主题:{theme}</div> }
|
效果对比:
- 调用setUserAge(26) → 只有UserName重新渲染(user.age变化)
- 调用setTheme(‘dark’) → 只有ThemeDisplay重新渲染(theme变化)
Selector的工作原理是:Zustand会对比上次渲染返回的值,只有值发生变化时才触发组件更新。这意味着你可以订阅任意深度的嵌套属性,而不必担心其他字段的变化会影响组件。
计算属性与派生状态
对于需要基于状态计算派生数据的场景,可以在组件中组合多个Selector:
interface StoreState { items: number[] total: number addItem: (item: number) => void }
const useStore = create<StoreState>((set) => ({ items: [], total: 0, addItem: (item) => set((state) => ({ items: [...state.items, item], total: state.total + item })) }))
function TotalDisplay() { const items = useStore((state) => state.items) const total = items.reduce((sum, item) => sum + item, 0) return <div>总和:{total}</div> }
|
状态持久化:自动保存到本地存储
基础持久化
Zustand内置的persist中间件让状态持久化变得极其简单:
import { create } from 'zustand' import { persist } from 'zustand/middleware'
interface UserState { token: string userInfo: any setToken: (token: string) => void setUserInfo: (info: any) => void logout: () => void }
export const useUserStore = create<UserState>()( persist( (set) => ({ token: '', userInfo: null, setToken: (token) => set({ token }), setUserInfo: (userInfo) => set({ userInfo }), logout: () => set({ token: '', userInfo: null }) }), { name: 'user-storage' } ) )
|
刷新页面后,状态会自动从localStorage恢复,无需手动处理。
高级配置
persist中间件支持丰富的配置选项:
import { persist, createJSONStorage } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
const useStore = create<UserState>()( persist( (set) => ({ }), { name: 'my-app-storage', storage: createJSONStorage(() => sessionStorage), partialize: (state) => ({ token: state.token, theme: state.theme }), onRehydrateStorage: () => (state) => { console.log('hydration finished', state) } } ) )
|
基础配置
开发时配合Redux DevTools使用,只需要添加devtools中间件:
import { create } from 'zustand' import { devtools } from 'zustand/middleware'
const useCounterStore = create( devtools( (set) => ({ count: 0, increase: () => set((state) => ({ count: state.count + 1 })), decrease: () => set((state) => ({ count: state.count - 1 })) }), { name: 'counter-store' } ) )
|
打开浏览器DevTools的Redux面板,你可以:
- 查看完整的state快照
- 查看所有action调用记录
- 时间旅行调试,回退到任意状态
- 手动dispatch action进行测试
生产环境配置
生产环境中建议限制devtools的使用:
import { devtools } from 'zustand/middleware'
const store = create( devtools( (set) => ({ }), { enabled: process.env.NODE_ENV === 'development', name: 'my-app' } ) )
|
复杂状态更新:Immer集成
问题场景
当状态嵌套较深时,传统的不可变更新写法非常冗长:
interface DeepState { nested: { object: { count: number } } }
const useStore = create<DeepState>((set) => ({ nested: { object: { count: 0 } }, increment: () => set((state) => ({ nested: { ...state.nested, object: { ...state.nested.object, count: state.nested.object.count + 1 } } })) }))
|
这种写法不仅繁琐,而且极易出错。
Immer解决方案
结合Immer中间件,可以用更直观的可变式写法更新深层状态:
import { create } from 'zustand' import { immer } from 'zustand/middleware/immer'
interface DeepState { nested: { object: { count: number } } increment: () => void }
const useStore = create<DeepState>()( immer((set) => ({ nested: { object: { count: 0 } }, increment: () => set((state) => { state.nested.object.count++ }) })) )
|
Immer会自动处理不可变更新,让代码保持简洁。需要注意的是,SSR场景下建议慎用Immer,因为它会带来额外的CPU开销。
最佳实践与注意事项
Store模块化拆分
在中大型项目中,建议将不同业务域的状态拆分为独立的store文件:
stores/ ├── userStore.ts // 用户认证状态 ├── cartStore.ts // 购物车状态 ├── uiStore.ts // UI相关状态(主题、侧边栏等) └── settingsStore.ts // 用户设置
|
每个store维护自己的状态和操作逻辑,职责清晰,便于维护:
interface UserState { isAuthenticated: boolean user: User | null login: (credentials: Credentials) => Promise<void> logout: () => void }
export const useUserStore = create<UserState>((set) => ({ isAuthenticated: false, user: null, login: async (credentials) => { const user = await authService.login(credentials) set({ isAuthenticated: true, user }) }, logout: () => set({ isAuthenticated: false, user: null }) }))
|
模块化拆分的好处:
- 状态职责单一,易于追踪和维护
- 避免单个store过于臃肿
- 新成员容易理解状态结构和变化来源
- 便于代码分割(code splitting)
避免常见陷阱
- 不要在组件外直接调用getState()
const store = create((set) => ({ count: 0 })) console.log(store.getState().count)
function MyComponent() { const count = useStore((state) => state.count) }
|
- 保持状态序列化的考虑
如果使用persist中间件,确保状态可以被序列化。避免在state中存储函数、类实例或循环引用的对象。
- Selector中避免创建新对象
const user = useStore((state) => ({ name: state.name }))
const name = useStore((state) => state.name)
|
合理使用中间件
中间件虽然强大,但不要过度使用。只添加实际需要的中间件,避免不必要的性能开销。
总结
Zustand以其极简的API、优异的性能和灵活的扩展性,成为React状态管理的优秀选择。它不追求大而全,而是专注于解决实际开发中的痛点。
通过本文,你应该已经掌握了:
- Zustand的核心概念和优势
- 创建和使用store的多种方式
- Selector精准订阅的实现原理
- 持久化、DevTools、Immer等常用技巧
- 模块化拆分和最佳实践
对于中小型项目,Zustand完全可以替代Redux,提供更简洁的开发体验。对于大型项目,它可以作为领域状态管理的轻量选择,与服务端状态管理方案(如React Query)配合使用。
如果你正在寻找一个简单、高效、现代化的React状态管理方案,Zustand绝对值得一试。