写在前面
在 React 应用开发中,状态管理从来不只是“存一个值”那么简单。
从最初的 useState 到自定义 Hooks,再到 Context + useReducer 的组合,我们一直在寻找一种既能保持简洁,又能应对复杂性的方案。尤其当应用开始需要跨组件共享状态、持久化数据、甚至支持调试与扩展时,那些“看似够用”的临时方案,往往会在不经意间变成维护的泥潭。
而今天,我们站在一个更成熟的工具生态之上——Zustand 的出现,不是为了颠覆,而是为了简化那些本不该复杂的逻辑。它不强制你采用单一全局状态树,也不绑架你的架构选择;相反,它用极简的 API,让我们能把精力真正聚焦在业务本身。
接下来,就让我们从一个最朴素的计数器开始,看看 Zustand 是如何优雅地解决“刷新不丢状态”这个经典难题的。
没有zustand 时
在没有 Zustand 的时代,我们通常依靠 自定义 Hooks 来实现组件内的状态管理——这确实是一种简洁、符合 React 设计模式 的方式。
就拿之前的 Todo 小项目来说,我们可能会这样组织代码:
// src/hooks/useTodos.ts
export function useTodos() {
// 初始化状态,从本地存储读取
const [todos, setTodos] = useState<Todo[]>(
() => getStorage<Todo[]>(STORAGE_KEY, [])
);
// 状态变化时自动保存到本地存储
useEffect(() => {
setStorage<Todo[]>(STORAGE_KEY, todos);
}, [todos]);
// Todo 操作方法
const addTodo = (title: string) => {
const newTodo: Todo = { id: +new Date(), title, completed: false };
setTodos([...todos, newTodo]);
};
//其它省略
// 返回状态和方法
return { todos, addTodo, toggleTodo, removeTodo };
}
配合一个通用的本地存储工具:
// src/utils/storages.ts
export function getStorage<T>(key: string, defaultValue: T): T {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
}
export function setStorage<T>(key: string, value: T) {
localStorage.setItem(key, JSON.stringify(value));
}
在应用入口处使用:
// src/App.tsx
export default function App() {
const { todos, addTodo, toggleTodo, removeTodo } = useTodos();
return (
<div>
<TodoInput onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onRemove={removeTodo}
/>
</div>
);
}
这样做当然无可厚非:没有引入第三方依赖,逻辑清晰,TypeScript 支持良好,也确实完成了本地持久化的需求。
但是,
当应用逐渐复杂,状态需要跨越多个组件层级、甚至多个页面共享时,我们就不得不考虑将状态“提升”——最常见的做法是引入 React Context。虽然 Context 能解决跨组件通信的问题,但它也带来了新的挑战:
逻辑分散:状态初始化、持久化逻辑、操作方法被拆分在多个文件中,难以集中维护;
模板代码冗余:每个需要持久化的状态几乎都要重复写 useState + useEffect + 存储工具调用;
性能隐患:Context 的更新会让用户重新渲染,除非手动优化(如拆分 Context 或使用 useMemo);
扩展性受限:一旦需要加入中间件(如日志、时间旅行、版本迁移等),自定义 Hook 很难支持。或者说可以支持,但支持方式却没那么“优雅”
更关键的是,这种“每个功能写一个自定义 Hook”的模式,在项目规模扩大后,会迅速演变成大量结构相似但无法复用的代码片段——看似简单,实则隐藏了长期维护的成本。
正因如此,我们才需要一个更先进、更专注的状态管理工具。
它应当天然支持模块化,内置持久化能力,避免不必要的重渲染,并且让状态逻辑集中、可测试、可扩展。
而 Zustand,正是为解决这些问题而生。
zustand
Zustand 是一个极简却功能强大的 React 状态管理库,其核心理念是“每个 Store 负责管理特定的状态”,从而天然支持多状态管理。与传统全局单一状态树不同,Zustand 鼓励将状态按功能拆分为多个独立的 Store,彼此解耦、职责清晰,极大提升了代码的可维护性和可测试性。
我们可以轻松地在 Vite 脚手架生成的经典页面基础上,引入 Zustand 来增强交互能力。比如,为页面添加 +1、-1 和 reset 按钮,并让这些操作的状态在页面刷新后依然保留——这正是 Zustand 结合其内置中间件 persist 所擅长的场景。
以下是一个典型的使用示例:
接下来我会用zustand来增加下面的+1 -1 reset按钮 多余的样式删掉了
import {
useState
} from 'react';
import { useCounterStore } from './store/counter';
import { useTodoStore } from './store/todo';
function App() {
const {
count,
increment,
decrement,
reset,
} = useCounterStore();
return (
<>
<button onClick={increment}>
count is {count}
</button>
<button onClick={decrement}>
减1
</button>
<button onClick={reset}>
reset
</button>
</>
)
}
export default App
对应的 Store 定义如下:
import { create }from 'zustand';
import { persist } from 'zustand/middleware';
interface CounterState {
count: number;
increment: () => void;
decrement:() => void;
reset:() => void;
}
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',
})
)
这里要介绍下 persist:
persist是 Zustand 提供的一个内置中间件,用于将状态持久化到本地存储
如 localStorage 或 sessionStorage。通过使用 persist,即使用户刷新页面、关闭浏览器再重新打开,应用的状态(例如计数器的数值)也不会丢失。
它的工作原理是自动在状态发生变化时将其序列化为 JSON 字符串并保存到指定的存储介质中,并在页面加载时从存储中读取对应的数据,反序列化后作为 Store 的初始状态。这一过程对开发者几乎是透明的,极大地简化了状态持久化的实现。
在使用 persist 时,一个关键的配置参数是 name,例如 { name: 'counter' }。这个 name 会作为本地存储中的键名(key),实际数据会被保存在 localStorage.getItem('counter') 中。设置唯一的 name
这样做不仅能标识当前 Store 的数据,还能确保多个 Store 之间的数据彼此隔离,避免因键名冲突导致状态覆盖或读取错误。
persist 的工作流程大致如下:当页面加载时,中间件会尝试从 localStorage(或其他指定存储)中读取以 name 为键的数据;如果存在,则通过 JSON.parse 将其解析为 JavaScript 对象,并用作 Store 的初始状态;随后,每当状态发生变更,persist 会自动调用 JSON.stringify 将最新状态序列化,并重新写入存储。整个过程形成了一个闭环,实现了“自动保存 + 自动恢复”的效果。
实际存储的内容通常是一个包含 state 和 version 字段的 JSON 对象,例如 {"state":{"count":5},"version":0}。这意味着即使页面刷新,Store 也能准确还原之前的状态(如 count 仍为 5)。
除了 name,persist 还支持多种可选配置:可以通过 storage 指定使用 sessionStorage 而非默认的 localStorage;通过 version 实现版本控制,便于未来进行数据结构迁移;还可以通过 onRehydrateStorage 注册回调函数,在数据从存储中恢复时执行自定义逻辑(如显示加载提示或处理兼容性问题)。
总之,
persist中间件显著增强了 Zustand 的实用性,而name参数则是其实现可靠持久化的基石。有了它,开发者可以轻松实现“刷新不丢状态”的用户体验,让前端应用更加健壮和用户友好。
总结
回过头看,从自定义 Hook 到 Zustand,并不是堆砌技术,而是对可维护性、可扩展性和开发者体验的持续追求。
Zustand 之所以“聪明”,不在于它功能繁多,而在于它精准地切中了现代前端状态管理的核心痛点:解耦、持久、高效、可测。通过 persist 中间件,它把原本需要手动处理的存储逻辑封装得几乎“隐形”;通过独立 Store 的设计,它让每个功能模块的状态边界清晰、互不干扰;而这一切,都建立在极低的学习成本和零样板代码的基础上。
更重要的是,Zustand 甘愿做一个克制的工具——这恰恰是它能在众多状态管理库中脱颖而出的原因。
所以,当你下次面对一个需要跨页面共享、本地持久化、甚至未来可能加入时间旅行或日志追踪的状态场景时,不妨试试 Zustand。