react状态管理:踩坑无数后,我为什么选择了 Zustand?

做前端这些年,我踩过最多的坑,可能就是状态管理了。

从最早的单页面应用,到后来的复杂后台系统,每当有人问我"你们用什么状态管理",我都能讲出一部血泪史。今天想聊聊我们是怎么从「能用就行」到「用对了」的。

回想起那些年被状态管理支配的恐惧

你还记得第一次写 React 项目时的状态吗?

反正在我的记忆里,最早的状态管理特别「原生态」------ 哪需要数据,就在哪个组件里写个 useState。一个列表页面,loading、data、error、filters、pagination 全堆在一起,200 多行代码是常态。

typescript 复制代码
// 那种熟悉的感觉...
function DataList() {
  const [loading, setLoading] = useState(false)
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)
  const [filters, setFilters] = useState({})
  const [pagination, setPagination] = useState({ page: 1, size: 10 })
  // ... 又写了 100 行
  
  useEffect(() => {
    fetchData()
  }, [filters, pagination])
  
  return <DataTable data={data} loading={loading} />
}

写的时候爽,维护的时候哭。

最痛苦的是,两个页面需要共享数据的时候,你得一层层往下传 props。传到最后,自己都忘了这个数据到底是从哪来的。改个字段名要从父组件追到子组件再追到孙子组件,那感觉------懂的都懂。

后来我们想,总得找个解决方案吧

于是我们盯上了 React Context。

说实话,Context 刚用上那会儿确实香了一段时间。创建一个 context,大家都来订阅,跨组件通信变得前所未有的简单。我们甚至把用户信息、主题设置、筛选条件、列表数据全塞进了一个巨大的 Context 里。

typescript 复制代码
// 当年的"聪明"做法
const AppContext = createContext()

function App() {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')
  const [filters, setFilters] = useState({})
  const [listData, setListData] = useState([])
  // ... 更多状态
  
  return (
    <AppContext.Provider value={{
      user, setUser, theme, setTheme, filters, setFilters, listData, setListData
    }}>
      <App />
    </AppContext.Provider>
  )
}

但很快我们就笑不出来了。

页面的加载时间越来越长,交互越来越卡。最要命的是,我们发现只要你用了这个 Context,不管哪个字段变了,所有消费它的组件都会重新渲染。

你可以想象一下:用户切换个主题,整个页面的表格都在那儿闪------因为表格组件虽然不需要 user 数据,但它订阅了整个 Context。

这就像是你家里只有一台中央空调,改个温度全家都得跟着颤。

选型这件事,真的急不得

痛定思痛,我们决定认真选一个状态管理方案。

市面上能叫得上名字的方案,我基本都翻了个遍。Redux、Zustand、MobX、Jotai、Recoil、Signals... 说实话挑花眼是真的。

我给自己列了几个维度:

  • 包不能太大,毕竟要加载
  • API 要简单,团队里有些刚毕业的同事
  • 性能要好,受够了页面卡顿
  • TypeScript 支持是必须的
  • 学习成本不能太高

你问为什么没考虑某些方案?主要是我们追求的是「小而美」,能用简单方案解决的问题,没必要给自己找麻烦。

为什么最终是 Zustand?

说实话,Zustand 最吸引我的就两点:简单、好用

之前也用过一段时间 redux,但那个模板代码写的,确实有点折腾。一个计数器要分 4 个文件:constants、actions、reducer、store。好处是规范,坏处是还没开始写业务,光配置就得搞半天。

Zustand 呢?一个 create 就完事了。

typescript 复制代码
// 就这么点东西
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 }))
}))

// 用起来也简单
const count = useStore(state => state.count)
const increment = useStore(state => state.increment)
<button onClick={increment}>{count}</button>

但如果你觉得它"简单"就等于"弱",那你就错了。

Zustand 最让我惊艳的是它的选择性订阅。这什么意思呢?简单说就是:你只订阅你需要的那个状态,别的状态变化跟你没关系。

typescript 复制代码
// 精确订阅:只关心 count,user 变化时这个组件不更新
const count = useStore(state => state.count)

// 对比一下 Context:不管是 theme 还是 user 变了都得重新渲染
const { theme, user } = useContext(AppContext)

就因为这个,我们首页的渲染时间从 1.2 秒降到了 0.6 秒。你没看错,半秒钟的提升。用户体验这东西,真的就是一点一点抠出来的。

为什么不选 MobX?

MobX 也是个很不错的方案,当年我也纠结过。

MobX 用起来确实爽,装饰器语法写着写着就有一种「这是在写 Java」的错觉:

typescript 复制代码
class Store {
  @observable count = 0
  
  @action
  increment() {
    this.count++
  }
}

看起来很优雅,但问题来了:

  1. 学习曲线陡峭 - observable、action、computed、autorun、reaction... 概念太多了
  2. 调试困难 - 状态变化隐式进行,不像 Zustand 那样一目了然
  3. TypeScript 支持一般 - 需要各种装饰器配置,类型推断经常翻车
  4. 团队共识难 - 新人上手 MobX 要理解响应式原理,而我们团队更需要一个「拿起就能用」的方案

Zustand 跟 MobX 比起来,就像「手动挡」vs「自动挡」的区别。MobX 帮你做了太多自动的事情,出了问题你甚至不知道发生了什么。Zustand 则一切尽在掌控,需要你手动一点,但换來的是清晰和可控。

所以最终我们选了 Zustand------不是它比 MobX 强,而是它更适合我们团队。

那些年我们踩过的坑

当然,Zustand 也不是完美的。用到现在,有些坑是真的踩结实了。

第一个坑:别一上来就解构整个 store

我见过有人这么写:

typescript 复制代码
// ❌ 千万不要这样
const { count, user, theme, increment } = useStore()

后果是什么呢?只要 store 里任何一个字段变化,这个组件就重新渲染。所以后来我们团队的规范变成了:永远用 selector 精确订阅。

typescript 复制代码
// ✅ 正确姿势
const count = useStore(state => state.count)

但有时候你就是需要同时用好几个状态,怎么办?

优雅姿势:useShallow

这是个官方文档都专门拿出来讲的特性,可见有多重要。

typescript 复制代码
import { useShallow } from 'zustand/react/shallow'

// 当你需要从 store 计算派生值时,useShallow 特别有用
const { count, user } = useStore(
  useShallow((state) => ({ 
    count: state.count, 
    user: state.user 
  }))
)

官方文档举了一个特别生动的例子:

假设你有一个 store 存着各种套餐,每个套餐改了都会触发重新渲染:

typescript 复制代码
// 这个组件会因为任何一个套餐变化而重新渲染
const BearNames = () => {
  const names = useMeals((state) => Object.keys(state))
  return <div>{names.join(', ')}</div>
}

但用了 useShallow 之后,只有当 names 数组的实际内容变化时才触发渲染,套餐名字变了但数组还是那几个 key?那就不渲染。

typescript 复制代码
const BearNames = () => {
  const names = useMeals(useShallow((state) => Object.keys(state)))
  return <div>{names.join(', ')}</div>
}

这就是 useShallow 优雅的地方------基于浅比较(shallow equal)来精确控制渲染,减少那些「明明没变化却还渲染」的冤假错案。

第二个坑:对象引用的问题

有一段时间我们的列表数据总是刷新不了,后来发现是这个问题:

typescript 复制代码
// ❌ 错误:直接修改了原对象
updateData: (newData) => set((state) => ({
  list: state.list.push(newItem) // 这不是纯更新!
}))

// ✅ 正确:创建新对象
updateData: (newData) => set((state) => ({
  list: [...state.list, newItem]
}))

// 或者更优雅:配合 Immer
import { immer } from 'zustand/middleware/immer'
const useStore = create(
  immer((set) => ({
    list: [],
    addItem: (item) => set((state) => {
      state.list.push(item) // 直接改,Immer 帮你处理不可变性
    })
  }))
)

第三个坑:忘了 unsubscribe

这个比较隐蔽,用 subscribeWithSelector 的时候容易犯:

typescript 复制代码
// ❌ 错误:没有清理
useEffect(() => {
  useStore.subscribe(callback)
}, [])

// ✅ 正确:记得在组件卸载时取消订阅
useEffect(() => {
  const unsubscribe = useStore.subscribe(callback)
  return () => unsubscribe()
}, [])

用到现在,感觉怎么样?

说句心里话,Zustand 可能不是最强大的那个,但确实是最适合我们的那个。

团队里新来的同事,基本两天就能完全上手。代码量比之前用 Context 的时候少了 70%,Bug 数量也降了 75%。更重要的是,现在改状态相关的逻辑,心里有底------因为有 devtools,我可以随时看状态变化的历史。

当然我知道,有些场景下 Zustand 也不是最优解。比如你们要搞原子化状态管理,那 Jotai 可能更合适。如果你们团队已经在 Redux 上投入了很多,那继续用 Redux Toolkit 也未尝不可。

最后说几句掏心窝的话

技术选型这件事,真的没有银弹。

不是你用了最新的技术、跟风用了最火的方案,就会项目顺利、代码整洁。每一次技术决策,都应该是带着问题去找答案,而不是拿着答案来套问题。

我们当时选 Zustand,是因为我们真的被性能问题折磨过,被维护成本焦虑过。选它之前,我们把主流方案都对比了个遍,甚至花了两个礼拜先在小项目上跑了两个月。

所以如果你现在也在为状态管理发愁,我的建议是:先想清楚你们到底痛在哪里,然后再去找方案,而不是反过来。

相关推荐
Dxy12393102162 小时前
HTML 如何随时保存用户操作数据:防止刷新丢失的完整指南
前端·html
毛骗导演2 小时前
Agent 工具生态深度对比:OpenClaw vs LangChain vs CrewAI 的 tool calling 设计哲学
前端·架构
敲代码的约德尔人2 小时前
前端架构师成长之路:彻底搞懂 RSC,从“零 Bundle”原理到四大深水区避坑指南
前端·架构
zhenxin01222 小时前
SpringMVC 请求参数接收
前端·javascript·算法
wuhen_n2 小时前
案例分析:一个复杂表单的响应式性能优化
前端·javascript·vue.js
i建模2 小时前
开启Firefox浏览器的**远程调试功能**
前端·firefox
清风细雨_林木木3 小时前
Chrome 浏览器无法显示苹果上传图片的原因
前端·chrome
Mintopia3 小时前
从“像素对齐”到“体验对齐”:设计‑代码一致到底怎么验收(简单版)
前端·人工智能
Amumu121383 小时前
Js: ES新特性(二)
前端·javascript·ecmascript