嘿,各位前端伙伴们,咱们聊聊 React 的状态管理吧。
想必我们都经历过这样的场景:项目初期,一切都很简单,几个组件,用 useState
管理着各自的本地状态,岁月静好。但随着应用功能的迭代和复杂度的提升,状态管理很快就变成了一张盘根错E结的网。"属性(props)一层层往下钻"让人头疼,useContext
+ useReducer
的组合拳虽然能解决问题,但写起来也不轻松。至于 Redux,它很强大,但那套繁琐的"样板戏"(boilerplate)也常常劝退不少人。
感觉就像是为了管理状态,我们写的代码比实现功能本身还要多。
但如果我告诉你,有一种更简单、更清爽的方式呢?一个既强大又轻量的全局状态管理方案?
Zustand,闪亮登场!
Zustand 是一个"小而美、快又稳"的状态管理库,它完全基于 Hooks 构建。它的设计理念就是极简主义,API 直观到让你几乎没有学习成本,立马就能上手。在本文中,我们将通过一个完整的实战案例,深入体验 Zustand 是如何把"快乐"重新带回到我们的状态管理工作中。
为什么是 Zustand?那个让你"豁然开朗"的瞬间
在现代前端开发模式中,我们的工作流通常是 "UI 组件 + 全局应用状态管理"。
对于非常迷你的项目,或许我们根本不需要一个专门的状态库。但对于中大型项目,一个统一管理状态的中央"仓库"(store)几乎是标配。这时候,react-router-dom
负责路由,而状态管理库则负责"灵魂"。
Zustand 正是在这个领域大放异彩。它巧妙地将中心化状态管理的思想,用一套令人愉悦的、纯粹的 Hooks API 包装起来。你可以轻松地将所有分散在组件中的状态,收归中央(store)统一管理。
话不多说,我们来看一个经典例子,感受下它到底有多简单。
经典计数器:大道至简
每个状态管理库的"新手村"教程里,都少不了一个计数器(Counter)的例子。但 Zustand 的实现,清爽得令人发指。
首先,我们为计数器创建一个 store:
javascript
// src/store/count.js
import { create } from 'zustand';
// 'create' 函数创建了一个 hook
export const useCounterStore = create((set) => ({
count: 0,
// 'set' 函数用于更新状态
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}));
没错,就这么几行!我们用 Zustand 提供的 create
函数,创建了一个名为 useCounterStore
的自定义 Hook。这个 Hook 就是我们访问状态和操作状态(actions)的唯一入口。set
函数则负责执行状态的更新。
接下来,在 React 组件中使用它:
jsx
// src/components/Counter/index.jsx
import { useCounterStore } from '../../store/count.js'
const Counter = () => {
// 像调用普通 hook 一样,解构出需要的状态和方法
const {
count,
increment,
decrement
} = useCounterStore()
return (
<>
Counter: {count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
)
}
export default Counter
看到了吗?没有 Provider
包裹根组件,没有复杂的 connect
或 mapState
。就像调用一个普通的 Hook 一样,直接、简单、高效。这就是 Zustand 的魅力所在。
轻松拿捏异步操作
真实世界的应用,远不止计数器这么简单。我们经常需要从远端 API 获取数据,并处理好加载(loading)和错误(error)状态。Zustand 处理起异步逻辑同样得心应手。
在我们的例子中,我们需要从 GitHub API 拉取一个用户的仓库列表。首先,用 Axios 把 API 请求相关的逻辑封装好:
javascript
// src/api/config.js
import axios from 'axios'
// 设置基础 URL
axios.defaults.baseURL = "https://api.github.com"
export default axios.create({})
// src/api/repo.js
import axios from './config'
// 获取仓库列表的 API
export const getRepoList = async (owner) =>
await axios.get(`/users/${owner}/repos`)
然后,我们创建一个专门管理仓库列表状态的 store:
javascript
// src/store/repos.js
import { getRepoList } from '../api/repo'
import { create } from 'zustand'
export const useRepoStore = create((set) => ({
repos: [], // 仓库数据
loading: false, // 加载状态
error: null, // 错误信息
// 这是一个异步 action
fetchRepos: async () => {
set({ loading: true, error: null }); // 开始请求,更新 loading 状态
try {
const res = await getRepoList('shunwuyu'); // 'shunwuyu' 是我的 GitHub ID,欢迎 follow :)
set({ repos: res.data, loading: false }) // 请求成功,更新数据和 loading 状态
} catch(error) {
set({ error: error.message, loading: false }) // 请求失败,更新 error 和 loading 状态
}
}
}));
这个 useRepoStore
完美地封装了 repos
数据、loading
和 error
这三个核心状态。fetchRepos
这个 action 则是一个 async
函数,它清晰地描述了整个数据请求的业务流程,并在流程的每个阶段通过 set
函数更新状态。
对应的组件代码也同样优雅:
jsx
// src/components/RepoList/index.jsx
import { useRepoStore } from '../../store/repos'
import { useEffect } from 'react'
const RepoList = () => {
const { repos, loading, error, fetchRepos } = useRepoStore()
useEffect(() => {
fetchRepos() // 在组件挂载时调用 action
}, [fetchRepos]) // 依赖项里加上 fetchRepos 是一个好习惯
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>
return (
<div>
<h2>My GitHub Repos</h2>
<ul>
{repos.map(repo => (
<li key={repo.id}>
<a href={repo.html_url} target="_blank" rel="noreferrer">{repo.name}</a>
<p>{repo.description || 'No description'}</p>
</li>
))}
</ul>
</div>
)
}
export default RepoList
组件的逻辑非常清晰:在 useEffect
中触发数据获取,然后根据 store 中的 loading
, error
, repos
状态来渲染不同的 UI。声明式、可预测、易于维护。
管理复杂状态:从 To-Do List 看全局状态模块化
Zustand 不仅能处理简单状态,对于更复杂的数据结构(如数组、对象)也游刃有余。接下来,让我们看看经典的待办事项(To-Do List)例子。
我们可以借鉴代码中注释的理念:全局状态模块化。每个独立的业务领域,都应该有自己独立的 store 文件。
javascript
// src/store/todo.js
import { create } from 'zustand';
export const useTodosStore = create((set) => ({
todos: [
{ id: 1, text: '学习 Zustand', completed: false },
{ id: 2, text: '打豆豆', completed: true },
],
// 添加 todo
addTodo: (text) => set((state) => ({
todos: [
...state.todos,
{ id: Date.now(), text, completed: false }
]
})),
// 切换 todo 状态
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(
(todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
// 删除 todo
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id)
}))
}))
这个 useTodosStore
精心管理着一个 todos
数组,并提供了增、删、改的 actions。请注意,当新状态需要依赖旧状态时,我们向 set
函数传递一个 (state) => ({...})
形式的函数,这能确保我们总是在最新的状态之上进行修改,避免了潜在的 state race condition。
汇总:一个清爽的 App 入口
最后,我们看看 App.jsx
组件,它将所有功能组合在一起:
jsx
// src/App.jsx
import Counter from './components/Counter'
import TodoList from './components/TodoList'
import RepoList from './components/RepoList'
import './App.css'
function App() {
return (
<>
<h1>Zustand Demo</h1>
<Counter />
<hr />
<RepoList />
<hr />
<TodoList />
</>
)
}
export default App
我们的根组件 App
现在变得异常整洁。它不需要关心任何具体的状态和业务逻辑,只是作为一个"组织者",把各个独立的、由 Zustand 驱动的业务组件渲染出来。
结语
Zustand 为 React 的状态管理带来了一股清流。它务实、简单、无侵入性,让你从繁琐的样板代码中解放出来,重新专注于构建应用本身。通过拥抱 Hooks 并保持 API 的极简,Zustand 让状态管理从一种负担,变成了一种乐趣。
如果你正在开启一个新项目,或者希望简化现有项目的状态管理方案,我强烈推荐你尝试一下 Zustand。它很可能会成为你工具箱里那块不可或-缺的"瑞士军刀"。