用Zustand轻松搞定React状态管理的那些事儿

❤若惜 框架 阅读 2,309
赞 20 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近刚做完一个电商类的中型项目,说起来还挺有意思的。这个项目的核心需求是一个购物车功能,用户可以在多个页面间切换,同时购物车的状态需要实时更新。一开始我还在纠结用什么状态管理工具,Redux?MobX?后来同事推荐了Zustand,说是轻量又简单,我就决定试试。

用Zustand轻松搞定React状态管理的那些事儿

为什么选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 }),
}));
`&gt;
&lt;p&gt;这种做法的好处是职责更清晰了,但维护成本也高了不少,尤其是当不同Store之间需要共享数据时,逻辑会变得复杂。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案三:引入中间件&lt;/strong&gt;。Zustand支持中间件,比如
persistdevtools,还有一些社区提供的性能优化工具。&lt;/li&gt;
&lt;/ul&gt;</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(&#039;https://jztheme.com/api/cart&#039;)
.then(response =&gt; response.json())
.then(data =&gt; useCartStore.setState({ items: data.items }));
}
&gt;
&lt;p&gt;看起来没问题,但实际上踩了几个坑:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异步操作没有统一管理,导致代码分散在各个组件里,维护起来很痛苦。&lt;/li&gt;
&lt;li&gt;多次调用API时,状态更新可能会冲突,尤其是在快速切换页面的时候。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后来我调整了一下方案,把异步逻辑封装到Store里:&lt;/p&gt;</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的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续我会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论