Zustand 状态管理库完全指南 - 基础篇

336 阅读36分钟

🐻 Zustand 状态管理库完全指南 - 基础篇

从零开始掌握 React 最简单的状态管理解决方案

📖 前言

在 React 开发中,状态管理一直是一个重要且复杂的话题。从 Redux 的繁琐样板代码,到 Context API 的性能问题,再到各种状态管理库的学习成本,开发者们一直在寻找更好的解决方案。

Zustand 的出现为我们带来了新的选择:它简单、轻量、功能强大,让状态管理变得前所未有的简单。无论你是 React 新手还是经验丰富的开发者,这篇文章都将帮助你完全掌握 Zustand 的使用。

📚 目录

基础篇

  1. 什么是 Zustand?
  2. 为什么选择 Zustand?
  3. 安装和环境准备
  4. 第一个 Store - 快速入门
  5. 核心概念详解
  6. 基础用法实战
  7. 状态更新的多种方式
  8. 选择器和性能优化

1. 什么是 Zustand?

简单介绍

Zustand(德语中的"状态")是一个为 React 应用设计的轻量级状态管理库。它的设计哲学是"保持简单",让开发者能够以最少的代码实现强大的状态管理功能。

想象一下:如果你曾经被 Redux 的复杂性困扰,或者对 Context API 的性能问题感到头疼,那么 Zustand 就像是一股清流,让状态管理回归简单和直观。

什么是状态管理?

在深入了解 Zustand 之前,我们先来理解一下什么是状态管理:

状态(State) 就是应用程序在某个时刻的数据快照。比如:

  • 用户是否已登录
  • 购物车中有哪些商品
  • 当前页面的主题是深色还是浅色
  • 表单中用户输入的内容

状态管理 就是如何在应用中存储、访问和更新这些数据的方法。

为什么需要状态管理?

想象一个简单的场景:你正在开发一个购物网站,用户在商品页面点击"加入购物车",然后导航到购物车页面查看商品。

没有状态管理的问题

  • 每个组件都要自己管理数据
  • 组件之间传递数据非常复杂
  • 数据容易不同步
  • 代码重复且难以维护

有了状态管理

  • 数据集中存储,任何组件都能访问
  • 数据更新自动同步到所有相关组件
  • 代码更简洁,逻辑更清晰

Zustand 的设计理念

Zustand 的作者在设计时遵循了几个核心原则:

  1. 简单至上:API 设计直观,学习成本低
  2. 性能优先:精确的重新渲染控制
  3. TypeScript 友好:完整的类型支持
  4. 灵活性:可以适应各种使用场景
  5. 最小化:不强制任何特定的架构模式

核心特性详解

🚀 超轻量级
  • 压缩后仅 2.5kb:这意味着它不会给你的应用增加太多负担
  • 零依赖:不依赖其他第三方库,减少了潜在的冲突
  • Tree-shaking 友好:只打包你实际使用的功能

为什么轻量级很重要? 在现代 Web 开发中,包体积直接影响应用的加载速度。2.5kb 的大小意味着:

  • 用户下载时间更短
  • 应用启动更快
  • 移动设备上的体验更好
🎯 极简 API 设计
  • 学习成本低:只需要掌握几个核心概念就能上手
  • 样板代码少:相比 Redux,代码量减少 80% 以上
  • 直观易懂:API 设计符合直觉,不需要记忆复杂的概念

什么是样板代码? 样板代码是指那些重复的、必须写但不包含业务逻辑的代码。比如在 Redux 中,你需要写:

  • Action Types 定义
  • Action Creators
  • Reducers
  • Store 配置

而在 Zustand 中,这些都被简化了。

🔧 TypeScript 原生支持
  • 完整的类型推断:自动推断状态和方法的类型
  • 类型安全:编译时就能发现类型错误
  • 智能提示:IDE 能提供完整的代码补全

TypeScript 的好处

// Zustand 会自动推断类型
const useStore = create(set => ({
  count: 0, // TypeScript 知道这是 number
  increment: () => set(state => ({ count: state.count + 1 })),
}))

// 在组件中使用时,IDE 会提供完整的类型提示
function Counter() {
  const count = useStore(state => state.count) // count 的类型是 number
  const increment = useStore(state => state.increment) // increment 的类型是 () => void
}
🌟 无需 Provider 包装
  • 直接使用:不需要在应用顶层包装 Provider 组件
  • 减少嵌套:避免了 Provider 地狱问题
  • 更好的测试:组件测试更加简单

Provider 地狱是什么? 在使用 Context API 或 Redux 时,你可能会看到这样的代码:

// Provider 地狱示例
<AuthProvider>
  <ThemeProvider>
    <UserProvider>
      <CartProvider>
        <App />
      </CartProvider>
    </UserProvider>
  </ThemeProvider>
</AuthProvider>

而 Zustand 不需要这些包装:

// 直接使用,无需 Provider
function App() {
  return <YourComponent />
}
🔄 内置异步支持
  • Promise 友好:原生支持 async/await
  • 错误处理:内置错误状态管理
  • 加载状态:轻松管理加载状态

异步操作示例

const useStore = create(set => ({
  data: null,
  loading: false,
  error: null,

  fetchData: async () => {
    set({ loading: true })
    try {
      const response = await fetch('/api/data')
      const data = await response.json()
      set({ data, loading: false })
    } catch (error) {
      set({ error: error.message, loading: false })
    }
  },
}))
🎨 丰富的中间件生态
  • 官方中间件:devtools、persist、immer 等
  • 社区中间件:满足各种特殊需求
  • 自定义中间件:可以轻松创建自己的中间件

中间件的作用: 中间件可以扩展 Zustand 的功能,比如:

  • persist:自动保存状态到 localStorage
  • devtools:在浏览器开发者工具中调试状态
  • immer:使用 Immer 简化不可变更新

2. 为什么选择 Zustand?

现有状态管理方案的痛点

在深入了解 Zustand 之前,让我们先看看现有状态管理方案的一些问题:

Redux 的问题

1. 样板代码过多 Redux 需要大量的样板代码来实现简单的功能:

// Redux 实现一个简单的计数器需要这些文件:

// types.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

// actions.js
export const increment = () => ({ type: INCREMENT })
export const decrement = () => ({ type: DECREMENT })

// reducer.js
import { INCREMENT, DECREMENT } from './types'

const initialState = { count: 0 }

export const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { ...state, count: state.count + 1 }
    case DECREMENT:
      return { ...state, count: state.count - 1 }
    default:
      return state
  }
}

// store.js
import { createStore } from 'redux'
import { counterReducer } from './reducer'

export const store = createStore(counterReducer)

2. 学习曲线陡峭 新手需要理解的概念:

  • Flux 架构:单向数据流的概念
  • Actions:描述发生了什么的对象
  • Reducers:纯函数,描述如何更新状态
  • Store:保存应用状态的地方
  • Middleware:处理异步操作的中间件
  • Selectors:从 store 中提取数据的函数

3. 配置复杂

// 复杂的 store 配置
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducers'

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
Context API 的问题

1. 性能问题 Context 的一个主要问题是当 context 值改变时,所有消费者都会重新渲染:

// 问题示例
const AppContext = createContext()

function AppProvider({ children }) {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')

  // 当 theme 改变时,所有使用 AppContext 的组件都会重新渲染
  // 即使它们只关心 user 数据
  return <AppContext.Provider value={{ user, setUser, theme, setTheme }}>{children}</AppContext.Provider>
}

2. 嵌套地狱 多个 Context 会导致组件嵌套过深:

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <UserProvider>
          <CartProvider>
            <NotificationProvider>
              <LanguageProvider>
                <YourActualApp />
              </LanguageProvider>
            </NotificationProvider>
          </CartProvider>
        </UserProvider>
      </ThemeProvider>
    </AuthProvider>
  )
}

3. 使用复杂 每个 Context 都需要创建 Provider 和 Consumer:

// 需要为每个 Context 创建这些
const ThemeContext = createContext()

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}
其他状态管理库的问题

1. 学习成本高 每个库都有自己的概念和 API:

  • MobX:需要理解观察者模式、装饰器语法
  • Recoil:需要理解 atoms、selectors、effects
  • Jotai:需要理解原子化状态管理

2. 包体积大 一些状态管理库的包体积对比:

  • Redux + React-Redux + Redux-Toolkit: ~45kb
  • MobX + MobX-React: ~60kb
  • Recoil: ~80kb
  • Zustand: ~2.5kb ✅

3. 生态不完善 一些较新的库可能缺少:

  • 成熟的开发者工具
  • 丰富的中间件生态
  • 完善的文档和教程
  • 社区支持

Zustand 如何解决这些问题

1. 代码对比 - 看看差异有多大

让我们用一个简单的计数器来对比不同方案的代码量:

Redux 方案(需要约 50 行代码):

// actions/counterActions.js
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'

export const increment = () => ({ type: INCREMENT })
export const decrement = () => ({ type: DECREMENT })

// reducers/counterReducer.js
const initialState = { count: 0 }

export const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 }
    case DECREMENT:
      return { count: state.count - 1 }
    default:
      return state
  }
}

// store/index.js
import { createStore } from 'redux'
import { counterReducer } from '../reducers/counterReducer'

export const store = createStore(counterReducer)

// 在组件中使用(还需要 Provider 包装)
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from '../actions/counterActions'

function Counter() {
  const count = useSelector(state => state.count)
  const dispatch = useDispatch()

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

Zustand 方案(只需要约 15 行代码):

import { create } from 'zustand'

// 创建 store - 所有逻辑都在这里
const useCounterStore = create(set => ({
  count: 0, // 初始状态
  increment: () => set(state => ({ count: state.count + 1 })), // 增加方法
  decrement: () => set(state => ({ count: state.count - 1 })), // 减少方法
}))

// 在组件中使用 - 无需 Provider
function Counter() {
  const { count, increment, decrement } = useCounterStore()

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

可以看到,Zustand 的代码量减少了 70% 以上!

2. 性能对比
// Context API - 所有消费者都会重新渲染
const ThemeContext = createContext()

function App() {
  const [theme, setTheme] = useState('light')
  const [user, setUser] = useState(null)

  // 当 theme 改变时,所有使用这个 context 的组件都会重新渲染
  // 即使它们只关心 user 数据
  return (
    <ThemeContext.Provider value={{ theme, setTheme, user, setUser }}>
      <ComponentA />
      <ComponentB />
    </ThemeContext.Provider>
  )
}

// Zustand - 精确的重新渲染控制
const useAppStore = create(set => ({
  theme: 'light',
  user: null,
  setTheme: theme => set({ theme }),
  setUser: user => set({ user }),
}))

function ComponentA() {
  // 只有当 theme 改变时,这个组件才会重新渲染
  const theme = useAppStore(state => state.theme)
  return <div>Theme: {theme}</div>
}

function ComponentB() {
  // 只有当 user 改变时,这个组件才会重新渲染
  const user = useAppStore(state => state.user)
  return <div>User: {user?.name}</div>
}

优势总结

开发效率提升
  • 快速上手:5 分钟就能学会基本用法
  • 代码简洁:一个文件就能完成状态管理
  • 调试方便:内置 DevTools 支持
  • 重构容易:状态结构改变时修改量最小
性能优势明显
  • 精确更新:只有相关组件才会重新渲染
  • 按需加载:可以动态创建和销毁 store
  • 内存友好:自动垃圾回收不用的状态
  • 渲染优化:内置的选择器优化机制
开发体验极佳
  • TypeScript 原生支持:完整的类型推断和检查
  • 热重载友好:开发时状态不会丢失
  • 测试简单:不需要复杂的 mock 和 setup
  • 文档齐全:官方文档清晰易懂
生态系统完善
  • 官方中间件:满足常见需求
  • 社区活跃:问题能快速得到解答
  • 持续更新:定期发布新功能和修复
  • 向后兼容:升级不会破坏现有代码

3. 安装和环境准备

环境要求

在开始使用 Zustand 之前,请确保你的开发环境满足以下要求:

  • React: 16.8.0 或更高版本(需要 Hooks 支持)
  • TypeScript: 4.1.0 或更高版本(如果使用 TypeScript)
  • Node.js: 12.0.0 或更高版本

为什么需要 React 16.8.0+? Zustand 基于 React Hooks 构建,而 Hooks 是在 React 16.8.0 中引入的。如果你的项目使用的是更早的版本,需要先升级 React。

检查你的 React 版本

# 查看当前 React 版本
npm list react

# 或者查看 package.json
cat package.json | grep react

安装 Zustand

Zustand 的安装非常简单,支持所有主流的包管理器:

# 使用 npm 安装
npm install zustand

# 使用 yarn 安装
yarn add zustand

# 使用 pnpm 安装(推荐,速度更快)
pnpm add zustand

# 使用 bun 安装(新兴的快速包管理器)
bun add zustand

安装完成后,你就可以开始使用 Zustand 了! 不需要额外的配置或设置。

验证安装

创建一个简单的测试文件来验证 Zustand 是否正确安装:

// test-zustand.js
import { create } from 'zustand'

console.log('Zustand 安装成功!', typeof create) // 应该输出: Zustand 安装成功! function

项目结构建议

为了更好地组织你的 Zustand 代码,建议采用以下项目结构:

src/
├── stores/           # 存放所有的 store 文件
│   ├── index.ts     # 导出所有 stores
│   ├── userStore.ts # 用户相关状态
│   ├── cartStore.ts # 购物车状态
│   └── themeStore.ts # 主题设置
├── hooks/           # 自定义 hooks
│   └── useLocalStorage.ts
├── types/           # TypeScript 类型定义
│   └── store.ts
└── components/      # React 组件
    └── ...

开发工具设置

1. 安装 Zustand DevTools
# 开发依赖,用于调试
npm install --save-dev @redux-devtools/extension
2. 配置 TypeScript(如果使用)

tsconfig.json 中确保包含以下配置:

{
  "compilerOptions": {
    "strict": true,
    "jsx": "react-jsx",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}
3. VSCode 扩展推荐

为了更好的开发体验,推荐安装以下 VSCode 扩展:

  • ES7+ React/Redux/React-Native snippets:提供 React 代码片段
  • TypeScript Importer:自动导入 TypeScript 类型
  • Bracket Pair Colorizer:彩色括号匹配
  • Auto Rename Tag:自动重命名标签

快速开始模板

创建一个简单的模板来快速开始:

// stores/templateStore.ts
import { create } from 'zustand'

// 定义状态接口
interface TemplateState {
  // 状态属性
  count: number
  name: string
  isLoading: boolean

  // 操作方法
  increment: () => void
  decrement: () => void
  setName: (name: string) => void
  setLoading: (loading: boolean) => void
  reset: () => void
}

// 创建 store
export const useTemplateStore = create<TemplateState>(set => ({
  // 初始状态
  count: 0,
  name: '',
  isLoading: false,

  // 操作方法
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
  setName: name => set({ name }),
  setLoading: isLoading => set({ isLoading }),
  reset: () => set({ count: 0, name: '', isLoading: false }),
}))

常见安装问题

问题 1:TypeScript 类型错误
# 如果遇到 TypeScript 类型错误,安装类型定义
npm install --save-dev @types/react @types/react-dom
问题 2:模块解析错误

如果遇到模块解析错误,检查你的 package.json 中的 type 字段:

{
  "type": "module" // 如果使用 ES modules
}
问题 3:版本冲突

如果遇到版本冲突,可以查看依赖树:

# 查看依赖树
npm ls zustand

# 强制重新安装
npm install --force

下一步

安装完成后,你就可以开始创建你的第一个 Store 了!接下来的章节将详细介绍如何使用 Zustand 的各种功能。

4. 第一个 Store - 快速入门

理解 Store 的概念

在 Zustand 中,Store 就像是一个全局的状态容器,它包含:

  • 状态数据:应用需要共享的数据
  • 操作方法:修改状态的函数
  • 计算属性:基于状态计算出的值

Store 的优势

  • 🌍 全局访问:任何组件都可以访问
  • 🔄 自动更新:状态改变时相关组件自动重新渲染
  • 🎯 类型安全:TypeScript 提供完整的类型检查

创建你的第一个 Store

让我们从一个简单的计数器开始,逐步了解 Zustand 的工作原理:

import { create } from 'zustand'

// 第一步:定义状态的类型(TypeScript 用户)
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,
    }),
}))

代码解释

  • create 函数用于创建一个新的 store
  • set 函数用于更新状态,它接收一个函数或对象
  • state 参数是当前的状态,可以基于它计算新状态
  • 返回的 useCounterStore 是一个 React Hook

在组件中使用 Store

现在让我们创建一个组件来使用这个 store:

import React from 'react'

// 第三步:在组件中使用 store
function Counter() {
  // 从 store 中获取状态和方法
  const { count, increment, decrement, reset } = useCounterStore()

  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h2>Zustand 计数器示例</h2>

      {/* 显示当前计数 */}
      <p style={{ fontSize: '24px', margin: '20px 0' }}>
        当前计数: <strong>{count}</strong>
      </p>

      {/* 操作按钮 */}
      <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
        <button onClick={increment}>➕ 增加</button>
        <button onClick={decrement}>➖ 减少</button>
        <button onClick={reset}>🔄 重置</button>
      </div>
    </div>
  )
}

export default Counter

完整的应用示例

让我们创建一个完整的应用来展示这个计数器:

// App.tsx
import React from 'react'
import Counter from './Counter'

function App() {
  return (
    <div className="App">
      <h1>我的第一个 Zustand 应用</h1>
      <Counter />
    </div>
  )
}

export default App

运行结果

当你运行这个应用时,你会看到:

  • 一个显示当前计数的界面
  • 三个按钮:增加、减少、重置
  • 点击按钮时,计数会立即更新
  • 重要:如果你有多个 Counter 组件,它们会共享同一个状态!

多组件共享状态示例

让我们创建两个组件来展示状态共享:

// CounterDisplay.tsx - 只显示计数
function CounterDisplay() {
  const count = useCounterStore(state => state.count)

  return (
    <div style={{ padding: '10px', border: '1px solid #ccc' }}>
      <h3>计数显示组件</h3>
      <p>当前计数: {count}</p>
    </div>
  )
}

// CounterControls.tsx - 只包含控制按钮
function CounterControls() {
  const { increment, decrement, reset } = useCounterStore()

  return (
    <div style={{ padding: '10px', border: '1px solid #ccc' }}>
      <h3>控制按钮组件</h3>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

// 在 App 中使用
function App() {
  return (
    <div>
      <CounterDisplay />
      <CounterControls />
      {/* 两个组件会自动同步状态! */}
    </div>
  )
}

神奇的地方:当你在 CounterControls 中点击按钮时,CounterDisplay 中的数字会自动更新!这就是 Zustand 状态共享的威力。

小结

通过这个简单的例子,你已经学会了:

  1. ✅ 如何安装和导入 Zustand
  2. ✅ 如何创建一个基本的 store
  3. ✅ 如何在组件中使用 store
  4. ✅ 如何在多个组件间共享状态

接下来,我们将深入了解 Zustand 的核心概念和更高级的用法。

5. 核心概念详解

理解 create 函数

create 函数是 Zustand 的核心,它可以接收一个或两个参数:

import { create } from 'zustand'

// create 函数的完整签名
const useStore = create(
  (set, get, api) => ({
    // 这里返回的对象就是你的 store
    // 包含状态和方法
  }),
  {
    // 可选的配置对象
    name: 'my-store', // store 的名称,用于调试
  }
)

第一个参数 - 状态创建函数

  • set: 用于更新状态的函数
  • get: 用于获取当前状态的函数
  • api: store 的 API 对象,包含 setStategetStatesubscribe 等方法

第二个参数 - 配置对象(可选)

  • name: 字符串,store 的名称,主要用于调试和开发工具中的标识

create 函数的工作原理

  1. 创建阶段create 函数被调用时,它会:

    • 创建一个内部的状态存储
    • 设置状态更新机制
    • 返回一个 React Hook
  2. 使用阶段:当组件调用返回的 Hook 时:

    • 组件会订阅状态变化
    • 当状态更新时,相关组件会重新渲染
    • 组件卸载时,会自动取消订阅

完整示例展示所有参数

// 创建一个完整的 store,展示所有参数的使用
const useAdvancedStore = create(
  (set, get, api) => {
    console.log('Store 正在创建...') // 只会执行一次
    console.log('API 对象:', api) // 包含 setState, getState, subscribe 等

    return {
      // 状态
      count: 0,
      user: { name: 'John', age: 25 },
      history: [],

      // 基础更新(第一个参数)
      increment: () => {
        console.log('当前状态:', get()) // 获取当前状态
        set(
          state => ({ count: state.count + 1 }),
          false, // 不完全替换
          'increment' // 动作名称
        )
      },

      // 合并更新 vs 完全替换(第二个参数)
      updateUserPartial: (name) => {
        set(
          { user: { name } }, // 只更新 name,但会丢失 age!
          false, // 合并更新
          'updateUserPartial'
        )
      },

      updateUserCorrect: (name) => {
        set(
          state => ({ 
            user: { ...state.user, name } // 正确的合并方式
          }),
          false,
          'updateUserCorrect'
        )
      },

      // 完全替换状态(第二个参数为 true)
      resetToDefault: () => {
        set(
          { count: 0, user: { name: '', age: 0 }, history: [] },
          true, // 完全替换整个状态
          'resetToDefault'
        )
      },

      // 复杂的状态更新
      incrementWithHistory: () => {
        const currentState = get()
        set(
          {
            count: currentState.count + 1,
            history: [...currentState.history, currentState.count],
          },
          false,
          'incrementWithHistory'
        )
      },

      // 使用 API 对象的方法
      subscribeToChanges: () => {
        const unsubscribe = api.subscribe(
          (state) => console.log('状态改变:', state)
        )
        return unsubscribe
      },

      // 直接使用 API 的 setState
      setStateDirectly: (newState) => {
        api.setState(newState, false, 'setStateDirectly')
      },
    }
  },
  {
    name: 'advanced-store', // store 名称,用于调试
  }
)

// 在组件中使用
function AdvancedCounter() {
  const { 
    count, 
    user, 
    history, 
    increment, 
    updateUserCorrect, 
    resetToDefault,
    subscribeToChanges 
  } = useAdvancedStore()

  // 订阅状态变化
  useEffect(() => {
    const unsubscribe = subscribeToChanges()
    return unsubscribe // 组件卸载时取消订阅
  }, [])

  return (
    <div>
      <p>计数: {count}</p>
      <p>用户: {user.name} ({user.age}岁)</p>
      <p>历史记录: {history.join(', ')}</p>
      
      <button onClick={increment}>+1 (带历史记录)</button>
      <button onClick={() => updateUserCorrect('张三')}>更新用户名</button>
      <button onClick={resetToDefault}>重置所有状态</button>
    </div>
  )
}

理解 set 函数

set 函数是更新状态的核心工具,它有三个参数:

set(partial, replace, actionName)

参数详解

  1. 第一个参数 partial:新的状态值,可以是对象或函数
  2. 第二个参数 replace:布尔值,是否完全替换状态(默认 false)
  3. 第三个参数 actionName:字符串,动作名称,用于调试和开发工具
1. 第一个参数 - 状态更新值

传入对象(直接更新)

const useStore = create(set => ({
  name: 'John',
  age: 25,

  // 直接设置新值
  setName: name => set({ name }),
  setAge: age => set({ age }),

  // 同时更新多个值
  updateProfile: (name, age) => set({ name, age }),
}))

传入函数(基于当前状态更新)

const useStore = create(set => ({
  count: 0,
  items: [],

  // 基于当前状态计算新状态
  increment: () =>
    set(state => ({
      count: state.count + 1,
    })),

  // 更复杂的状态更新
  addItem: item =>
    set(state => ({
      items: [...state.items, item],
      count: state.items.length + 1,
    })),
}))
2. 第二个参数 - replace 标志
const useStore = create(set => ({
  user: { name: 'John', age: 25, email: 'john@example.com' },
  theme: 'light',

  // 默认行为:合并更新(replace = false)
  updateUserName: name => 
    set({ user: { name } }), // 会保留 age 和 email

  // 部分替换:只替换 user 对象
  replaceUser: newUser => 
    set({ user: newUser }, false), // 默认值,合并更新

  // 完全替换:替换整个状态
  resetStore: () => 
    set({ user: { name: '', age: 0, email: '' } }, true), // 完全替换,theme 会被删除
}))
3. 第三个参数 - actionName 动作名称
const useStore = create(set => ({
  count: 0,
  
  // 带动作名称,便于调试
  increment: () => set(
    state => ({ count: state.count + 1 }),
    false,
    'increment' // 在 DevTools 中显示的动作名称
  ),
  
  decrement: () => set(
    state => ({ count: state.count - 1 }),
    false,
    'decrement'
  ),
  
  reset: () => set(
    { count: 0 },
    false,
    'reset'
  ),
}))

什么时候用哪种方式?

  • 🔄 传入函数:当新状态依赖于当前状态时
  • 📝 传入对象:当你知道确切的新值时
  • 🔄 replace = false:大多数情况下使用,合并更新
  • 🔄 replace = true:需要完全重置状态时使用
  • 🐛 actionName:开发和调试时使用,生产环境可省略

理解 get 函数

get 函数用于在 store 内部获取当前状态:

const useStore = create((set, get) => ({
  count: 0,
  multiplier: 2,

  increment: () => set(state => ({ count: state.count + 1 })),

  // 使用 get 获取当前状态
  getDoubledCount: () => {
    const currentState = get()
    return currentState.count * currentState.multiplier
  },

  // 在复杂逻辑中使用 get
  complexUpdate: () => {
    const { count, multiplier } = get()
    const newCount = count * multiplier

    set({ count: newCount })
  },
}))

状态的不可变性

虽然 Zustand 内部处理了状态的不可变性,但理解这个概念很重要:

const useStore = create(set => ({
  user: { name: 'John', age: 25 },

  // ❌ 错误:直接修改状态
  updateUserWrong: newName =>
    set(state => {
      state.user.name = newName // 这会修改原始对象
      return state
    }),

  // ✅ 正确:创建新对象
  updateUserCorrect: newName =>
    set(state => ({
      user: { ...state.user, name: newName },
    })),
}))

TypeScript 类型定义

为了更好的开发体验,建议为你的 store 定义类型:

// 定义状态类型
interface UserState {
  user: {
    name: string
    age: number
    email: string
  }
  isLoading: boolean
  error: string | null
}

// 定义操作类型
interface UserActions {
  setUser: (user: UserState['user']) => void
  setLoading: (loading: boolean) => void
  setError: (error: string | null) => void
  updateUserName: (name: string) => void
}

// 组合类型
type UserStore = UserState & UserActions

// 创建 store
const useUserStore = create<UserStore>(set => ({
  // 状态
  user: { name: '', age: 0, email: '' },
  isLoading: false,
  error: null,

  // 操作
  setUser: user => set({ user }),
  setLoading: isLoading => set({ isLoading }),
  setError: error => set({ error }),
  updateUserName: name =>
    set(state => ({
      user: { ...state.user, name },
    })),
}))

核心概念总结

通过学习这些核心概念,你已经掌握了 Zustand 的基础知识:

  1. create 函数

    • Zustand 的入口点,用于创建 store
    • 理解其工作原理有助于更好地使用 Zustand
  2. set 函数

    • 状态更新的核心工具
    • 支持对象和函数两种更新方式
    • 选择合适的方式能提高代码质量
  3. get 函数

    • 在 store 内部获取当前状态
    • 用于复杂逻辑和状态依赖计算
  4. TypeScript 集成

    • 完整的类型安全支持
    • 提高开发效率和代码质量

参数使用的最佳实践

  1. create 的第二个参数 name

    • 在开发环境中非常有用,帮助识别不同的 store
    • 在 Redux DevTools 中会显示这个名称
    • 建议为每个 store 都设置一个有意义的名称
  2. set 的第二个参数 replace

    • 99% 的情况下使用默认值 false(合并更新)
    • 只在需要完全重置状态时使用 true
    • 使用 true 时要特别小心,会删除所有未指定的状态
  3. set 的第三个参数 actionName

    • 开发阶段强烈建议使用,便于调试
    • 在 Redux DevTools 中可以看到每个动作的名称
    • 生产环境可以省略以减少包体积
  4. api 对象的使用

    • 提供了底层的 API 访问能力
    • 通常在高级用法中使用,如自定义中间件
    • 包含 setStategetStatesubscribe 等方法

这些概念是使用 Zustand 的基础,理解它们将帮助你更好地设计和实现状态管理逻辑。

6. 基础用法实战

在这个章节中,我们将通过实际的代码示例来学习 Zustand 在不同场景下的使用方法。每个示例都包含完整的代码和详细的解释。

1. 字符串状态管理

字符串是最基本的数据类型,常用于存储用户输入、状态标识等。在实际开发中,我们经常需要管理用户名、邮箱、状态标识等字符串数据。

为什么字符串状态管理很重要?

在 Web 应用中,字符串状态无处不在:

  • 用户信息:姓名、邮箱、手机号等
  • 界面文本:标题、描述、提示信息等
  • 状态标识:loading、success、error 等
  • 表单数据:用户输入的各种文本内容

让我们通过一个完整的用户信息管理示例来学习字符串状态的处理:

// 用户信息管理 - 完整示例
const useUserStore = create(set => ({
  // 状态
  username: '',
  email: '',
  isLoggedIn: false,

  // 操作方法
  setUsername: username => set({ username }),
  setEmail: email => set({ email }),

  // 登录操作
  login: (username, email) =>
    set({
      username,
      email,
      isLoggedIn: true,
    }),

  // 退出登录
  logout: () =>
    set({
      username: '',
      email: '',
      isLoggedIn: false,
    }),
}))

// 在组件中使用
function UserProfile() {
  const { username, email, isLoggedIn, login, logout } = useUserStore()

  const handleLogin = () => {
    login('张三', 'zhangsan@example.com')
  }

  return (
    <div>
      {isLoggedIn ? (
        <div>
          <p>欢迎, {username}!</p>
          <p>邮箱: {email}</p>
          <button onClick={logout}>退出登录</button>
        </div>
      ) : (
        <button onClick={handleLogin}>登录</button>
      )}
    </div>
  )
}

关键知识点解析

  1. 批量更新的优势

    • login 方法同时更新多个状态(usernameemailisLoggedIn
    • 这样只会触发一次重新渲染,而不是三次
    • 提高了性能,避免了中间状态的闪烁
  2. 状态重置模式

    • logout 方法将所有相关状态重置为初始值
    • 这是一个常见的模式,确保用户退出后状态完全清理
    • 避免了数据泄露和状态混乱
  3. 条件渲染策略

    • 基于 isLoggedIn 状态显示不同的 UI
    • 这种模式让组件逻辑更清晰,用户体验更好
    • 可以轻松扩展为更复杂的用户状态管理

实际应用场景

  • 用户登录/注册系统
  • 个人资料编辑页面
  • 用户权限管理
  • 多语言应用的用户偏好设置

2. 数组状态管理

数组是 Web 应用中最常用的数据结构之一,用于存储列表数据。在现代 Web 开发中,我们经常需要处理各种列表:待办事项、用户列表、商品列表、评论列表等。

数组状态管理的挑战

  1. 不可变性要求:React 要求状态更新是不可变的,不能直接修改原数组
  2. 性能考虑:大型数组的频繁更新可能影响性能
  3. 复杂操作:添加、删除、修改、排序等操作需要正确处理
  4. 数据同步:数组数据往往需要与服务器保持同步

Zustand 的数组管理优势

  • 简化了不可变更新的语法
  • 提供了清晰的状态更新模式
  • 支持复杂的数组操作逻辑
  • 易于与异步操作结合

让我们通过一个完整的待办事项管理系统来学习数组状态的最佳实践:

// 待办事项管理
interface Todo {
  id: number
  text: string
  completed: boolean
}

const useTodoStore = create(set => ({
  // 状态
  todos: [] as Todo[],

  // 添加待办事项
  addTodo: (text: string) =>
    set(state => ({
      todos: [
        ...state.todos,
        {
          id: Date.now(), // 简单的 ID 生成
          text,
          completed: false,
        },
      ],
    })),

  // 切换完成状态
  toggleTodo: (id: number) =>
    set(state => ({
      todos: state.todos.map(todo => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)),
    })),

  // 删除待办事项
  removeTodo: (id: number) =>
    set(state => ({
      todos: state.todos.filter(todo => todo.id !== id),
    })),

  // 清空所有已完成的待办事项
  clearCompleted: () =>
    set(state => ({
      todos: state.todos.filter(todo => !todo.completed),
    })),
}))

// 在组件中使用
function TodoList() {
  const { todos, addTodo, toggleTodo, removeTodo, clearCompleted } = useTodoStore()
  const [inputText, setInputText] = useState('')

  const handleAdd = () => {
    if (inputText.trim()) {
      addTodo(inputText.trim())
      setInputText('')
    }
  }

  return (
    <div>
      <div>
        <input value={inputText} onChange={e => setInputText(e.target.value)} placeholder="输入待办事项" />
        <button onClick={handleAdd}>添加</button>
      </div>

      <ul>
        {todos.map(todo => (
          <li
            key={todo.id}
            style={{
              textDecoration: todo.completed ? 'line-through' : 'none',
            }}
          >
            <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} />
            {todo.text}
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>

      <button onClick={clearCompleted}>清空已完成</button>
    </div>
  )
}

数组操作的最佳实践详解

  1. 添加元素的正确方式

    // ✅ 正确:使用展开运算符
    set(state => ({ todos: [...state.todos, newTodo] }))
    
    // ❌ 错误:直接修改原数组
    set(state => { 
      state.todos.push(newTodo) // 这会破坏不可变性
      return state 
    })
    
  2. 删除元素的高效方法

    // ✅ 使用 filter 方法创建新数组
    set(state => ({ 
      todos: state.todos.filter(todo => todo.id !== id) 
    }))
    
  3. 修改元素的标准模式

    // ✅ 使用 map 方法更新特定元素
    set(state => ({
      todos: state.todos.map(todo =>
        todo.id === id 
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    }))
    
  4. 批量操作的优化策略

    // ✅ 一次性处理多个操作
    set(state => ({
      todos: state.todos
        .filter(todo => !todo.completed)  // 删除已完成
        .map(todo => ({ ...todo, priority: 'normal' }))  // 重置优先级
    }))
    

性能优化技巧

  • 对于大型数组,考虑使用虚拟化渲染
  • 避免在渲染过程中进行复杂的数组计算
  • 使用 useMemo 缓存计算结果
  • 考虑分页或懒加载来减少内存使用

3. 复杂状态管理 - 购物车示例

购物车是电商应用中最复杂的状态管理场景之一,它涉及多种数据类型、复杂的业务逻辑和实时计算。通过这个例子,我们将学习如何处理真实世界中的复杂状态管理需求。

购物车系统的复杂性

  1. 多种数据类型:商品信息、数量、价格、折扣等
  2. 复杂的业务逻辑:添加商品、修改数量、应用优惠券等
  3. 实时计算:总价、商品数量、折扣金额等需要实时更新
  4. 用户体验:购物车状态、加载状态、错误处理等
  5. 数据持久化:用户的购物车数据需要保存

我们将要实现的功能

  • ✅ 添加商品到购物车(支持数量累加)
  • ✅ 修改商品数量
  • ✅ 删除商品
  • ✅ 应用和移除优惠券
  • ✅ 实时计算总价和商品数量
  • ✅ 购物车展开/折叠控制
  • ✅ 清空购物车功能

让我们一步步构建这个完整的购物车系统:

// 定义商品接口
interface Product {
  id: number
  name: string
  price: number
  image: string
  category: string
}

// 定义购物车项接口
interface CartItem extends Product {
  quantity: number
}

// 定义购物车状态接口
interface CartState {
  items: CartItem[]
  total: number
  itemCount: number
  isOpen: boolean
  discount: number

  // 购物车操作
  addItem: (product: Product) => void
  removeItem: (productId: number) => void
  updateQuantity: (productId: number, quantity: number) => void
  clearCart: () => void

  // UI 控制
  toggleCart: () => void
  closeCart: () => void

  // 优惠券
  applyDiscount: (percent: number) => void
  removeDiscount: () => void

  // 计算方法
  calculateTotal: () => void
}

// 创建购物车 store
const useCartStore = create<CartState>((set, get) => ({
  // 初始状态
  items: [],
  total: 0,
  itemCount: 0,
  isOpen: false,
  discount: 0,

  // 添加商品到购物车
  addItem: product => {
    const { items, calculateTotal } = get()
    const existingItem = items.find(item => item.id === product.id)

    if (existingItem) {
      // 如果商品已存在,增加数量
      set({
        items: items.map(item => (item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item)),
      })
    } else {
      // 如果商品不存在,添加新商品
      set({
        items: [...items, { ...product, quantity: 1 }],
      })
    }

    // 重新计算总价和数量
    calculateTotal()
  },

  // 从购物车移除商品
  removeItem: productId => {
    const { items, calculateTotal } = get()
    set({
      items: items.filter(item => item.id !== productId),
    })
    calculateTotal()
  },

  // 更新商品数量
  updateQuantity: (productId, quantity) => {
    const { items, calculateTotal } = get()

    if (quantity <= 0) {
      // 如果数量为 0 或负数,移除商品
      set({
        items: items.filter(item => item.id !== productId),
      })
    } else {
      // 更新数量
      set({
        items: items.map(item => (item.id === productId ? { ...item, quantity } : item)),
      })
    }

    calculateTotal()
  },

  // 清空购物车
  clearCart: () =>
    set({
      items: [],
      total: 0,
      itemCount: 0,
      discount: 0,
    }),

  // 切换购物车显示状态
  toggleCart: () => set(state => ({ isOpen: !state.isOpen })),

  // 关闭购物车
  closeCart: () => set({ isOpen: false }),

  // 应用折扣
  applyDiscount: percent => {
    set({ discount: percent })
    get().calculateTotal()
  },

  // 移除折扣
  removeDiscount: () => {
    set({ discount: 0 })
    get().calculateTotal()
  },

  // 计算总价和商品数量
  calculateTotal: () => {
    const { items, discount } = get()

    const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    const total = subtotal * (1 - discount / 100)
    const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)

    set({ total, itemCount })
  },
}))

// 购物车组件
function ShoppingCart() {
  const { items, total, itemCount, isOpen, discount, addItem, removeItem, updateQuantity, clearCart, toggleCart, closeCart, applyDiscount, removeDiscount } =
    useCartStore()

  // 示例商品
  const sampleProduct: Product = {
    id: 1,
    name: 'iPhone 14',
    price: 999,
    image: '/iphone14.jpg',
    category: 'electronics',
  }

  return (
    <div>
      {/* 购物车图标 */}
      <div style={{ position: 'relative', cursor: 'pointer' }} onClick={toggleCart}>
        🛒
        {itemCount > 0 && (
          <span
            style={{
              position: 'absolute',
              top: -8,
              right: -8,
              backgroundColor: 'red',
              color: 'white',
              borderRadius: '50%',
              width: 20,
              height: 20,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              fontSize: 12,
            }}
          >
            {itemCount}
          </span>
        )}
      </div>

      {/* 购物车内容 */}
      {isOpen && (
        <div
          style={{
            position: 'fixed',
            top: 0,
            right: 0,
            width: 400,
            height: '100vh',
            backgroundColor: 'white',
            boxShadow: '-2px 0 10px rgba(0,0,0,0.1)',
            padding: 20,
            overflowY: 'auto',
          }}
        >
          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 20 }}>
            <h2>购物车</h2>
            <button onClick={closeCart}></button>
          </div>

          {/* 商品列表 */}
          {items.length === 0 ? (
            <p>购物车为空</p>
          ) : (
            <div>
              {items.map(item => (
                <div
                  key={item.id}
                  style={{
                    display: 'flex',
                    justifyContent: 'space-between',
                    alignItems: 'center',
                    padding: 10,
                    borderBottom: '1px solid #eee',
                  }}
                >
                  <div>
                    <h4>{item.name}</h4>
                    <p>¥{item.price}</p>
                  </div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                    <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
                    <span>{item.quantity}</span>
                    <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
                    <button onClick={() => removeItem(item.id)}>删除</button>
                  </div>
                </div>
              ))}

              {/* 折扣控制 */}
              <div style={{ margin: '20px 0' }}>
                <h4>优惠券</h4>
                <button onClick={() => applyDiscount(10)}>使用 10% 折扣</button>
                <button onClick={() => applyDiscount(20)}>使用 20% 折扣</button>
                <button onClick={removeDiscount}>移除折扣</button>
                {discount > 0 && <p>当前折扣: {discount}%</p>}
              </div>

              {/* 总计 */}
              <div style={{ marginTop: 20, paddingTop: 20, borderTop: '2px solid #eee' }}>
                <h3>总计: ¥{total.toFixed(2)}</h3>
                <div style={{ display: 'flex', gap: 10, marginTop: 10 }}>
                  <button onClick={clearCart}>清空购物车</button>
                  <button style={{ backgroundColor: '#007bff', color: 'white' }}>结算</button>
                </div>
              </div>
            </div>
          )}

          {/* 测试按钮 */}
          <div style={{ marginTop: 20 }}>
            <button onClick={() => addItem(sampleProduct)}>添加示例商品</button>
          </div>
        </div>
      )}
    </div>
  )
}

这个购物车示例的核心价值

  1. 复杂状态协调

    • 演示了如何管理多个相互关联的状态
    • 展示了状态更新时的数据一致性保证
    • 学习了如何避免状态更新时的竞态条件
  2. 计算属性模式

    • calculateTotal 方法展示了如何实现响应式计算
    • 总价和商品数量会自动根据商品列表和折扣变化
    • 这种模式确保了数据的一致性和准确性
  3. 条件逻辑处理

    • 商品存在性检查避免了重复添加
    • 数量为零时自动删除商品的智能处理
    • 折扣应用的条件判断和边界处理
  4. 用户体验优化

    • 购物车的展开/折叠状态管理
    • 实时的商品数量显示(购物车图标上的红色徽章)
    • 清晰的操作反馈和状态提示
  5. 真实业务逻辑

    • 折扣系统的实现(支持百分比折扣)
    • 购物车数据的结构化管理
    • 可扩展的商品属性设计

实际应用场景

  • 电商网站的购物车功能
  • 在线订餐系统的订单管理
  • 软件许可证的选择和计费
  • 任何需要"添加到集合"功能的应用

可以进一步扩展的功能

  • 商品库存检查
  • 多种优惠券类型(满减、折扣码等)
  • 购物车数据的本地存储
  • 与后端 API 的同步
  • 商品推荐和相关商品建议

4. 对象状态管理

// 用户设置管理
interface UserSettings {
  theme: 'light' | 'dark'
  language: 'zh' | 'en'
  notifications: {
    email: boolean
    push: boolean
    sms: boolean
  }
  preferences: {
    autoSave: boolean
    showTips: boolean
  }
}

const useSettingsStore = create<{
  settings: UserSettings
  updateTheme: (theme: UserSettings['theme']) => void
  updateLanguage: (language: UserSettings['language']) => void
  updateNotifications: (type: keyof UserSettings['notifications'], value: boolean) => void
  updatePreferences: (key: keyof UserSettings['preferences'], value: boolean) => void
  resetSettings: () => void
}>(set => ({
  // 初始状态
  settings: {
    theme: 'light',
    language: 'zh',
    notifications: {
      email: true,
      push: false,
      sms: false,
    },
    preferences: {
      autoSave: true,
      showTips: true,
    },
  },

  // 更新主题
  updateTheme: theme =>
    set(state => ({
      settings: {
        ...state.settings,
        theme,
      },
    })),

  // 更新语言
  updateLanguage: language =>
    set(state => ({
      settings: {
        ...state.settings,
        language,
      },
    })),

  // 更新通知设置
  updateNotifications: (type, value) =>
    set(state => ({
      settings: {
        ...state.settings,
        notifications: {
          ...state.settings.notifications,
          [type]: value,
        },
      },
    })),

  // 更新偏好设置
  updatePreferences: (key, value) =>
    set(state => ({
      settings: {
        ...state.settings,
        preferences: {
          ...state.settings.preferences,
          [key]: value,
        },
      },
    })),

  // 重置所有设置
  resetSettings: () =>
    set({
      settings: {
        theme: 'light',
        language: 'zh',
        notifications: {
          email: true,
          push: false,
          sms: false,
        },
        preferences: {
          autoSave: true,
          showTips: true,
        },
      },
    }),
}))

7. 状态更新的多种方式

在 Zustand 中,状态更新是整个状态管理的核心。理解不同的更新方式及其适用场景,对于构建高性能、用户体验良好的应用至关重要。

为什么状态更新方式很重要?

  1. 性能影响:不同的更新方式会影响组件重新渲染的次数
  2. 用户体验:更新方式决定了 UI 的响应速度和流畅度
  3. 数据一致性:正确的更新方式能避免中间状态和数据不一致
  4. 代码维护性:清晰的更新模式让代码更易理解和维护

让我们深入了解各种状态更新的方式和最佳实践:

1. 立即更新 vs 批量更新

这是状态管理中最基础但也最重要的概念。选择正确的更新方式直接影响应用性能。

const useStore = create(set => ({
  count: 0,
  name: '',

  // 立即更新 - 每次调用 set 都会触发重新渲染
  updateSeparately: (count, name) => {
    set({ count }) // 第一次渲染
    set({ name }) // 第二次渲染
  },

  // 批量更新 - 只触发一次重新渲染
  updateTogether: (count, name) => {
    set({ count, name }) // 只有一次渲染
  },
}))

性能建议:尽量使用批量更新,减少不必要的重新渲染。

状态更新最佳实践总结

  1. 批量更新优先

    • 同时更新多个相关状态,避免多次渲染
    • 提高用户体验,避免 UI 闪烁
  2. 条件更新策略

    • 在更新前检查条件,避免无意义的状态变更
    • 使用函数式更新进行复杂的条件判断
  3. 异步操作模式

    • 统一的 loading/error/data 状态管理
    • 合理的错误处理和用户反馈
  4. 状态设计原则

    • 保持状态结构简单清晰
    • 避免深层嵌套,优先使用扁平化结构
    • 合理拆分状态,避免单个状态过于复杂

通过掌握这些状态更新技巧,你可以构建出响应迅速、用户体验良好的应用。

2. 条件更新

const useStore = create((set, get) => ({
  count: 0,
  max: 10,

  // 带条件的更新
  safeIncrement: () => {
    const { count, max } = get()
    if (count < max) {
      set({ count: count + 1 })
    }
  },

  // 使用函数式更新的条件判断
  conditionalUpdate: () =>
    set(state => {
      if (state.count >= state.max) {
        return state // 返回原状态,不触发更新
      }
      return { count: state.count + 1 }
    }),
}))

3. 复杂状态更新

const useShoppingCartStore = create((set, get) => ({
  items: [],
  total: 0,

  // 添加商品到购物车
  addItem: product =>
    set(state => {
      const existingItem = state.items.find(item => item.id === product.id)

      if (existingItem) {
        // 如果商品已存在,增加数量
        return {
          items: state.items.map(item => (item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item)),
          total: state.total + product.price,
        }
      } else {
        // 如果商品不存在,添加新商品
        return {
          items: [...state.items, { ...product, quantity: 1 }],
          total: state.total + product.price,
        }
      }
    }),

  // 使用 get 函数的复杂更新
  applyDiscount: discountPercent => {
    const currentState = get()
    const discountAmount = currentState.total * (discountPercent / 100)
    const newTotal = currentState.total - discountAmount

    set({
      total: newTotal,
      // 可以添加折扣信息
      discount: {
        percent: discountPercent,
        amount: discountAmount,
      },
    })
  },
}))

4. 异步状态更新

const useAsyncStore = create((set, get) => ({
  data: null,
  loading: false,
  error: null,

  // 异步操作
  fetchData: async url => {
    // 开始加载
    set({ loading: true, error: null })

    try {
      const response = await fetch(url)
      const data = await response.json()

      // 成功获取数据
      set({ data, loading: false })
    } catch (error) {
      // 处理错误
      set({
        error: error.message,
        loading: false,
      })
    }
  },
}))

8. 选择器和性能优化

性能优化是 Zustand 的一大优势。通过合理使用选择器,你可以确保组件只在需要的时候重新渲染,从而提升应用性能。

1. 基础选择器使用

选择器(Selector)是 Zustand 性能优化的核心工具。它是一个函数,用于从 store 中选择特定的状态片段。只有当选择的状态发生变化时,组件才会重新渲染。

选择器的工作原理

  1. 精确订阅:组件只订阅它实际需要的状态部分
  2. 浅比较:Zustand 使用浅比较来检测状态变化
  3. 自动优化:未变化的状态不会触发重新渲染
  4. 内存效率:避免了不必要的计算和渲染

选择器的重要性

  • 🚀 性能提升:减少不必要的重新渲染
  • 🎯 精确控制:只在需要时更新组件
  • 🧹 代码简洁:让组件逻辑更清晰
  • 🔧 易于测试:选择器可以单独测试

让我们通过实际例子来学习选择器的使用:

const useStore = create(set => ({
  user: { name: 'John', age: 25 },
  theme: 'light',
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
}))

function UserComponent() {
  // 只选择需要的状态 - 只有 user 改变时才会重新渲染
  const user = useStore(state => state.user)

  return <div>用户: {user.name}</div>
}

function ThemeComponent() {
  // 只选择 theme - 只有 theme 改变时才会重新渲染
  const theme = useStore(state => state.theme)

  return <div>主题: {theme}</div>
}

2. 多个状态选择

function MultiSelectComponent() {
  // 方法1:分别选择(推荐)
  const user = useStore(state => state.user)
  const theme = useStore(state => state.theme)

  // 方法2:一次选择多个
  const { user, theme } = useStore(state => ({
    user: state.user,
    theme: state.theme,
  }))

  return (
    <div>
      <p>用户: {user.name}</p>
      <p>主题: {theme}</p>
    </div>
  )
}

3. 使用 shallow 比较优化

import { shallow } from 'zustand/shallow'

function OptimizedComponent() {
  // 使用 shallow 比较避免不必要的重新渲染
  const { user, theme } = useStore(
    state => ({ user: state.user, theme: state.theme }),
    shallow, // 浅比较优化
  )

  return (
    <div>
      <p>用户: {user.name}</p>
      <p>主题: {theme}</p>
    </div>
  )
}

4. 选择器函数

// 创建选择器函数
const selectUser = state => state.user
const selectUserName = state => state.user.name
const selectIsLoggedIn = state => !!state.user.name

function UserComponent() {
  const user = useStore(selectUser)
  const userName = useStore(selectUserName)
  const isLoggedIn = useStore(selectIsLoggedIn)

  return <div>{isLoggedIn ? <p>欢迎, {userName}!</p> : <p>请登录</p>}</div>
}

5. 计算属性和派生状态

const useStore = create(set => ({
  items: [],

  // 基础操作
  addItem: item =>
    set(state => ({
      items: [...state.items, item],
    })),
}))

// 创建计算属性选择器
const selectItemCount = state => state.items.length
const selectTotalPrice = state => state.items.reduce((sum, item) => sum + item.price, 0)
const selectExpensiveItems = state => state.items.filter(item => item.price > 100)

function ShoppingCart() {
  const items = useStore(state => state.items)
  const itemCount = useStore(selectItemCount)
  const totalPrice = useStore(selectTotalPrice)
  const expensiveItems = useStore(selectExpensiveItems)

  return (
    <div>
      <p>商品数量: {itemCount}</p>
      <p>总价: ¥{totalPrice}</p>
      <p>贵重商品: {expensiveItems.length} 件</p>
    </div>
  )
}

6. 性能优化最佳实践

// ✅ 好的实践:精确选择
function GoodComponent() {
  const userName = useStore(state => state.user.name)
  const userAge = useStore(state => state.user.age)

  return (
    <div>
      {userName} ({userAge}岁)
    </div>
  )
}

// ❌ 不好的实践:选择整个 store
function BadComponent() {
  const store = useStore() // 任何状态改变都会重新渲染

  return (
    <div>
      {store.user.name} ({store.user.age}岁)
    </div>
  )
}

// ✅ 好的实践:使用 shallow 比较
function GoodMultiSelectComponent() {
  const { name, age } = useStore(
    state => ({
      name: state.user.name,
      age: state.user.age,
    }),
    shallow,
  )

  return (
    <div>
      {name} ({age}岁)
    </div>
  )
}

7. 订阅外部状态变化

// 在组件外部监听状态变化
const useStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
}))

// 订阅状态变化
const unsubscribe = useStore.subscribe(
  state => state.count, // 选择器
  count => {
    // 回调函数
    console.log('计数变化:', count)
  },
)

// 取消订阅
// unsubscribe()

8. 性能优化实战示例

让我们创建一个实际的性能对比示例,展示选择器的重要性:

// 创建一个包含多个状态的 store
const useAppStore = create(set => ({
  // 用户信息
  user: { name: 'John', email: 'john@example.com' },

  // 主题设置
  theme: 'light',

  // 计数器
  count: 0,

  // 通知列表
  notifications: [],

  // 操作方法
  updateUser: user => set({ user }),
  toggleTheme: () =>
    set(state => ({
      theme: state.theme === 'light' ? 'dark' : 'light',
    })),
  increment: () => set(state => ({ count: state.count + 1 })),
  addNotification: notification =>
    set(state => ({
      notifications: [...state.notifications, notification],
    })),
}))

// ❌ 不好的做法:组件会在任何状态变化时重新渲染
function BadComponent() {
  const store = useAppStore() // 获取整个 store

  console.log('BadComponent 重新渲染了')

  return (
    <div>
      <h3>用户名: {store.user.name}</h3>
      <p>当前主题: {store.theme}</p>
    </div>
  )
}

// ✅ 好的做法:只有相关状态变化时才重新渲染
function GoodComponent() {
  const userName = useAppStore(state => state.user.name)
  const theme = useAppStore(state => state.theme)

  console.log('GoodComponent 重新渲染了')

  return (
    <div>
      <h3>用户名: {userName}</h3>
      <p>当前主题: {theme}</p>
    </div>
  )
}

// 测试组件
function PerformanceTest() {
  const { count, increment, addNotification } = useAppStore()

  return (
    <div>
      <h2>性能测试</h2>
      <p>计数: {count}</p>

      <button onClick={increment}>增加计数(观察控制台输出)</button>

      <button onClick={() => addNotification({ id: Date.now(), message: '新通知' })}>添加通知(观察控制台输出)</button>

      <div style={{ display: 'flex', gap: 20 }}>
        <div>
          <h4>❌ 不好的组件</h4>
          <BadComponent />
        </div>

        <div>
          <h4>✅ 好的组件</h4>
          <GoodComponent />
        </div>
      </div>
    </div>
  )
}

性能对比结果

  • 当你点击"增加计数"时,BadComponent 会重新渲染,但 GoodComponent 不会
  • 当你点击"添加通知"时,BadComponent 会重新渲染,但 GoodComponent 不会
  • 只有当用户名或主题变化时,GoodComponent 才会重新渲染

9. 高级性能优化技巧

// 使用 useMemo 优化复杂计算
function ExpensiveComponent() {
  const items = useAppStore(state => state.items)

  // 使用 useMemo 缓存复杂计算结果
  const expensiveCalculation = useMemo(() => {
    console.log('执行复杂计算...')
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  }, [items])

  return <div>总价: ¥{expensiveCalculation}</div>
}

// 使用 useCallback 优化事件处理函数
function OptimizedComponent() {
  const addItem = useAppStore(state => state.addItem)

  // 使用 useCallback 缓存事件处理函数
  const handleAddItem = useCallback(() => {
    addItem({ id: Date.now(), name: '新商品', price: 100 })
  }, [addItem])

  return <button onClick={handleAddItem}>添加商品</button>
}

// 创建自定义 Hook 封装复杂逻辑
function useCartSummary() {
  return useAppStore(state => {
    const items = state.items
    const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)

    return {
      items,
      total,
      itemCount,
      isEmpty: items.length === 0,
    }
  }, shallow)
}

// 使用自定义 Hook
function CartSummary() {
  const { total, itemCount, isEmpty } = useCartSummary()

  if (isEmpty) {
    return <p>购物车为空</p>
  }

  return (
    <div>
      <p>商品数量: {itemCount}</p>
      <p>总价: ¥{total}</p>
    </div>
  )
}

10. 性能监控和调试

// 创建一个带有性能监控的 store
const useMonitoredStore = create((set, get) => ({
  data: [],
  loading: false,

  fetchData: async () => {
    console.time('fetchData')
    set({ loading: true })

    try {
      // 模拟 API 调用
      await new Promise(resolve => setTimeout(resolve, 1000))
      const data = Array.from({ length: 1000 }, (_, i) => ({ id: i, value: Math.random() }))

      set({ data, loading: false })
      console.timeEnd('fetchData')
    } catch (error) {
      set({ loading: false })
      console.error('获取数据失败:', error)
    }
  },
}))

// 性能监控组件
function PerformanceMonitor() {
  const [renderCount, setRenderCount] = useState(0)

  useEffect(() => {
    setRenderCount(prev => prev + 1)
  })

  return (
    <div
      style={{
        position: 'fixed',
        top: 10,
        right: 10,
        background: 'rgba(0,0,0,0.8)',
        color: 'white',
        padding: 10,
      }}
    >
      渲染次数: {renderCount}
    </div>
  )
}

性能优化总结

  1. 精确选择状态:只选择组件需要的状态片段
  2. 使用 shallow 比较:避免对象引用变化导致的重新渲染
  3. 合理使用 useMemo:缓存复杂计算结果
  4. 使用 useCallback:缓存事件处理函数
  5. 创建自定义 Hook:封装复杂的状态逻辑
  6. 监控渲染性能:使用开发者工具检查渲染次数

选择器使用的黄金法则

  • 🎯 精确订阅:组件只订阅它真正需要的状态
  • 🚀 避免过度选择:不要选择整个 store 对象
  • 🔄 合理使用 shallow:对于多个状态的选择,使用 shallow 比较
  • 📊 监控性能:定期检查组件的重新渲染情况
  • 🧹 保持简洁:选择器逻辑应该简单明了

通过掌握这些选择器和性能优化技巧,你可以构建出高性能、响应迅速的 React 应用。记住,性能优化是一个持续的过程,需要在开发过程中不断关注和改进。


📖 结语

恭喜你完成了 Zustand 基础篇的学习!

📚 推荐资源

🤝 交流与反馈

如果你在学习过程中遇到问题,或者有任何建议,欢迎:

  • 在评论区留言讨论
  • 提交 Issue 或 Pull Request
  • 加入相关的技术交流群

如果这篇文章对你有帮助,请点赞收藏支持一下!有问题欢迎在评论区讨论。 🎉