用Zustand轻松搞定React状态管理的那些事儿
项目初期的技术选型
最近刚做完一个电商类的中型项目,说起来还挺有意思的。这个项目的核心需求是一个购物车功能,用户可以在多个页面间切换,同时购物车的状态需要实时更新。一开始我还在纠结用什么状态管理工具,Redux?MobX?后来同事推荐了Zustand,说是轻量又简单,我就决定试试。
为什么选Zustand呢?其实原因很简单:项目工期有点紧,Redux那一套太重了,写起来麻烦;MobX虽然也不错,但我对它的响应式机制一直有点不放心,尤其是复杂场景下容易出问题。而Zustand的API设计非常直观,上手快,文档也清晰,就它了。
从零开始用Zustand
先说一下基本用法吧,核心代码其实就这几行:
import create from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (itemId) => set((state) => ({
items: state.items.filter(item => item.id !== itemId)
})),
clearCart: () => set({ items: [] }),
}));
上面这段代码定义了一个简单的购物车状态,包括添加商品、移除商品和清空购物车的功能。在组件里使用也非常方便:
import React from 'react';
import { useCartStore } from './store';
function Cart() {
const items = useCartStore(state => state.items);
const clearCart = useCartStore(state => state.clearCart);
return (
<div>
<h3>购物车</h3>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={clearCart}>清空购物车</button>
</div>
);
}
刚开始用的时候感觉特别爽,代码简洁明了,状态管理的逻辑也很清晰。但随着项目的推进,问题就慢慢浮现了。
最大的坑:性能问题
项目做到一半的时候,我发现一个问题:每次状态更新,所有用到useCartStore的组件都会重新渲染。这在小项目里可能没什么感觉,但在我们的项目里,购物车相关的组件非常多,性能开销一下子就上去了。
折腾了半天才发现,Zustand默认是浅比较的,也就是说只要状态对象发生变化,所有订阅该状态的组件都会重新渲染。为了解决这个问题,我尝试了几种方案:
- 方案一:手动优化选择器。通过传递自定义的比较函数来避免不必要的渲染。
const items = useCartStore(
state => state.items,
(prev, next) => prev.length === next.length // 简单比较长度
);
这种方法确实有效,但写起来有点繁琐,尤其是当状态结构复杂的时候。
- 方案二:拆分Store。把购物车的状态拆分成多个独立的小Store,每个组件只订阅自己需要的部分。
const useCartItemStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));const useCartTotalStore = create((set) => ({
total: 0,
updateTotal: (newTotal) => set({ total: newTotal }),
}));
`>persist
<p>这种做法的好处是职责更清晰了,但维护成本也高了不少,尤其是当不同Store之间需要共享数据时,逻辑会变得复杂。</p>
<ul>
<li><strong>方案三:引入中间件</strong>。Zustand支持中间件,比如和devtools,还有一些社区提供的性能优化工具。</li>
</ul></code></pre>javascript
import { devtools, persist } from 'zustand/middleware';const useCartStore = create(
persist(
devtools((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (itemId) => set((state) => ({
items: state.items.filter(item => item.id !== itemId)
})),
})),
{ name: 'cart-storage' }
)
);
>
<p>最终我选择了方案一和方案三结合的方式。虽然还是有些小问题(比如偶尔某些组件还是会多渲染一次),但整体性能已经可以接受了。</p><h2>另一个难题:异步操作</h2>
<p>项目里还有一个需求是购物车的数据需要从后端接口获取,并且支持同步更新。刚开始我是直接在组件里调用API,然后更新Store,代码大概长这样:</p>
<pre class="pure-highlightjs line-numbers language-javascript"><code class="no-highlight language-javascript">function fetchCartData() {
fetch('https://jztheme.com/api/cart')
.then(response => response.json())
.then(data => useCartStore.setState({ items: data.items }));
}
>`>
<p>看起来没问题,但实际上踩了几个坑:</p>
<ul>
<li>异步操作没有统一管理,导致代码分散在各个组件里,维护起来很痛苦。</li>
<li>多次调用API时,状态更新可能会冲突,尤其是在快速切换页面的时候。</li>
</ul>
<p>后来我调整了一下方案,把异步逻辑封装到Store里:</p></code></pre>javascript
const useCartStore = create((set) => ({
items: [],
fetchCart: async () => {
const response = await fetch('https://jztheme.com/api/cart');
const data = await response.json();
set({ items: data.items });
},
addItem: async (item) => {
await fetch('https://jztheme.com/api/cart/add', { method: 'POST', body: JSON.stringify(item) });
set((state) => ({ items: [...state.items, item] }));
},
}));
这样做的好处是逻辑集中了,代码也更清晰。不过还是有一点小问题:如果API调用失败,状态回滚的逻辑需要额外处理,这点我还没完全搞定。
回顾与反思
总的来说,Zustand在这个项目里的表现还算不错。它轻量、易用,适合中小型项目,但也有一些需要注意的地方:
- 性能优化是个大问题,尤其是当组件数量增多时,必须提前规划好。
- 异步操作的管理需要额外注意,建议尽量封装到Store里。
- 对于复杂项目,拆分Store可能是必要的,但要权衡维护成本。
当然,也有一些地方还可以改进。比如,我希望未来能深入研究一下Zustand的中间件生态,看看有没有更好的性能优化工具。另外,状态回滚的逻辑还需要进一步完善。
以上是我个人对Zustand的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续我会继续分享这类博客。

暂无评论