Zustand状态管理实战:轻量高效替代Redux的前端方案

欧阳桂霞 框架 阅读 1,983
赞 32 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

我之前在项目里用 Redux,写个状态管理要配 reducer、action、selector,改个字段得跨三个文件,烦死了。后来试了 Zustand,真香——核心逻辑就一个文件,几行代码搞定。下面这个例子,你直接复制就能跑:

Zustand状态管理实战:轻量高效替代Redux的前端方案

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
  dec: () => set((state) => ({ count: state.count - 1 })),
}))

// 组件里直接用
function Counter() {
  const { count, inc, dec } = useStore()
  return (
    <div>
      <span>{count}</span>
      <button onClick={inc}>+</button>
      <button onClick={dec}>-</button>
    </div>
  )
}

是不是比 Context + useReducer 简洁多了?亲测有效,中小型项目直接上 Zustand,省下时间去改需求。

这个场景最好用:异步数据 + loading 状态

很多人以为 Zustand 只能存简单状态,其实它处理异步完全没问题。我常用这种结构:把 loading、error、data 全塞进 store,避免组件里写一堆 useState。比如拉用户列表:

import { create } from 'zustand'

const useUserStore = create((set) => ({
  users: [],
  loading: false,
  error: null,
  fetchUsers: async () => {
    set({ loading: true, error: null })
    try {
      const res = await fetch('https://jztheme.com/api/users')
      const data = await res.json()
      set({ users: data, loading: false })
    } catch (err) {
      set({ error: err.message, loading: false })
    }
  },
}))

组件里直接调 fetchUsers(),不用管中间状态切换。这里注意下,我踩过坑:**不要在 set 里直接解构旧状态**,比如 set({ ...state, loading: true }) 是错的!Zustand 的 set 默认是 shallow merge,但如果你传函数,它会自动合并。所以正确写法是上面那样,或者显式传入函数:

set((state) => ({ ...state, loading: true }))

不过一般没必要,直接对象就行,Zustand 会帮你 merge。

踩坑提醒:这三点一定注意

  • 别在 store 里存大对象引用:Zustand 默认是浅比较,如果 store 里有个大数组,每次更新都返回新引用,所有订阅组件都会重渲染。解决办法:用 subscribeWithSelector 中间件,或者拆分 store。比如我把用户信息和 UI 状态分开两个 store,互不影响。
  • 中间件顺序很重要:比如你同时用 persist(持久化)和 devtools,必须把 persist 放最外层,否则 devtools 看不到持久化的初始值。我折腾了半天才发现:
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

const useStore = create(
  devtools(
    persist(
      (set) => ({
        // your state
      }),
      { name: 'my-store' }
    )
  )
)
  • 服务端渲染(SSR)要小心:Zustand 在服务端不会初始化状态,如果你在 getServerSideProps 里用 store,会报错。解决方案很简单:只在客户端用。或者用官方推荐的 createWithEqualityFn 配合 shallow 比较,但多数情况直接加个 typeof window !== 'undefined' 判断更省事。

高级技巧:拆分 store 还是合并?

新手常纠结:该搞一个大 store 还是多个小 store?我的经验是:按业务模块拆,别按数据类型拆。比如电商项目,user、cart、product 各一个 store,而不是把所有 loading 放一起。

好处很明显:组件只订阅自己需要的部分。Zustand 的订阅机制很智能,如果你只取 useStore(state => state.user),那 cart 变更时这个组件不会重渲染。亲测有效,性能比 Redux 好太多。

但注意:如果两个状态经常一起变,比如搜索页的 keyword 和 results,那就放同一个 store。避免频繁跨 store 调用,否则逻辑分散难维护。

再分享个骚操作:动态 store

有时候你需要根据 ID 动态生成 store,比如聊天应用里每个会话独立状态。Zustand 官方文档没细说,但其实可以这样搞:

const createStoreMap = new Map()

function getChatStore(chatId) {
  if (!createStoreMap.has(chatId)) {
    const store = create((set) => ({
      messages: [],
      addMessage: (msg) => set((state) => ({
        messages: [...state.messages, msg]
      })),
    }))
    createStoreMap.set(chatId, store)
  }
  return createStoreMap.get(chatId)
}

// 组件里
function Chat({ chatId }) {
  const { messages, addMessage } = getChatStore(chatId)()
  // ...
}

这里注意内存泄漏问题!如果会话关闭了,记得从 map 里 delete 掉。我加了个 cleanup 函数:

function destroyChatStore(chatId) {
  createStoreMap.delete(chatId)
}

虽然有点 hack,但在复杂场景下真能解决问题。当然,如果项目不大,直接用一个 store 存 { [chatId]: messages } 更简单。

结尾碎碎念

Zustand 真的让我少写了 70% 的样板代码。现在新项目基本默认选它,除非团队强制用 Redux Toolkit。它的核心思想就一点:**状态即函数,简单直接**。不用学一堆概念,上手就能干活。

以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如结合 Immer 写 immutable 更新),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流——毕竟前端方案没有银弹,只有更适合当前项目的解法。

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

暂无评论