导航
导航
文章目录󰁋
  1. 为什么选择Zustand
    1. 现状痛点
    2. Zustand核心优势
    3. 主流状态管理方案对比
  2. 快速上手:创建第一个Store
    1. 基础用法
    2. 处理异步操作
  3. 精准订阅:避免无效渲染
    1. 问题分析
    2. 解决方案:Selector精准订阅
    3. 计算属性与派生状态
  4. 状态持久化:自动保存到本地存储
    1. 基础持久化
    2. 高级配置
  5. 开发工具:Redux DevTools集成
    1. 基础配置
    2. 生产环境配置
  6. 复杂状态更新:Immer集成
    1. 问题场景
    2. Immer解决方案
  7. 最佳实践与注意事项
    1. Store模块化拆分
    2. 避免常见陷阱
  8. 总结
NEW
🚀

前端系统进阶指南

系统化学习前端知识

关注公众号

公众号:前端进价之旅

极简React状态管理方案:Zustand使用指南与实战技巧

在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

在选择时,通常是选择 zustandjotaivaltio,他们都是对应类型前者的优化版本。

单向数据流的优势是数据本身比较干净。负担轻。但是当数据结构非常复杂的时候,通常需要结合不可变数据集 immutable.jsimmer.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——通过函数精确指定需要订阅的状态片段:

// 正确用法:使用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:

// 在store中定义计算逻辑
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中的key
}
)
)

刷新页面后,状态会自动从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), // 使用sessionStorage
partialize: (state) => ({
// 只持久化部分字段
token: state.token,
theme: state.theme
}),
onRehydrateStorage: () => (state) => {
// 持久化恢复后的回调
console.log('hydration finished', state)
}
}
)
)

开发工具:Redux DevTools集成

基础配置

开发时配合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中显示的名称
)
)

打开浏览器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维护自己的状态和操作逻辑,职责清晰,便于维护:

// stores/userStore.ts
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)

避免常见陷阱

  1. 不要在组件外直接调用getState()
// 错误用法
const store = create((set) => ({ count: 0 }))
console.log(store.getState().count) // 可能拿到过期值

// 正确用法
function MyComponent() {
const count = useStore((state) => state.count)
// 在组件内使用
}
  1. 保持状态序列化的考虑

如果使用persist中间件,确保状态可以被序列化。避免在state中存储函数、类实例或循环引用的对象。

  1. Selector中避免创建新对象
// 错误:每次渲染都创建新对象
const user = useStore((state) => ({ name: state.name }))

// 正确:直接返回原始值
const name = useStore((state) => state.name)
  1. 合理使用中间件

    中间件虽然强大,但不要过度使用。只添加实际需要的中间件,避免不必要的性能开销。

总结

Zustand以其极简的API、优异的性能和灵活的扩展性,成为React状态管理的优秀选择。它不追求大而全,而是专注于解决实际开发中的痛点。

通过本文,你应该已经掌握了:

  • Zustand的核心概念和优势
  • 创建和使用store的多种方式
  • Selector精准订阅的实现原理
  • 持久化、DevTools、Immer等常用技巧
  • 模块化拆分和最佳实践

对于中小型项目,Zustand完全可以替代Redux,提供更简洁的开发体验。对于大型项目,它可以作为领域状态管理的轻量选择,与服务端状态管理方案(如React Query)配合使用。

如果你正在寻找一个简单、高效、现代化的React状态管理方案,Zustand绝对值得一试。

支持一下
扫一扫,支持poetries
  • 微信扫一扫
  • 支付宝扫一扫