Store 切片模式 #

什么是切片模式? #

切片模式是将大型 Store 拆分成多个独立、可复用模块的设计模式。每个切片负责一个特定领域的状态和逻辑。

为什么需要切片模式? #

text
大型应用状态管理挑战
├── Store 过大 ────── 难以维护和理解
├── 逻辑耦合 ─────── 不同领域混杂
├── 复用困难 ─────── 无法独立使用
└── 协作冲突 ─────── 多人开发困难

基本切片模式 #

定义切片 #

tsx
import { StateCreator } from 'zustand'

// 用户切片
interface UserSlice {
  user: {
    id: string
    name: string
    email: string
  } | null
  setUser: (user: UserSlice['user']) => void
  clearUser: () => void
}

const createUserSlice: StateCreator<UserSlice> = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
  clearUser: () => set({ user: null }),
})

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

const createCounterSlice: StateCreator<CounterSlice> = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
})

组合切片 #

tsx
import { create } from 'zustand'

type StoreState = UserSlice & CounterSlice

const useStore = create<StoreState>()((...a) => ({
  ...createUserSlice(...a),
  ...createCounterSlice(...a),
}))

完整的切片模式实现 #

切片类型定义 #

tsx
// types/slices.ts
import { StateCreator } from 'zustand'

export type SliceCreator<T> = StateCreator<T, [], [], T>

export interface SliceConfig<T> {
  name: string
  create: SliceCreator<T>
  dependencies?: string[]
}

用户切片 #

tsx
// slices/userSlice.ts
import { SliceCreator } from '../types/slices'

export interface User {
  id: string
  name: string
  email: string
  avatar?: string
}

export interface UserSlice {
  user: User | null
  isAuthenticated: boolean
  isLoading: boolean
  error: string | null
  
  // Actions
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  updateUser: (data: Partial<User>) => void
  clearError: () => void
}

export const createUserSlice: SliceCreator<UserSlice> = (set, get) => ({
  user: null,
  isAuthenticated: false,
  isLoading: false,
  error: null,
  
  login: async (email, password) => {
    set({ isLoading: true, error: null })
    
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      })
      
      if (!response.ok) {
        throw new Error('登录失败')
      }
      
      const user = await response.json()
      set({ user, isAuthenticated: true, isLoading: false })
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : '登录失败',
        isLoading: false,
      })
    }
  },
  
  logout: () => {
    set({ user: null, isAuthenticated: false, error: null })
  },
  
  updateUser: (data) => {
    const { user } = get()
    if (user) {
      set({ user: { ...user, ...data } })
    }
  },
  
  clearError: () => set({ error: null }),
})

购物车切片 #

tsx
// slices/cartSlice.ts
import { SliceCreator } from '../types/slices'

export interface CartItem {
  productId: string
  name: string
  price: number
  quantity: number
  image: string
}

export interface CartSlice {
  items: CartItem[]
  totalItems: number
  totalPrice: number
  
  // Actions
  addItem: (item: Omit<CartItem, 'quantity'>) => void
  removeItem: (productId: string) => void
  updateQuantity: (productId: string, quantity: number) => void
  clearCart: () => void
}

export const createCartSlice: SliceCreator<CartSlice> = (set, get) => ({
  items: [],
  totalItems: 0,
  totalPrice: 0,
  
  addItem: (item) => set((state) => {
    const existingItem = state.items.find(i => i.productId === item.productId)
    
    let newItems: CartItem[]
    if (existingItem) {
      newItems = state.items.map(i =>
        i.productId === item.productId
          ? { ...i, quantity: i.quantity + 1 }
          : i
      )
    } else {
      newItems = [...state.items, { ...item, quantity: 1 }]
    }
    
    return {
      items: newItems,
      totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
      totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
    }
  }),
  
  removeItem: (productId) => set((state) => {
    const newItems = state.items.filter(i => i.productId !== productId)
    return {
      items: newItems,
      totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
      totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
    }
  }),
  
  updateQuantity: (productId, quantity) => {
    if (quantity <= 0) {
      return get().removeItem(productId)
    }
    
    set((state) => {
      const newItems = state.items.map(i =>
        i.productId === productId ? { ...i, quantity } : i
      )
      
      return {
        items: newItems,
        totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
        totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
      }
    })
  },
  
  clearCart: () => set({
    items: [],
    totalItems: 0,
    totalPrice: 0,
  }),
})

主题切片 #

tsx
// slices/themeSlice.ts
import { SliceCreator } from '../types/slices'

export type Theme = 'light' | 'dark' | 'system'

export interface ThemeSlice {
  theme: Theme
  resolvedTheme: 'light' | 'dark'
  
  // Actions
  setTheme: (theme: Theme) => void
  toggleTheme: () => void
}

const getSystemTheme = (): 'light' | 'dark' => {
  if (typeof window === 'undefined') return 'light'
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}

const resolveTheme = (theme: Theme): 'light' | 'dark' => {
  return theme === 'system' ? getSystemTheme() : theme
}

export const createThemeSlice: SliceCreator<ThemeSlice> = (set, get) => ({
  theme: 'system',
  resolvedTheme: resolveTheme('system'),
  
  setTheme: (theme) => set({
    theme,
    resolvedTheme: resolveTheme(theme),
  }),
  
  toggleTheme: () => {
    const { resolvedTheme } = get()
    const newTheme = resolvedTheme === 'light' ? 'dark' : 'light'
    set({ theme: newTheme, resolvedTheme: newTheme })
  },
})

组合所有切片 #

tsx
// store/index.ts
import { create } from 'zustand'
import { devtools, persist, createJSONStorage } from 'zustand/middleware'
import { createUserSlice, UserSlice } from './slices/userSlice'
import { createCartSlice, CartSlice } from './slices/cartSlice'
import { createThemeSlice, ThemeSlice } from './slices/themeSlice'

type StoreState = UserSlice & CartSlice & ThemeSlice

export const useStore = create<StoreState>()(
  devtools(
    persist(
      (...a) => ({
        ...createUserSlice(...a),
        ...createCartSlice(...a),
        ...createThemeSlice(...a),
      }),
      {
        name: 'app-storage',
        storage: createJSONStorage(() => localStorage),
        partialize: (state) => ({
          theme: state.theme,
          items: state.items,
        }),
      }
    ),
    { name: 'AppStore' }
  )
)

切片间通信 #

通过 get 函数访问其他切片 #

tsx
// slices/orderSlice.ts
import { SliceCreator } from '../types/slices'
import { CartItem } from './cartSlice'

export interface OrderSlice {
  orders: Order[]
  createOrder: () => Promise<void>
}

export const createOrderSlice: SliceCreator<OrderSlice & CartSlice> = (set, get) => ({
  orders: [],
  
  createOrder: async () => {
    // 访问购物车切片的状态
    const { items, totalPrice, clearCart } = get()
    
    if (items.length === 0) {
      throw new Error('购物车为空')
    }
    
    const response = await fetch('/api/orders', {
      method: 'POST',
      body: JSON.stringify({ items, totalPrice }),
    })
    
    const order = await response.json()
    
    // 更新订单切片
    set((state) => ({
      orders: [...state.orders, order],
    }))
    
    // 清空购物车
    clearCart()
  },
})

使用中间件共享状态 #

tsx
// slices/notificationSlice.ts
import { SliceCreator } from '../types/slices'

export interface Notification {
  id: string
  type: 'success' | 'error' | 'warning' | 'info'
  message: string
}

export interface NotificationSlice {
  notifications: Notification[]
  
  addNotification: (notification: Omit<Notification, 'id'>) => void
  removeNotification: (id: string) => void
  clearNotifications: () => void
}

export const createNotificationSlice: SliceCreator<NotificationSlice> = (set) => ({
  notifications: [],
  
  addNotification: (notification) => set((state) => ({
    notifications: [
      ...state.notifications,
      { ...notification, id: Date.now().toString() },
    ],
  })),
  
  removeNotification: (id) => set((state) => ({
    notifications: state.notifications.filter(n => n.id !== id),
  })),
  
  clearNotifications: () => set({ notifications: [] }),
}))

// 在其他切片中使用通知
export const createUserSliceWithNotifications: SliceCreator<UserSlice & NotificationSlice> = 
  (set, get) => ({
    // ... 用户切片逻辑
    
    login: async (email, password) => {
      set({ isLoading: true, error: null })
      
      try {
        const response = await fetch('/api/auth/login', {
          method: 'POST',
          body: JSON.stringify({ email, password }),
        })
        
        if (!response.ok) throw new Error('登录失败')
        
        const user = await response.json()
        set({ user, isAuthenticated: true, isLoading: false })
        
        // 显示成功通知
        get().addNotification({
          type: 'success',
          message: '登录成功',
        })
      } catch (error) {
        const message = error instanceof Error ? error.message : '登录失败'
        set({ error: message, isLoading: false })
        
        // 显示错误通知
        get().addNotification({
          type: 'error',
          message,
        })
      }
    },
  })

动态切片 #

按需加载切片 #

tsx
// store/dynamicStore.ts
import { create, StoreApi, UseBoundStore } from 'zustand'

interface DynamicSlice {
  loaded: boolean
  loadSlice: <T extends object>(name: string, slice: StateCreator<T>) => void
}

let store: UseBoundStore<StoreApi<any>>

export const getStore = () => {
  if (!store) {
    store = create<DynamicSlice>((set, get) => ({
      loaded: false,
      
      loadSlice: (name, slice) => {
        const currentState = get()
        if (!(name in currentState)) {
          set({ [name]: slice(set as any, get as any, store as any) })
        }
      },
    }))
  }
  return store
}

// 使用
async function loadUserSlice() {
  const { createUserSlice } = await import('./slices/userSlice')
  getStore().getState().loadSlice('user', createUserSlice)
}

条件切片 #

tsx
// store/conditionalStore.ts
import { create } from 'zustand'

interface BaseSlice {
  initialized: boolean
  initialize: () => void
}

interface FeatureSlice {
  featureEnabled: boolean
  enableFeature: () => void
}

type StoreState = BaseSlice & Partial<FeatureSlice>

export const useStore = create<StoreState>((set, get) => ({
  initialized: false,
  
  initialize: () => {
    set({ initialized: true })
    
    // 根据条件加载功能切片
    if (someCondition) {
      import('./slices/featureSlice').then(({ createFeatureSlice }) => {
        set(createFeatureSlice(set as any, get as any, useStore as any))
      })
    }
  },
}))

切片工厂 #

创建可复用切片 #

tsx
// factories/createCrudSlice.ts
import { StateCreator } from 'zustand'

interface CrudSlice<T extends { id: string }> {
  items: T[]
  selectedItem: T | null
  
  addItem: (item: T) => void
  updateItem: (id: string, data: Partial<T>) => void
  removeItem: (id: string) => void
  selectItem: (item: T | null) => void
  clearItems: () => void
}

export function createCrudSlice<T extends { id: string }>(
  name: string,
  initialItems: T[] = []
): StateCreator<CrudSlice<T>> {
  return (set) => ({
    items: initialItems,
    selectedItem: null,
    
    addItem: (item) => set((state) => ({
      items: [...state.items, item],
    })),
    
    updateItem: (id, data) => set((state) => ({
      items: state.items.map((item) =>
        item.id === id ? { ...item, ...data } : item
      ),
    })),
    
    removeItem: (id) => set((state) => ({
      items: state.items.filter((item) => item.id !== id),
      selectedItem: state.selectedItem?.id === id ? null : state.selectedItem,
    })),
    
    selectItem: (item) => set({ selectedItem: item }),
    
    clearItems: () => set({ items: [], selectedItem: null }),
  })
}

// 使用
interface Todo {
  id: string
  text: string
  completed: boolean
}

const createTodoSlice = createCrudSlice<Todo>('todos')

interface Post {
  id: string
  title: string
  content: string
}

const createPostSlice = createCrudSlice<Post>('posts')

项目结构 #

推荐目录结构 #

text
src/
├── store/
│   ├── index.ts              # 主 store 导出
│   ├── slices/
│   │   ├── userSlice.ts      # 用户切片
│   │   ├── cartSlice.ts      # 购物车切片
│   │   ├── themeSlice.ts     # 主题切片
│   │   └── index.ts          # 切片导出
│   ├── middleware/
│   │   ├── logger.ts         # 日志中间件
│   │   └── index.ts
│   └── types/
│       └── slices.ts         # 切片类型定义
├── hooks/
│   ├── useUser.ts            # 用户相关 hooks
│   ├── useCart.ts            # 购物车相关 hooks
│   └── useTheme.ts           # 主题相关 hooks
└── components/
    └── ...

切片导出 #

tsx
// store/slices/index.ts
export type { UserSlice, User } from './userSlice'
export { createUserSlice } from './userSlice'

export type { CartSlice, CartItem } from './cartSlice'
export { createCartSlice } from './cartSlice'

export type { ThemeSlice, Theme } from './themeSlice'
export { createThemeSlice } from './themeSlice'

自定义 Hooks #

tsx
// hooks/useUser.ts
import { useStore } from '../store'

export function useUser() {
  const user = useStore((state) => state.user)
  const isAuthenticated = useStore((state) => state.isAuthenticated)
  const isLoading = useStore((state) => state.isLoading)
  const error = useStore((state) => state.error)
  const login = useStore((state) => state.login)
  const logout = useStore((state) => state.logout)
  const updateUser = useStore((state) => state.updateUser)
  
  return {
    user,
    isAuthenticated,
    isLoading,
    error,
    login,
    logout,
    updateUser,
  }
}

// 使用
function UserProfile() {
  const { user, isAuthenticated, logout } = useUser()
  
  if (!isAuthenticated) {
    return <LoginPrompt />
  }
  
  return (
    <div>
      <h1>{user?.name}</h1>
      <button onClick={logout}>Logout</button>
    </div>
  )
}

最佳实践 #

1. 保持切片独立 #

tsx
// ✅ 好:切片独立,无直接依赖
const createUserSlice = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
})

// ❌ 不好:切片直接依赖其他切片
const createUserSlice = (set, get) => ({
  user: null,
  setUser: (user) => {
    set({ user })
    // 直接调用其他切片的方法
    get().updateCart({ userId: user.id })
  },
})

2. 使用类型安全的组合 #

tsx
// ✅ 好:显式类型定义
type StoreState = UserSlice & CartSlice & ThemeSlice

const useStore = create<StoreState>()((...a) => ({
  ...createUserSlice(...a),
  ...createCartSlice(...a),
  ...createThemeSlice(...a),
}))

3. 封装选择器 #

tsx
// hooks/useSelectors.ts
import { shallow } from 'zustand/shallow'

export function useUserSelectors() {
  return useStore(
    (state) => ({
      user: state.user,
      isAuthenticated: state.isAuthenticated,
    }),
    shallow
  )
}

总结 #

切片模式的关键点:

  • 将大型 Store 拆分成独立的切片
  • 每个切片负责一个特定领域
  • 使用类型安全的组合方式
  • 通过 get 函数实现切片间通信
  • 使用工厂函数创建可复用切片

接下来,让我们学习 计算属性,掌握派生状态处理技巧。

最后更新:2026-03-28