在现代 React 应用开发中,状态管理是核心环节之一。相较于 Redux 的繁琐配置和 Context API 的性能局限,Zustand 凭借其简洁的 API 设计、优秀的类型支持和轻量化特性,成为越来越多开发者的首选。本文将以一个「文章列表状态管理」的实战案例为核心,从基础概念到高级特性,全面讲解 Zustand 的使用方法,并与 Redux/RTK 进行对比分析。
一、Zustand 核心优势
在开始实战前,先明确 Zustand 为何能脱颖而出:
- 极简 API:无需 Provider 包裹根组件,一行代码创建 Store,学习成本极低。
- 原生 TypeScript 支持:类型推导清晰,无需额外定义大量类型文件,天生类型安全。
- 中间件生态 :官方提供
devtools
(Redux 调试)、persist
(状态持久化)等实用中间件,开箱即用。 - 轻量高效:包体积仅约 1KB(gzip 后),无多余依赖,性能损耗可忽略。
- 灵活的状态更新:支持直接修改状态或函数式更新,满足复杂场景需求。
二、实战准备:环境与依赖
首先确保项目中安装 Zustand(支持 React 16.8+):
bash
# npm
npm install zustand
# yarn
yarn add zustand
# pnpm
pnpm add zustand
若需要使用调试工具或状态持久化,无需额外安装依赖------相关中间件已内置在 zustand/middleware
中。
三、从 0 到 1:构建文章列表 Store
以下将以「文章列表状态管理」为例,分步骤讲解 Zustand 的完整使用流程,涵盖类型定义、状态初始化、异步操作、中间件集成等核心环节。
3.1 第一步:定义 State 与 Actions 类型(类型安全基石)
Zustand 推荐先明确状态(State
)和操作(Actions
)的类型,这是实现类型安全的关键。通过 TypeScript 约束,可在开发阶段规避状态赋值错误、函数参数不匹配等问题。
typescript
// 引入业务相关类型(根据实际项目定义)
import type { Article, QueryParams, PageResult } from '@/types'
// 1. 状态(State)类型:定义需要存储的数据结构
type State = {
// 核心业务数据
hasQueryArticleList: boolean; // 是否已发起过列表查询(避免重复初始化)
articleList: Article[]; // 文章列表数据
currentPage: number; // 当前页码(分页控制)
totalPage: number; // 总页数(判断是否有更多数据)
hasMore: boolean; // 是否还有更多数据(控制"加载更多"按钮)
scrollTop: number; // 页面滚动高度(持久化滚动位置)
// 交互状态(提升用户体验)
loading: boolean; // 加载中状态(防重复请求、显示加载动画)
error: string | null; // 错误信息(请求失败时显示)
}
// 2. 操作(Actions)类型:定义修改状态的方法
type Actions = {
// 异步业务操作
queryArticleList: (queryParams: QueryParams) => Promise<void>; // 初始化查询列表
loadMore: (queryParams: QueryParams) => Promise<void>; // 加载更多
// 基础状态操作
setScrollTop: (scrollTop: number) => void; // 更新滚动高度
resetArticleState: () => void; // 重置所有状态
clearError: () => void; // 清除错误信息
}
3.2 第二步:初始化状态(统一初始值)
为避免状态分散赋值导致的不一致,建议定义一个初始状态对象,后续直接复用:
typescript
const initialState: State = {
// 核心业务数据初始值
hasQueryArticleList: false,
articleList: [],
currentPage: 1, // 分页默认从第 1 页开始
totalPage: 0, // 初始无数据,总页数为 0
hasMore: false, // 初始无更多数据
scrollTop: 0, // 初始滚动高度为 0
// 交互状态初始值
loading: false, // 初始非加载中
error: null // 初始无错误
}
3.3 第三步:实现 Store 核心逻辑(StateCreator)
通过 StateCreator
函数定义状态与操作的具体逻辑,该函数接收两个核心方法:
set
:用于更新状态,支持对象式(set({ key: value })
)和函数式(set(state => ({ key: state.key + 1 }))
)更新。get
:用于获取当前状态(如判断加载中状态、获取当前页码)。
typescript
import { create, type StateCreator } from 'zustand'
import { queryArticleList } from '@/api' // 文章列表接口请求函数
// 定义 Store 创建器:整合 State 与 Actions
const storeCreator: StateCreator<State & Actions> = (set, get) => ({
// 1. 合并初始状态
...initialState,
// 2. 基础操作实现
/** 重置状态(适用场景:切换筛选条件、清空列表) */
resetArticleState: () => set(initialState),
/** 清除错误信息(适用场景:用户关闭错误提示) */
clearError: () => set({ error: null }),
/** 更新滚动高度(适用场景:页面滚动事件中调用) */
setScrollTop: (scrollTop: number) => set({ scrollTop }),
// 3. 异步操作:初始化查询文章列表(覆盖旧数据)
queryArticleList: async (queryParams: QueryParams) => {
// 防重复请求:若正在加载中,直接返回
const { loading } = get()
if (loading) return Promise.resolve()
try {
// 开启加载中 + 清空旧错误
set({ loading: true, error: null })
// 发起接口请求(显式类型断言,确保类型安全)
const res = await queryArticleList(queryParams)
const pageData = res.data as PageResult<Article>
// 请求成功:更新状态(覆盖旧列表、更新分页信息)
set({
articleList: pageData.records,
currentPage: pageData.current,
totalPage: pageData.pages,
hasMore: pageData.current < pageData.pages,
hasQueryArticleList: true,
error: null
})
} catch (err) {
// 请求失败:格式化错误信息
const errorMsg = err instanceof Error ? err.message : '查询文章列表失败'
console.error('【列表查询失败】:', err)
set({ error: errorMsg })
} finally {
// 无论成功/失败,关闭加载中
set({ loading: false })
}
},
// 4. 异步操作:加载更多(追加数据)
loadMore: async (queryParams: QueryParams) => {
// 边界判断:加载中 / 无更多数据时,不发起请求
const { loading, hasMore, currentPage } = get()
if (loading || !hasMore) return Promise.resolve()
try {
set({ loading: true, error: null })
// 构造下一页参数:复用筛选条件,页码+1
const requestParams = { ...queryParams, pageNum: currentPage + 1 }
// (可选)模拟网络延迟(开发环境测试用)
await new Promise(resolve => setTimeout(resolve, 500))
// 发起请求
const res = await queryArticleList(requestParams)
const pageData = res.data as PageResult<Article>
// 请求成功:追加数据(而非覆盖)
set(state => ({
articleList: [...state.articleList, ...(pageData.records || [])],
currentPage: pageData.current,
totalPage: pageData.pages,
hasMore: pageData.current < pageData.pages,
error: null
}))
} catch (err) {
const errorMsg = err instanceof Error ? err.message : '加载更多失败'
console.error('【加载更多失败】:', err)
set({ error: errorMsg })
} finally {
set({ loading: false })
}
}
})
3.4 第四步:集成中间件(调试 + 持久化)
Zustand 中间件可增强 Store 功能,常用的有 devtools
(Redux 调试工具支持)和 persist
(状态持久化)。通过链式调用整合中间件,最终创建可在组件中使用的 Hook。
typescript
import { devtools, persist, createJSONStorage } from 'zustand/middleware'
// 创建并导出 Store Hook
const useArticleStore = create<State & Actions>()(
// 1. 集成 Redux 调试工具(仅开发环境生效)
devtools(
// 2. 集成状态持久化(存储到 localStorage)
persist(storeCreator, {
name: 'article-storage', // 持久化存储的 key(localStorage 中可见)
storage: createJSONStorage(() => localStorage), // 存储方式(支持 localStorage/sessionStorage)
// 选择性持久化:仅存储需要保留的状态(排除临时状态如 loading/error)
partialize: state => {
const { loading, error, ...persistedState } = state
return persistedState
}
}),
// 调试工具配置
{
name: 'ArticleStore', // 调试工具中显示的 Store 名称(多 Store 时便于区分)
enabled: process.env.NODE_ENV === 'development' // 仅开发环境启用
}
)
)
export default useArticleStore
四、在组件中使用 Store
创建好 Store 后,在 React 组件中通过自定义 Hook(如 useArticleStore
)获取状态和操作,用法简洁且无需 Provider 包裹。
4.1 基础用法:获取状态与调用操作
tsx
import React, { useEffect } from 'react'
import useArticleStore from '@/store/useArticleStore'
import type { QueryParams } from '@/types'
const ArticleListPage: React.FC = () => {
// 1. 获取状态(推荐使用解构,避免不必要的重渲染)
const {
articleList,
loading,
error,
hasMore,
scrollTop,
queryArticleList,
loadMore,
setScrollTop,
clearError
} = useArticleStore()
// 2. 初始化查询参数
const defaultQueryParams: QueryParams = {
pageNum: 1,
pageSize: 10,
category: 'tech' // 示例:默认查询"技术"分类
}
// 3. 页面首次加载时查询列表
useEffect(() => {
queryArticleList(defaultQueryParams)
}, [queryArticleList, defaultQueryParams])
// 4. 监听滚动事件,记录滚动高度(用于持久化)
useEffect(() => {
const handleScroll = () => {
const top = window.scrollY
setScrollTop(top)
}
window.addEventListener('scroll', handleScroll)
// 组件卸载时移除监听
return () => window.removeEventListener('scroll', handleScroll)
}, [setScrollTop])
// 5. 组件挂载时恢复滚动位置(持久化生效)
useEffect(() => {
window.scrollTo(0, scrollTop)
}, [scrollTop])
// 渲染逻辑...
return (
<div className="article-list-page">
{/* 错误提示 */}
{error && (
<div className="error-bar">
{error}
<button onClick={clearError}>关闭</button>
</div>
)}
{/* 文章列表 */}
<div className="article-list">
{loading && <div>加载中...</div>}
{!loading && articleList.map(article => (
<div key={article.id} className="article-item">
<h3>{article.title}</h3>
<p>{article.summary}</p>
</div>
))}
</div>
{/* 加载更多按钮 */}
{hasMore && !loading && (
<button onClick={() => loadMore(defaultQueryParams)}>
加载更多
</button>
)}
</div>
)
}
export default ArticleListPage
4.2 性能优化:避免不必要的重渲染
默认情况下,组件会在 Store 中任何状态变化时重渲染。若组件仅依赖部分状态,可通过选择器(Selector) 精确获取所需状态,减少重渲染次数。
方式 1:使用函数式选择器(基础优化)
tsx
// 仅获取 articleList 和 loading,仅当这两个状态变化时才重渲染
const articleList = useArticleStore(state => state.articleList)
const loading = useArticleStore(state => state.loading)
方式 2:使用 shallow
比较(复杂对象/数组优化)
若选择器返回对象或数组(如 { articleList, hasMore }
),默认会进行引用比较 ,导致每次状态变化都重渲染。此时可结合 shallow
中间件进行浅比较:
typescript
// 1. 引入 shallow 中间件
import { shallow } from 'zustand/shallow'
// 2. 用 shallow 比较对象/数组
const { articleList, hasMore } = useArticleStore(
state => ({
articleList: state.articleList,
hasMore: state.hasMore
}),
shallow // 浅比较:仅当 articleList 或 hasMore 本身变化时才重渲染
)
五、Zustand 与 Redux/RTK 的对比分析
选择状态管理库时,了解不同方案的优缺点至关重要。以下从多个维度对比 Zustand 与 Redux(及 Redux Toolkit):
5.1 核心概念与API设计
特性 | Zustand | Redux (传统) | Redux Toolkit (RTK) |
---|---|---|---|
核心概念 | 基于 Hook,无 Provider 包裹 | 单一 Store、Action、Reducer、Middleware | 简化 Redux,整合 createSlice、createAsyncThunk |
状态更新 | 直接通过 set 方法修改 | 必须通过 dispatch(action) 触发 reducer | 通过 createSlice 的 reducers 直接修改(Immer 支持) |
异步操作 | 直接在 Action 中写 async/await | 需要额外 middleware(如 redux-thunk) | 内置 createAsyncThunk 处理异步 |
模板代码量 | 极少(无需定义 action type、action creator) | 极多(action type、action creator、reducer 分离) | 较少(createSlice 自动生成 action) |
类型支持 | 原生支持,类型推导自然 | 需手动定义大量类型(action、state、reducer) | 类型支持良好,但仍需显式定义部分类型 |
5.2 代码量对比(以文章列表为例)
Zustand 实现(约 150 行)
- 直接定义 State + Actions 类型
- 实现核心逻辑(同步/异步操作)
- 集成中间件(调试 + 持久化)
Redux Toolkit 实现(约 300 行)
- 定义 State 类型
- 创建 Slice(含 reducers 和 extraReducers)
- 定义异步 thunk(createAsyncThunk)
- 配置 Store(configureStore)
- 在根组件添加 Provider
- 组件中通过 useSelector + useDispatch 使用
5.3 适用场景分析
场景 | 推荐方案 | 理由 |
---|---|---|
小型项目/快速原型 | Zustand | 学习成本低,代码简洁,无需配置 |
中大型项目 | Zustand/RTK | Zustand 适合状态分散管理;RTK 适合严格遵循 Flux 架构的团队 |
团队协作(多人开发) | RTK | 严格的规范(action-reducer 分离)便于协作,减少代码风格差异 |
已有 Redux 经验的团队 | RTK | 平滑过渡,保留团队技术栈熟悉度 |
对包体积敏感的项目 | Zustand | 体积仅 1KB,远小于 RTK(约 15KB) |
需要与 Redux 生态集成 | RTK | 可无缝使用 redux-saga、redux-observable 等中间件 |
5.4 性能对比
- Zustand:通过选择器(selector)精确控制重渲染,性能优异;无 Context 嵌套问题。
- Redux:默认使用 Context 传递 Store,深层嵌套组件可能存在性能问题(需配合 memo + 精确 selector 优化)。
- RTK:通过 createSelector 等工具优化性能,但本质仍依赖 Context,大型应用需额外优化。
六、高级特性与最佳实践
6.1 多 Store 设计
Zustand 支持创建多个独立的 Store(如 useArticleStore
、useUserStore
),避免单一 Store 过于臃肿。多 Store 之间可通过 get
方法相互访问:
typescript
// 在 UserStore 中访问 ArticleStore 的状态
const useUserStore = create((set, get) => ({
getUserArticleCount: () => {
// 获取 ArticleStore 的文章列表
const articleList = useArticleStore.getState().articleList
// 获取当前用户 ID
const userId = get().userId
// 计算当前用户的文章数量
return articleList.filter(article => article.authorId === userId).length
}
}))
6.2 状态持久化进阶
persist
中间件支持更多配置,满足复杂场景需求:
blacklist
/whitelist
:排除/仅保留指定状态(与partialize
功能类似)。onRehydrateStorage
:持久化恢复前的回调(如处理过期数据)。version
:版本控制(用于数据迁移)。
示例:处理过期的持久化数据
typescript
persist(storeCreator, {
name: 'article-storage',
storage: createJSONStorage(() => localStorage),
// 持久化恢复前触发
onRehydrateStorage: (state) => {
// 返回一个回调,接收恢复后的状态
return (rehydratedState, error) => {
if (error) {
console.error('持久化数据恢复失败:', error)
} else if (rehydratedState) {
// 假设数据有效期为 1 小时,超过则重置
const now = Date.now()
const lastUpdateTime = rehydratedState.lastUpdateTime || 0
if (now - lastUpdateTime > 3600000) {
useArticleStore.setState(initialState) // 重置为初始状态
}
}
}
}
})
6.3 调试工具使用
集成 devtools
后,可在浏览器开发者工具的「Redux」标签中查看:
- 状态变更历史(每一次
set
操作都会被记录)。 - 每次变更的前后状态对比。
- 异步操作的执行流程(如
queryArticleList
的开始/结束)。
调试时可通过 name
字段区分多个 Store,便于定位问题。
七、总结
Zustand 以其「简洁、高效、类型安全」的特性,为 React 状态管理提供了优雅的解决方案。与 Redux/RTK 相比,它更适合追求开发效率、低学习成本的项目,同时在性能和扩展性上也不逊色。
本文通过实战案例,覆盖了 Zustand 的核心用法:
- 类型定义 :先定义
State
和Actions
类型,确保类型安全。 - Store 创建 :通过
StateCreator
实现状态与操作逻辑,支持同步/异步操作。 - 中间件集成 :利用
devtools
调试、persist
持久化,增强 Store 功能。 - 组件使用:通过自定义 Hook 获取状态,结合选择器优化性能。
无论是中小型项目的简单状态管理,还是大型项目的复杂状态拆分,Zustand 都能胜任。建议在实际项目中尝试,并根据业务需求灵活运用其高级特性。