Zustand:轻量级状态管理,从入门到实践

Zustand:轻量级状态管理,从入门到实践

在现代前端开发中,状态管理是一个绕不开的话题。随着应用复杂度的提升,组件间共享状态的需求变得越来越强烈。Redux、MobX 等传统方案虽然强大,但往往伴随着繁琐的样板代码和陡峭的学习曲线。

今天我们要介绍的是 Zustand ------ 一个极简、快速、可扩展的状态管理库。它基于 hooks 思想设计,API 简洁,几乎零样板代码,却能完美应对中小型应用乃至大型项目的状态管理需求。

如果说国家需要有中央银行,那么前端项目就需要中央状态管理系统。Zustand 就是这样一个"中央银行",它将状态集中存储,并提供一套清晰的修改规则,让组件可以轻松共享和操作数据。

本文将通过两个实战案例(计数器、Todo 应用),带你从零掌握 Zustand 的核心用法,并深入理解其高级特性。


第一章:快速上手 Zustand --- 计数器

我们先从一个最简单的计数器开始,感受 Zustand 的简洁与强大。

1. 安装

bash 复制代码
npm install zustand
# 或
yarn add zustand

2. 创建第一个 store

src/store/counter.ts 中:

tsx 复制代码
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// 定义状态类型
interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

// 创建 store
export const useCounterStore = create<CounterState>()(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: 'counter', // 持久化存储的 key
    }
  )
)

代码解读:

  • create<T>() 用于创建 store,返回一个自定义 hook。
  • persist 是 Zustand 提供的一个中间件,可以将 store 中的数据自动持久化到 localStorage 中。
  • set 函数用于更新状态,可以直接传入新状态,也可以传入一个函数接收当前状态并返回新状态。

3. 在组件中使用

App.tsx 中引入并使用:

tsx 复制代码
import { useCounterStore } from './store/counter'

function App() {
  const { count, increment, decrement, reset } = useCounterStore()

  return (
    <div className="card">
      <button onClick={increment}>增加</button>
      <span>{count}</span>
      <button onClick={decrement}>减少</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

无需 Provider,无需 Context,直接调用 hook 即可获取状态和操作方法! 这就是 Zustand 的魅力所在。


第二章:管理复杂状态 --- Todo 应用

接下来我们实现一个 Todo 列表,涵盖添加、切换完成状态、删除等功能,并使用 persist 中间件让数据持久化。

1. 定义类型

首先在 src/types/index.ts 中定义 Todo 类型:

ts 复制代码
export interface Todo {
  id: number
  text: string
  completed: boolean
}

2. 创建 Todo Store

src/store/todo.ts 中:

tsx 复制代码
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { Todo } from '../types'

interface TodoState {
  todos: Todo[]
  addTodo: (text: string) => void
  toggleTodo: (id: number) => void
  removeTodo: (id: number) => void
}

export const useTodoStore = create<TodoState>()(
  persist(
    (set) => ({
      todos: [],
      addTodo: (text) =>
        set((state) => ({
          todos: [
            ...state.todos,
            {
              id: Date.now(),
              text,
              completed: false,
            },
          ],
        })),
      toggleTodo: (id) =>
        set((state) => ({
          todos: state.todos.map((todo) =>
            todo.id === id ? { ...todo, completed: !todo.completed } : todo
          ),
        })),
      removeTodo: (id) =>
        set((state) => ({
          todos: state.todos.filter((todo) => todo.id !== id)
        })),
    }),
    {
      name: 'todos', // 持久化 key
    }
  )
)

3. 在组件中使用

App.tsx 中添加 Todo 相关 UI:

tsx 复制代码
import { useState } from 'react'
import { useTodoStore } from './store/todo'

function App() {
  const [inputValue, setInputValue] = useState('')
  const { todos, addTodo, toggleTodo, removeTodo } = useTodoStore()

  const handleAdd = () => {
    if (inputValue.trim() === '') return
    addTodo(inputValue)
    setInputValue('')
  }

  return (
    <section>
      <h2>Todo 列表 ({todos.length})</h2>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
          placeholder="输入待办事项"
        />
        <button onClick={handleAdd}>添加</button>
      </div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </section>
  )
}

效果 :添加的待办事项会立即显示,切换复选框可以划掉文字,删除按钮可移除对应项。刷新页面后数据依然存在 ------ 因为 persist 中间件已经帮我们同步到了 localStorage。

两个组合在一起的效果图

第三章:组合多个 Store 与性能优化

随着应用规模增长,我们往往会将不同领域的状态拆分到独立的 store 中。Zustand 鼓励这种模式 ------ 每个 store 都是独立的,通过自定义 hook 组合使用。

1. 在组件中同时使用多个 store

tsx 复制代码
import { useCounterStore } from './store/counter'
import { useTodoStore } from './store/todo'

function App() {
  const count = useCounterStore((state) => state.count)
  const todos = useTodoStore((state) => state.todos)

  // ...
}

2. 使用 selector 优化性能

Zustand 默认会对整个 state 进行浅比较,但如果你的组件只关心部分状态,建议使用 selector 来避免不必要的渲染。

tsx 复制代码
// 只选取 count,当 count 变化时才重新渲染
const count = useCounterStore((state) => state.count)

// 只选取 increment 函数(函数永远不会变,所以永远不会触发重渲染)
const increment = useCounterStore((state) => state.increment)

3. 创建组合式 Hook

如果多个 store 的状态经常一起使用,可以封装一个自定义 Hook:

tsx 复制代码
import { useCounterStore } from './counter'
import { useTodoStore } from './todo'

export const useCombinedStore = () => {
  const count = useCounterStore((state) => state.count)
  const todos = useTodoStore((state) => state.todos)
  const addTodo = useTodoStore((state) => state.addTodo)

  return { count, todos, addTodo }
}

4. 在 store 中访问其他 store

有时一个 store 的 action 需要依赖另一个 store 的状态。你可以在 action 内部导入并使用其他 store 的 hook(注意避免循环依赖)。

例如,在 todo store 中添加日志,记录当前计数器值:

tsx 复制代码
import { useCounterStore } from './counter'

export const useTodoStore = create<TodoState>()(
  (set, get) => ({
    // ...
    addTodo: (text) => {
      const count = useCounterStore.getState().count
      console.log('当前计数器值:', count)
      set((state) => ({
        todos: [...state.todos, { id: Date.now(), text, completed: false }]
      }))
    },
  })
)

第四章:中间件与高级用法

Zustand 提供了多种中间件,除了我们已使用的 persist,还有:

  • devtools:与 Redux DevTools 集成,方便调试。
  • subscribe:订阅状态变化。
  • immer:允许以可变方式编写更新逻辑。

1. 使用 immer 简化更新

安装 immer 相关中间件:

bash 复制代码
npm install immer

然后修改 store:

tsx 复制代码
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

interface TodoState {
  todos: Todo[]
  addTodo: (text: string) => void
  toggleTodo: (id: number) => void
}

export const useTodoStore = create<TodoState>()(
  immer(
    persist(
      (set) => ({
        todos: [],
        addTodo: (text) =>
          set((state) => {
            state.todos.push({ id: Date.now(), text, completed: false })
          }),
        toggleTodo: (id) =>
          set((state) => {
            const todo = state.todos.find((t) => t.id === id)
            if (todo) todo.completed = !todo.completed
          }),
      }),
      { name: 'todos' }
    )
  )
)

使用 immer 后,你可以像修改可变对象一样更新状态,代码更简洁。

2. 订阅状态变化

Zustand 的 store 本身就是一个可观察对象,你可以通过 subscribe 监听变化:

tsx 复制代码
const unsubscribe = useTodoStore.subscribe((state, prevState) => {
  console.log('todos 从', prevState.todos, '变为', state.todos)
})

// 取消订阅
unsubscribe()

这在需要在状态变化时触发副作用(如埋点、本地存储同步)时非常有用。


总结

通过本文的两个实战案例,我们完整地体验了 Zustand 的核心能力:

  • 极简 API:无需繁琐的 action 类型、reducer、provider,几行代码即可创建可全局访问的状态。
  • 灵活组合:可以按领域拆分成多个 store,通过 hooks 自由组合。
  • 中间件生态:支持持久化、调试、不可变更新等常见需求。
  • 完美集成 TypeScript:类型推导自然,开发体验极佳。

相比 Redux,Zustand 的学习成本几乎为零;相比 Context + useReducer,Zustand 避免了 Provider 嵌套和性能问题。它特别适合以下场景:

  • 中小型项目,希望快速迭代
  • 大型项目,需要清晰的状态边界和性能优化
  • 任何需要全局共享状态,但又不想引入复杂概念的项目

希望这篇文章能帮助你快速上手 Zustand,并在实际项目中得心应手。如果你有任何问题或经验分享,欢迎在评论区留言讨论!

相关推荐
codingWhat2 小时前
介绍一个手势识别库——AlloyFinger
前端·javascript·vue.js
踩着两条虫2 小时前
VTJ.PRO 双向代码转换原理揭秘
前端·vue.js·人工智能
扉川川2 小时前
OpenClaw 架构解析:一个生产级 AI Agent 是如何设计的
前端·人工智能
远山枫谷2 小时前
一文理清页面/组件通信与 Store 全局状态管理
前端·微信小程序
codingWhat2 小时前
手撸一个「能打」的 React Table 组件
前端·javascript·react.js
HelloReader2 小时前
Tauri 应用安全从开发到发布的威胁防御指南
前端
bluceli2 小时前
WebAssembly实战指南:将高性能计算带入浏览器
前端·webassembly
yuki_uix2 小时前
Object.entries:优雅处理 Object 的瑞士军刀
前端·javascript
奇迹_h6 小时前
打造你的HTML5打地鼠游戏:零基础入门实践
前端