一八四、Zustand 状态管理详解、与 Redux、MobX 的对比分析

Zustand 状态管理详解

本文档详细介绍 Zustand 的核心概念、使用方法,以及与 Redux、MobX 的对比分析。

一、Zustand 简介

1.1 什么是 Zustand

Zustand(德语中意为"状态")是一个小巧、快速、可扩展的 React 状态管理库,由 Poimandres(React Three Fiber 团队)开发维护。

核心特点:

  • 📦 极致轻量 - 仅 ~1KB(gzip),无外部依赖
  • 🎯 简洁 API - 没有样板代码,没有 Provider 包裹
  • 高性能 - 基于 selector 的精准渲染优化
  • 🔧 灵活扩展 - 丰富的中间件生态

1.2 安装

bash 复制代码
# npm
npm install zustand

# yarn
yarn add zustand

# pnpm
pnpm add zustand

二、核心概念与基础用法

2.1 创建 Store

Zustand 使用 create 函数创建 store,返回一个 React Hook:

typescript 复制代码
import { create } from 'zustand'

// 定义状态类型
interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

// 创建 store
const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}))

2.2 在组件中使用

tsx 复制代码
function Counter() {
  // 方式1:获取整个状态(不推荐,会导致不必要的重渲染)
  const { count, increment, decrement } = useCounterStore()
  
  // 方式2:使用 selector 精确订阅(推荐)
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

2.3 set 和 get 函数

typescript 复制代码
const useStore = create((set, get) => ({
  count: 0,
  
  // set: 更新状态
  increment: () => set((state) => ({ count: state.count + 1 })),
  
  // set 的第二个参数 replace(默认 false 会合并状态,true 会替换整个状态)
  resetAll: () => set({ count: 0 }, true),
  
  // get: 获取当前状态(用于 action 中读取最新状态)
  doubleCount: () => {
    const current = get().count
    set({ count: current * 2 })
  },
}))

2.4 异步 Actions

Zustand 天然支持异步操作,无需额外配置:

typescript 复制代码
const useUserStore = create((set) => ({
  user: null,
  loading: false,
  error: null,
  
  fetchUser: async (id: string) => {
    set({ loading: true, error: null })
    try {
      const response = await fetch(`/api/users/${id}`)
      const user = await response.json()
      set({ user, loading: false })
    } catch (error) {
      set({ error: error.message, loading: false })
    }
  },
}))

三、进阶用法

3.1 在 React 外部访问 Store

typescript 复制代码
// 获取当前状态
const state = useStore.getState()

// 订阅状态变化
const unsubscribe = useStore.subscribe(
  (state) => console.log('State changed:', state)
)

// 在组件外部更新状态
useStore.setState({ count: 10 })

// 销毁 store
useStore.destroy()

3.2 Selector 优化

typescript 复制代码
// ❌ 不推荐:每次都创建新的对象引用
const { count, user } = useStore((state) => ({ 
  count: state.count, 
  user: state.user 
}))

// ✅ 推荐方式1:分开订阅
const count = useStore((state) => state.count)
const user = useStore((state) => state.user)

// ✅ 推荐方式2:使用 useShallow 浅比较
import { useShallow } from 'zustand/react/shallow'

const { count, user } = useStore(
  useShallow((state) => ({ count: state.count, user: state.user }))
)

3.3 Store 切片(Slices Pattern)

大型应用可以将 store 拆分为多个切片:

typescript 复制代码
// slices/userSlice.ts
export const createUserSlice = (set, get) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
})

// slices/cartSlice.ts
export const createCartSlice = (set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ 
    items: [...state.items, item] 
  })),
})

// store.ts - 合并切片
const useStore = create((...args) => ({
  ...createUserSlice(...args),
  ...createCartSlice(...args),
}))

四、中间件生态

4.1 Immer 中间件(不可变数据更新)

typescript 复制代码
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

const useStore = create(
  immer((set) => ({
    todos: [],
    addTodo: (text) =>
      set((state) => {
        // 可以直接修改 state,Immer 会处理不可变性
        state.todos.push({ id: Date.now(), text, done: false })
      }),
    toggleTodo: (id) =>
      set((state) => {
        const todo = state.todos.find((t) => t.id === id)
        if (todo) todo.done = !todo.done
      }),
  }))
)

4.2 Persist 中间件(状态持久化)

typescript 复制代码
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

const useStore = create(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'app-storage', // localStorage key
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({ theme: state.theme }), // 只持久化部分状态
    }
  )
)

4.3 Devtools 中间件(Redux DevTools 集成)

typescript 复制代码
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const useStore = create(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set(
        (state) => ({ count: state.count + 1 }),
        false, // replace
        'increment' // action name(显示在 DevTools 中)
      ),
    }),
    { name: 'MyStore' }
  )
)

4.4 中间件组合

typescript 复制代码
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

const useStore = create(
  devtools(
    persist(
      immer((set) => ({
        // ... store 定义
      })),
      { name: 'storage-key' }
    ),
    { name: 'DevTools-Name' }
  )
)

五、项目实战示例

以下是本项目(低代码 H5 编辑器)中的 Zustand 使用示例:

typescript 复制代码
// store/builder.ts
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

export const useBuilderStore = create<BuilderState & BuilderActions>()(
  immer((set, get) => ({
    // 状态
    currentPage: createInitialPage(),
    selectedComponentIds: [],
    hoveredComponentId: null,
    previewMode: false,
    history: [],
    historyIndex: -1,
    clipboard: null,
    zoom: 100,
    deviceMode: 'mobile',

    // 添加组件 - 利用 Immer 直接修改嵌套状态
    addComponent: (type, parentId, index) => {
      const meta = registry.getComponent(type)
      if (!meta) return null

      const newComponent = {
        id: nanoid(),
        type,
        props: { ...meta.defaultProps },
        // ...
      }

      set((state) => {
        if (parentId) {
          const parent = findComponentInTree(state.currentPage.components, parentId)
          if (parent?.children) {
            parent.children.splice(index ?? parent.children.length, 0, newComponent)
          }
        } else {
          state.currentPage.components.splice(
            index ?? state.currentPage.components.length, 
            0, 
            newComponent
          )
        }
        state.selectedComponentIds = [newComponent.id]
      })

      return newComponent.id
    },

    // 更新组件属性
    updateComponentProps: (id, props) => {
      set((state) => {
        const component = findComponentInTree(state.currentPage.components, id)
        if (component) {
          component.props = { ...component.props, ...props }
        }
      })
    },

    // 使用 get() 在 action 中获取最新状态
    findComponentById: (id) => {
      return findComponentInTree(get().currentPage.components, id)
    },
  }))
)

六、与 Redux、MobX 对比

6.1 核心对比表

特性 Zustand Redux MobX
包体积 ~1KB ~7KB (+ toolkit ~11KB) ~16KB
样板代码 极少 较多(需要 actions, reducers, selectors) 较少
学习曲线 中高
Provider 要求 ❌ 不需要 ✅ 必需 ✅ 必需
TypeScript 支持 原生支持 需要额外配置 需要装饰器支持
异步处理 内置支持 需要 thunk/saga 内置支持
DevTools 通过中间件支持 原生支持 通过扩展支持
状态可变性 不可变(可配合 Immer) 强制不可变 可变(响应式)
渲染优化 基于 selector 需要手动 memo/selector 自动追踪

6.2 代码对比

计数器示例

Redux Toolkit

typescript 复制代码
// counterSlice.ts
import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1 },
    decrement: (state) => { state.value -= 1 },
  },
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer

// store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: { counter: counterReducer },
})

// App.tsx - 需要 Provider
import { Provider } from 'react-redux'
import { store } from './store'

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  )
}

// Counter.tsx
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './counterSlice'

function Counter() {
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
  
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

MobX

typescript 复制代码
// counterStore.ts
import { makeAutoObservable } from 'mobx'

class CounterStore {
  value = 0

  constructor() {
    makeAutoObservable(this)
  }

  increment() {
    this.value += 1
  }

  decrement() {
    this.value -= 1
  }
}

export const counterStore = new CounterStore()

// App.tsx - 需要 Provider(或直接导入)
import { createContext, useContext } from 'react'
import { counterStore } from './counterStore'

const StoreContext = createContext(counterStore)

function App() {
  return (
    <StoreContext.Provider value={counterStore}>
      <Counter />
    </StoreContext.Provider>
  )
}

// Counter.tsx
import { observer } from 'mobx-react-lite'
import { useContext } from 'react'

const Counter = observer(() => {
  const store = useContext(StoreContext)
  
  return (
    <div>
      <span>{store.value}</span>
      <button onClick={() => store.increment()}>+</button>
      <button onClick={() => store.decrement()}>-</button>
    </div>
  )
})

Zustand

typescript 复制代码
// counterStore.ts
import { create } from 'zustand'

const useCounterStore = create((set) => ({
  value: 0,
  increment: () => set((state) => ({ value: state.value + 1 })),
  decrement: () => set((state) => ({ value: state.value - 1 })),
}))

// Counter.tsx - 无需 Provider!
function Counter() {
  const value = useCounterStore((state) => state.value)
  const increment = useCounterStore((state) => state.increment)
  const decrement = useCounterStore((state) => state.decrement)
  
  return (
    <div>
      <span>{value}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

6.3 Zustand 相比 Redux 的优势

优势 说明
零样板代码 无需定义 action types、action creators、reducers,代码量减少 60-70%
无 Provider 地狱 不需要在根组件包裹 Provider,可以直接在任何地方使用
更好的代码组织 状态和操作可以放在一起,更符合模块化思维
更简单的异步 无需 redux-thunk、redux-saga 等额外库
更小的包体积 Zustand ~1KB vs Redux Toolkit ~18KB
原生 TypeScript 类型推断更自然,无需复杂的泛型配置
更灵活的 selector 内置的 selector 机制,自动进行渲染优化

6.4 Zustand 相比 MobX 的优势

优势 说明
更轻量 ~1KB vs ~16KB
无魔法 没有装饰器、Proxy 等"黑盒"机制,行为可预测
更好的 React 集成 基于 hooks 设计,与现代 React 模式一致
无需 observer 包裹 组件不需要用 HOC 包裹
更简单的心智模型 不可变数据更新(配合 Immer),与 React 思维一致
更容易调试 支持 Redux DevTools,状态变化清晰可追溯
SSR 友好 服务端渲染场景更简单

七、为什么越来越多项目选择 Zustand

7.1 开发体验革命

复制代码
Redux 时代的痛点:
├── 文件散落(actions/reducers/selectors/types)
├── 大量样板代码
├── 异步处理复杂
├── TypeScript 配置繁琐
└── 学习曲线陡峭

Zustand 的解决方案:
├── 单文件定义状态和操作
├── 几乎零样板代码
├── 原生支持异步
├── 开箱即用的 TypeScript
└── 10分钟上手

7.2 性能与体积

  • 首屏加载:Zustand 仅 1KB,对于移动端 H5 尤其重要
  • 运行时性能:基于 selector 的精准更新,避免不必要渲染
  • 内存占用:更少的中间层和抽象

7.3 现代 React 的最佳搭档

  • 完全拥抱 Hooks API
  • 与 React 18 Concurrent Mode 兼容
  • 支持 React Server Components

7.4 生态成熟

  • 官方中间件:persist、devtools、immer、subscribeWithSelector
  • 社区插件:zustand-lens、zustand-querystring、zustand-computed 等
  • 良好维护:由 Poimandres 团队持续维护,React Three Fiber 的同一团队

7.5 大厂和知名项目采用

  • Vercel - Next.js 团队在示例中推荐
  • Jotai/Valtio - 同一团队的姊妹项目,生态互通
  • 开源项目:Excalidraw、Cal.com、Remotion 等

7.6 适用场景

Zustand 特别适合:

✅ 中小型应用的全局状态管理

✅ 微前端场景(无 Provider 依赖)

✅ 需要跨组件共享状态的场景

✅ 追求极致性能和包体积的项目

✅ TypeScript 项目

✅ React Native 应用


八、最佳实践

8.1 文件组织建议

复制代码
src/
├── store/
│   ├── index.ts          # 统一导出
│   ├── useUserStore.ts   # 用户相关状态
│   ├── useCartStore.ts   # 购物车状态
│   └── useUIStore.ts     # UI 状态

8.2 命名规范

  • Store Hook 以 use 开头:useUserStoreuseCartStore
  • Actions 使用动词:setUserfetchDatatoggleModal

8.3 性能优化清单

  • 使用 selector 精确订阅所需状态
  • 避免在 selector 中返回新对象
  • 使用 useShallow 订阅多个原始值
  • 大型嵌套状态考虑使用 Immer 中间件
  • 频繁更新的状态考虑拆分为独立 store

8.4 TypeScript 最佳实践

typescript 复制代码
// 明确定义状态和操作的类型
interface State {
  count: number
}

interface Actions {
  increment: () => void
  setCount: (count: number) => void
}

// 组合类型
type Store = State & Actions

// 创建 store
const useStore = create<Store>()((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
  setCount: (count) => set({ count }),
}))

九、迁移指南

从 Redux 迁移

  1. 将 reducer 逻辑转换为 Zustand actions
  2. 移除 Provider 包裹
  3. useSelector + useDispatch 替换为直接使用 store hook
  4. 逐步迁移,可以两者共存

从 MobX 迁移

  1. 将 class store 转换为 Zustand 函数式 store
  2. 移除 observer HOC 包裹
  3. 将可变更新改为不可变更新(或使用 Immer)

十、总结

Zustand 代表了 React 状态管理的新趋势:更少的样板、更好的开发体验、更小的包体积

它不是要完全取代 Redux(Redux 在大型团队协作、复杂业务流程方面仍有优势),而是为大多数日常项目提供了一个更轻量、更实用的选择。

"The best code is no code at all." --- Zustand 的设计理念正是如此。


参考资源

相关推荐
wangmengxxw2 小时前
设计模式 -详解
开发语言·javascript·设计模式
Code小翊2 小时前
TypeScript 核心语法速查
前端·javascript·typescript
家里有只小肥猫2 小时前
uniApp下拉渐变头部 拿来即用
前端·javascript·uni-app
一起养小猫2 小时前
Flutter for OpenHarmony 实战:科学计算器完整开发指南
android·前端·flutter·游戏·harmonyos
Jinuss2 小时前
源码分析之React中Scheduler调度器的任务优先级
前端·react.js·前端框架
波波0072 小时前
每日一题:在 .NET 中遍历集合(如 List<T>、数组、字典)的过程中进行增删改查会不会有影响?可能引发哪些问题?实际开发中应如何避免?
前端·list
念念不忘 必有回响2 小时前
码云流水线前端资源传输至目标服务器
运维·服务器·前端
我是伪码农2 小时前
Vue 2.2
前端·javascript·vue.js
●VON2 小时前
React Native for OpenHarmony:深入剖析 Switch 组件的状态绑定、无障碍与样式定制
javascript·学习·react native·react.js·von