教程 zustand tutorial

切片模式 (Slice Pattern)

Zustand 的切片模式(Slice Pattern)是一种将大型 Store 拆分为多个小型、可管理的 “切片”(Slice)的架构模式。每个切片负责管理 Store 的一部分状态和相关操作,从而提高代码的模块化、可维护性和可测试性。

为什么需要切片模式?

随着应用规模的增长,单一的 Store 会变得越来越复杂,包含大量的状态和操作。这会导致:

  • 代码臃肿:单个文件包含数百行甚至数千行代码
  • 维护困难:难以定位和修改特定功能的代码
  • 团队协作问题:多人同时修改同一文件容易产生冲突
  • 可测试性降低:难以隔离特定功能进行单元测试

切片模式通过将 Store 拆分为多个独立的切片来解决这些问题。

切片模式的基本概念

切片(Slice)

切片是一个函数,它接收 set 和 get 方法作为参数,并返回 Store 的一部分(状态和操作):

javascript
// 定义一个计数器切片
const createCounterSlice = (set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
})

组合切片

通过对象扩展运算符将多个切片组合成一个完整的 Store:

javascript
import { create } from 'zustand'

// 定义切片
const createCounterSlice = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
})

const createUserSlice = (set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
})

// 组合切片创建 Store
const useStore = create((set, get) => ({
  ...createCounterSlice(set),
  ...createUserSlice(set),
}))

切片模式的实现方法

基本实现

javascript
import { create } from 'zustand'

// 用户切片
const createUserSlice = (set) => ({
  user: {
    id: null,
    name: '',
    email: '',
  },
  updateUser: (userData) => set((state) => ({
    user: { ...state.user, ...userData },
  })),
  clearUser: () => set({ user: { id: null, name: '', email: '' } }),
})

// 购物车切片
const createCartSlice = (set, get) => ({
  cart: [],
  addToCart: (product) => set((state) => ({
    cart: [...state.cart, product],
  })),
  removeFromCart: (productId) => set((state) => ({
    cart: state.cart.filter((item) => item.id !== productId),
  })),
  clearCart: () => set({ cart: [] }),
  getCartTotal: () => {
    const cart = get().cart
    return cart.reduce((total, item) => total + item.price, 0)
  },
})

// 主题切片
const createThemeSlice = (set) => ({
  theme: 'light',
  toggleTheme: () => set((state) => ({
    theme: state.theme === 'light' ? 'dark' : 'light',
  })),
  setTheme: (theme) => set({ theme }),
})

// 组合所有切片
const useStore = create((set, get) => ({
  ...createUserSlice(set),
  ...createCartSlice(set, get),
  ...createThemeSlice(set),
}))

// 使用 Store
function App() {
  const { count, increment, user, updateUser, theme, toggleTheme } = useStore()
  
  return (
    <div className={`app ${theme}`}>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
      
      <div>
        <h2>User: {user.name}</h2>
        <button onClick={() => updateUser({ name: 'New Name' })}>Update Name</button>
      </div>
      
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  )
}

处理切片间依赖

当一个切片需要访问或修改另一个切片的状态时,可以使用 get 方法:

javascript
// 用户切片
const createUserSlice = (set) => ({
  user: null,
  isLoggedIn: false,
  login: (userData) => set({
    user: userData,
    isLoggedIn: true,
  }),
  logout: () => set({
    user: null,
    isLoggedIn: false,
  }),
})

// 计数器切片 - 依赖用户切片
const createCounterSlice = (set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  incrementIfLoggedIn: () => {
    // 使用 get() 访问用户切片的状态
    if (get().isLoggedIn) {
      set((state) => ({ count: state.count + 1 }))
    }
  },
})

const useStore = create((set, get) => ({
  ...createUserSlice(set),
  ...createCounterSlice(set, get),
}))

使用 Immer 中间件

结合 immer 中间件可以更方便地处理嵌套状态:

javascript
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

// 用户切片
const createUserSlice = (set) => ({
  user: {
    id: null,
    name: '',
    settings: {
      theme: 'light',
      notifications: true,
    },
  },
  updateUserSettings: (settings) => set((state) => {
    state.user.settings = { ...state.user.settings, ...settings }
  }),
})

// 任务切片
const createTaskSlice = (set) => ({
  tasks: [],
  addTask: (task) => set((state) => {
    state.tasks.push({ id: Date.now(), ...task, completed: false })
  }),
  toggleTask: (taskId) => set((state) => {
    const task = state.tasks.find((t) => t.id === taskId)
    if (task) {
      task.completed = !task.completed
    }
  }),
})

const useStore = create(
  immer((set, get) => ({
    ...createUserSlice(set),
    ...createTaskSlice(set),
  }))
)

切片模式的最佳实践

1. 每个切片有明确的职责

每个切片应该只负责一个特定的功能领域,例如:

  • 用户管理(登录、注册、个人资料)
  • 购物车功能(添加、移除、结算)
  • 主题设置(切换、自定义)
  • 数据缓存(API 响应缓存)

2. 避免切片间的紧密耦合

尽量减少切片之间的直接依赖。如果必须依赖,使用 get() 方法访问其他切片的状态,而不是直接引用。

3. 使用 TypeScript 定义类型

为切片和 Store 定义明确的类型,提高代码的类型安全性:

typescript
import { create } from 'zustand'

// 用户切片类型
interface UserSlice {
  user: {
    id: string | null
    name: string
    email: string
  }
  updateUser: (userData: Partial<UserSlice['user']>) => void
  clearUser: () => void
}

// 计数器切片类型
interface CounterSlice {
  count: number
  increment: () => void
  decrement: () => void
}

// 组合后的 Store 类型
type AppStore = UserSlice & CounterSlice

// 用户切片实现
const createUserSlice = (set): UserSlice => ({
  user: {
    id: null,
    name: '',
    email: '',
  },
  updateUser: (userData) => set((state: AppStore) => ({
    user: { ...state.user, ...userData },
  })),
  clearUser: () => set({ user: { id: null, name: '', email: '' } }),
})

// 计数器切片实现
const createCounterSlice = (set): CounterSlice => ({
  count: 0,
  increment: () => set((state: AppStore) => ({ count: state.count + 1 })),
  decrement: () => set((state: AppStore) => ({ count: state.count - 1 })),
})

// 创建 Store
const useStore = create<AppStore>((set, get) => ({
  ...createUserSlice(set),
  ...createCounterSlice(set),
}))

4. 将切片分离到不同文件

将每个切片放在单独的文件中,便于组织和维护:

代码
stores/
├── index.ts          # 组合所有切片创建 Store
├── userSlice.ts      # 用户切片
├── counterSlice.ts   # 计数器切片
├── cartSlice.ts      # 购物车切片
└── themeSlice.ts     # 主题切片

5. 考虑使用中间件增强切片

可以为切片添加中间件,增强其功能:

javascript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// 用户切片 - 带持久化功能
const createUserSlice = (set) => ({
  user: null,
  login: (userData) => set({ user: userData }),
  logout: () => set({ user: null }),
})

// 计数器切片
const createCounterSlice = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
})

// 创建带持久化的 Store
const useStore = create(
  persist(
    (set, get) => ({
      ...createUserSlice(set),
      ...createCounterSlice(set),
    }),
    {
      name: 'app-storage',
      partialize: (state) => ({ user: state.user }), // 只持久化用户切片
    }
  )
)

高级用法

动态切片加载

在大型应用中,可以根据需要动态加载切片:

javascript
import { create } from 'zustand'

// 核心切片 - 总是加载
const createCoreSlice = (set) => ({
  appName: 'My App',
  version: '1.0.0',
})

// 创建基础 Store
const useStore = create((set, get) => ({
  ...createCoreSlice(set),
}))

// 动态加载用户切片
async function loadUserSlice() {
  const { createUserSlice } = await import('./userSlice')
  
  // 将切片添加到现有 Store
  useStore.setState((state) => ({
    ...state,
    ...createUserSlice(useStore.setState),
  }))
}

// 动态加载购物车切片
async function loadCartSlice() {
  const { createCartSlice } = await import('./cartSlice')
  
  useStore.setState((state) => ({
    ...state,
    ...createCartSlice(useStore.setState, useStore.getState),
  }))
}

切片组合和嵌套

可以创建更复杂的切片结构,包括嵌套切片:

javascript
import { create } from 'zustand'

// 地址切片
const createAddressSlice = (set) => ({
  address: {
    street: '',
    city: '',
    country: '',
    zipCode: '',
  },
  updateAddress: (addressData) => set((state) => ({
    address: { ...state.address, ...addressData },
  })),
})

// 用户切片 - 包含地址切片
const createUserSlice = (set, get) => ({
  user: {
    id: null,
    name: '',
    email: '',
    ...createAddressSlice(set).address, // 嵌套地址切片
  },
  updateUser: (userData) => set((state) => ({
    user: { ...state.user, ...userData },
  })),
  ...createAddressSlice(set), // 复用地址切片的操作
})

const useStore = create((set, get) => ({
  ...createUserSlice(set, get),
}))

切片继承和扩展

可以扩展现有切片,添加新的功能:

javascript
import { create } from 'zustand'

// 基础计数器切片
const createBaseCounterSlice = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
})

// 扩展计数器切片 - 添加高级功能
const createAdvancedCounterSlice = (set, get) => ({
  ...createBaseCounterSlice(set), // 继承基础功能
  incrementBy: (value: number) => set((state) => ({ count: state.count + value })),
  multiply: (factor: number) => set((state) => ({ count: state.count * factor })),
  reset: () => set({ count: 0 }),
})

const useStore = create((set, get) => ({
  ...createAdvancedCounterSlice(set, get),
}))

与其他模式的比较

切片模式 vs 单一 Store

特性 切片模式 单一 Store
代码组织 模块化,易于维护 集中式,复杂应用难以管理
团队协作 便于多人同时开发不同切片 容易产生冲突
可测试性 可隔离切片进行测试 难以隔离特定功能
性能 与单一 Store 相当 与切片模式相当
学习曲线 略高,需要理解切片概念 低,直接使用单一 Store

切片模式 vs Redux Toolkit

Zustand 的切片模式与 Redux Toolkit 的 createSlice 类似,但有一些关键区别:

特性 Zustand 切片模式 Redux Toolkit
学习曲线 低,API 简洁直观 中,需要理解 Redux 概念
样板代码 极少 较少(比传统 Redux 少)
中间件支持 丰富,与 Redux DevTools 兼容 丰富,原生支持 Redux DevTools
类型安全 良好,TypeScript 支持完善 良好,TypeScript 支持完善
灵活性 高,可自由组合切片 中,需要遵循 Redux 架构

常见问题与解决方案

1. 切片之间的命名冲突

问题:不同切片中定义了相同名称的状态或操作

解决方案:

  • 为切片中的状态和操作添加前缀
  • 使用命名空间组织切片
  • 仔细规划切片的职责,避免重叠
javascript
// 用户切片 - 添加前缀
const createUserSlice = (set) => ({
  userData: null, // 前缀 user
  userLogin: (user) => set({ userData: user }),
  userLogout: () => set({ userData: null }),
})

// 管理员切片 - 添加前缀
const createAdminSlice = (set) => ({
  adminData: null, // 前缀 admin
  adminLogin: (admin) => set({ adminData: admin }),
  adminLogout: () => set({ adminData: null }),
})

2. 切片间依赖导致的循环引用

问题:切片 A 依赖切片 B,切片 B 又依赖切片 A

解决方案:

  • 重构切片,提取公共依赖到新的切片
  • 使用事件或回调机制替代直接依赖
  • 重新设计状态结构,避免循环依赖

3. 动态切片加载时的类型问题

问题:TypeScript 无法识别动态加载的切片类型

解决方案:

  • 使用类型断言
  • 定义完整的 Store 类型,包含所有可能的切片
  • 使用模块增强扩展 Store 类型
typescript
import { create } from 'zustand'

// 基础 Store 类型
interface BaseStore {
  appName: string
  version: string
}

// 完整 Store 类型(包含所有可能的切片)
type AppStore = BaseStore & {
  user?: UserSlice
  cart?: CartSlice
}

// 创建 Store
const useStore = create<AppStore>((set) => ({
  appName: 'My App',
  version: '1.0.0',
}))

// 动态加载切片时使用类型断言
async function loadUserSlice() {
  const { createUserSlice } = await import('./userSlice')
  
  useStore.setState((state) => ({
    ...state,
    ...createUserSlice(useStore.setState),
  }))
}

总结

Zustand 的切片模式是一种强大的架构模式,可以帮助你组织大型应用的状态管理:

  1. 提高代码组织性:将 Store 拆分为多个小型、可管理的切片
  2. 增强可维护性:便于定位和修改特定功能的代码
  3. 促进团队协作:不同团队成员可以负责不同的切片
  4. 提高可测试性:可以隔离切片进行单元测试
  5. 支持代码复用:切片可以在不同应用中复用

通过遵循切片模式的最佳实践,你可以构建出更具扩展性和可维护性的 Zustand 应用。

在接下来的章节中,我们将学习 单元测试与 Mock,了解如何测试你的 Zustand Store 和切片。