状态管理一直是前端开发绕不开的话题。
用过 Redux 的朋友都懂,它虽然功能强大、生态丰富,但搭配 TypeScript 写起来实在太繁琐。一旦项目复杂一点,你得手动写一堆模板代码:Action 类型、Reducer 分支、Store 配置、Dispatch、Selector,再加上 TS 的类型声明……光是搭一套结构就能忙活半天了。
直到我接触了 Zustand,一个更符合 React 思维的状态管理库,我才发现——原来共享状态也能这么轻爽。
这篇文章就结合我自己做的 demo 项目,带你完整体验一下 Zustand + TS 的状态管理流程。
🚀 Zustand 是什么?为什么要选择它?
Zustand 是由 React Three Fiber 团队开发的一个轻量级、基于 hooks 的状态管理库,它的设计哲学是:用最小的 API,实现最大的效率。
你不需要 Provider,也不需要定义 Action、Reducer,只用一个 create() 函数就能搞定状态管理。
在使用前,它给我的第一印象,就是 Vue 的组合式 API + Pinia 的融合体。
选它的理由很简单:
- 写法简单,学习成本低
- 完美支持 TypeScript 类型推导
- 不依赖 context,不会出现 Provider 嵌套地狱
- 按需订阅,性能好,不会整个组件树重渲染
- 支持状态持久化、中间件扩展、状态模块拆分
可以说,它正好填补了 Redux 太重、useContext 太弱之间的那块空白区域。
🎯 搞个简单案例练练手
想象一下你正在写一个后台管理系统,支持“多页签”切换。每个 tab 代表一个打开的模块(比如用户列表、设置页等),我们希望:
- 支持新增和关闭 tab
- 能记录当前激活的 tab
- 所有页面都能访问和修改 tab 状态
如果用 Redux 来实现这些,得:
- 定义 TabState 接口
- 写 reducer 和 action type
- 用 immer 管理不可变数据
- 加 middleware 管异步
- 用 hooks 或 connect 绑定组件
太繁琐了,不如用 Zustand 搞个 demo,十几行代码就能搞定共享状态。
第一步:创建 Store
我们先来定义一下状态结构,使用 TypeScript 写一个接口:
// 定义单个 tab 的结构
interface TabItem {
id: string
title: string
}
// 整个 store 的结构
interface TabState {
tabs: TabItem[]
currentTabId: string
addTab: (tab: TabItem) => void
removeTab: (id: string) => void
setCurrentTab: (id: string) => void
}
然后用 Zustand 创建一个 store:
import { create } from 'zustand'
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 })
}))
是不是很简洁?一个 create() 函数就搞定共享状态,类型提示也直接到位。
第二步:页面中使用
接下来我们在页面中使用它。
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>
)
}
现在这个组件可以随时新增 Tab、切换当前 Tab,所有状态都由 Zustand 管理,其他组件也能随时访问它,非常方便。
✨ 你可能不知道的 Zustand 高级特性
除了基本的状态管理,Zustand 还提供了很多高级功能,比如
精准订阅,避免无效渲染
举个栗子:
假设我们有一个管理用户信息和主题设置的 store:
import { create } from 'zustand';
// 创建一个包含多个字段的状态
const useAppStore = create((set) => ({
// 用户信息
user: {
name: '张三',
age: 25,
isLogin: true
},
// 主题设置
theme: 'light',
// 修改主题的方法
setTheme: (newTheme) => set({ theme: newTheme }),
// 修改用户年龄的方法
setUserAge: (age) => set((state) => ({ user: { ...state.user, age } }))
}));
如果组件直接订阅整个状态对象,即使只用到其中一个字段,当状态中任何字段变化时,组件都会重新渲染:
// 组件 A:只需要显示用户名
function UserName() {
// 订阅了整个状态对象(错误示范)
const { user } = useAppStore();
console.log('UserName 组件渲染了');
return <div>用户名:{user.name}</div>;
}
// 组件 B:只需要显示主题
function ThemeDisplay() {
// 同样订阅了整个状态对象(
const { theme } = useAppStore();
console.log('ThemeDisplay 组件渲染了');
return <div>当前主题:{theme}</div>;
}
当调用 setUserAge(26) 修改用户年龄时,user 对象变化 → 即使 ThemeDisplay 只用到 theme(未变化),也会被强制重新渲染,控制台会打印 ThemeDisplay 组件渲染了。
那我们怎样解决这个问题? Selector 精确订阅!
通过 Selector 函数指定组件需要的具体字段,组件只会在 该字段变化时 重新渲染:
// 组件 A:只订阅 user.name
function UserName() {
// 精确选择需要的字段
const userName = useAppStore((state) => state.user.name);
console.log('UserName 组件渲染了');
return <div>用户名:{userName}</div>;
}
// 组件 B:只订阅 theme
function ThemeDisplay() {
// 精确选择需要的字段
const theme = useAppStore((state) => state.theme);
console.log('ThemeDisplay 组件渲染了');
return <div>当前主题:{theme}</div>;
}
- 当调用
setUserAge(26)时,只有user.age变化,user.name未变 →UserName组件不会重新渲染。 - 当调用
setTheme('dark')时,theme变化 → 只有ThemeDisplay组件重新渲染,UserName不受影响。
状态持久化:自动保存到 localStorage
Zustand 内置了 persist 中间件,可以一行代码让你的状态写入本地存储:
import { persist } from 'zustand/middleware'
const useUserStore = create(
persist(
(set) => ({
token: '',
setToken: (val) => set({ token: val }),
}),
{
name: 'user-storage', // localStorage key
}
)
)
下次页面加载会自动恢复状态,无需你手动做缓存逻辑。
DevTools 支持
开发时我们想配合 Redux DevTools 面板使用,也只需要包一层:
import { devtools } from 'zustand/middleware'
const useStore = create(devtools((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 }))
})))
开起来直接就能调状态了,兼容性也很好。
⚠️ 使用 Zustand 的注意事项
Zustand 写法清爽,但也别太随性,以下几点建议别踩坑:
- 避免在组件外直接 getState() ,容易拿到过期值,优先使用 hooks 获取。
- 状态不可变更新需自己管理,Zustand 默认不帮你处理 immutable。
- 模块化拆分 store,保持单一职责,一个功能一个 store 文件,利于维护和团队协作。
🧩 拆分 Store:一个模块一个文件
在中大型项目中,推荐将不同的业务状态拆成独立模块,每个模块单独写一个 Store 文件,结构更清晰、维护更方便。
比如我们可以这样拆:
stores/tabStore.ts:管理多页签状态stores/userStore.ts:管理用户登录态stores/formStore.ts:表单缓存数据stores/themeStore.ts:主题设置
每个 Store 内部维护自己的状态结构和操作函数,只暴露对应的 useXXXStore hook。
// stores/userStore.ts
interface UserState {
token: string
login: (token: string) => void
logout: () => void
}
export const useUserStore = create<UserState>((set) => ({
token: '',
login: (token) => set({ token }),
logout: () => set({ token: '' }),
}))
其他组件中直接使用:
const { token, login } = useUserStore()
这样做的好处是:
- 状态职责单一,易于维护
- 避免一个大 Store 里塞满所有业务逻辑
- 对新成员更友好,知道状态在哪改、在哪用
✅ 总结
如果你觉得 Redux 模板冗余、useContext 又力不从心,那 Zustand 会是一个非常舒服的中间选择:
- 写法清爽,没有 ceremony
- 类型推导完整,几乎不用手动声明
- 不用 Provider、不用 reducer、不用手动 dispatch
- 还能支持持久化、选择订阅、调试工具,非常全面
对我来说,Zustand 就是那个 刚刚好 的选择:干净利落、类型清晰、可扩展性强,非常适合中小型项目的快速开发。
如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看!
后续我会持续更新 React 重学系列,👉 记得关注我,不错过每一篇实战干货!