如何集成状态管理库(如 Zustand)?在 RSC 环境中需要注意什么?

35 阅读2分钟

如何集成状态管理库(如 Zustand)?在 RSC 环境中需要注意什么?

Zustand 集成

1. 基础设置

# 安装 Zustand
npm install zustand
// store/useStore.js
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const useStore = create(
  devtools(
    (set, get) => ({
      // 状态
      count: 0,
      user: null,
      theme: 'light',

      // 操作
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      setUser: (user) => set({ user }),
      setTheme: (theme) => set({ theme }),

      // 异步操作
      fetchUser: async (id) => {
        try {
          const response = await fetch(`/api/users/${id}`)
          const user = await response.json()
          set({ user })
        } catch (error) {
          console.error('Failed to fetch user:', error)
        }
      },
    }),
    {
      name: 'app-store', // 用于 Redux DevTools
    }
  )
)

export default useStore

2. 在 Client Components 中使用

// app/components/Counter.jsx
'use client'
import useStore from '@/store/useStore'

export default function Counter() {
  const { count, increment, decrement } = useStore()

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}
// app/components/UserProfile.jsx
'use client'
import { useEffect } from 'react'
import useStore from '@/store/useStore'

export default function UserProfile() {
  const { user, fetchUser } = useStore()

  useEffect(() => {
    fetchUser(1)
  }, [fetchUser])

  if (!user) {
    return <div>Loading...</div>
  }

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

RSC 环境中的注意事项

1. 不能在 Server Components 中使用

// ❌ 错误 - 在 Server Component 中使用 Zustand
// app/page.js
import useStore from '@/store/useStore'

export default function HomePage() {
  // 错误:Server Components 不能使用客户端状态
  const { count } = useStore()

  return (
    <div>
      <h1>Count: {count}</h1>
    </div>
  )
}

// ✅ 正确 - 使用 Client Component
// app/components/ClientCounter.jsx
'use client'
import useStore from '@/store/useStore'

export default function ClientCounter() {
  const { count } = useStore()

  return (
    <div>
      <h1>Count: {count}</h1>
    </div>
  )
}

// app/page.js
import ClientCounter from '@/components/ClientCounter'

export default function HomePage() {
  return (
    <div>
      <h1>Home Page</h1>
      <ClientCounter />
    </div>
  )
}

2. 服务器端数据初始化

// app/page.js - Server Component
import { db } from '@/lib/database'
import ClientCounter from '@/components/ClientCounter'

export default async function HomePage() {
  // 在服务器端获取数据
  const initialData = await db.counters.findFirst()

  return (
    <div>
      <h1>Home Page</h1>
      <ClientCounter initialCount={initialData?.count || 0} />
    </div>
  )
}
// app/components/ClientCounter.jsx
'use client'
import { useEffect } from 'react'
import useStore from '@/store/useStore'

export default function ClientCounter({ initialCount }) {
  const { count, setCount } = useStore()

  // 使用服务器端数据初始化客户端状态
  useEffect(() => {
    if (initialCount !== undefined) {
      setCount(initialCount)
    }
  }, [initialCount, setCount])

  return (
    <div>
      <h2>Count: {count}</h2>
    </div>
  )
}

3. 状态持久化

// store/useStore.js
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useStore = create(
  persist(
    (set, get) => ({
      count: 0,
      user: null,
      theme: 'light',

      increment: () => set((state) => ({ count: state.count + 1 })),
      setUser: (user) => set({ user }),
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'app-storage', // localStorage 键名
      partialize: (state) => ({
        count: state.count,
        theme: state.theme,
      }), // 只持久化部分状态
    }
  )
)

export default useStore

实际应用示例

1. 购物车状态管理

// store/useCartStore.js
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

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

      addItem: (product) => {
        const items = get().items
        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 }],
          })
        }

        get().calculateTotal()
      },

      removeItem: (productId) => {
        set({
          items: get().items.filter((item) => item.id !== productId),
        })
        get().calculateTotal()
      },

      updateQuantity: (productId, quantity) => {
        if (quantity <= 0) {
          get().removeItem(productId)
          return
        }

        set({
          items: get().items.map((item) =>
            item.id === productId ? { ...item, quantity } : item
          ),
        })
        get().calculateTotal()
      },

      calculateTotal: () => {
        const total = get().items.reduce(
          (sum, item) => sum + item.price * item.quantity,
          0
        )
        set({ total })
      },

      clearCart: () => set({ items: [], total: 0 }),
    }),
    {
      name: 'cart-storage',
    }
  )
)

export default useCartStore
// app/components/Cart.jsx
'use client'
import useCartStore from '@/store/useCartStore'

export default function Cart() {
  const { items, total, removeItem, updateQuantity, clearCart } = useCartStore()

  return (
    <div>
      <h2>Shopping Cart</h2>
      {items.length === 0 ? (
        <p>Your cart is empty</p>
      ) : (
        <>
          {items.map((item) => (
            <div key={item.id}>
              <h3>{item.name}</h3>
              <p>Price: ${item.price}</p>
              <div>
                <button
                  onClick={() => updateQuantity(item.id, item.quantity - 1)}
                >
                  -
                </button>
                <span>{item.quantity}</span>
                <button
                  onClick={() => updateQuantity(item.id, item.quantity + 1)}
                >
                  +
                </button>
              </div>
              <button onClick={() => removeItem(item.id)}>Remove</button>
            </div>
          ))}
          <div>
            <h3>Total: ${total}</h3>
            <button onClick={clearCart}>Clear Cart</button>
          </div>
        </>
      )}
    </div>
  )
}

2. 用户认证状态

// store/useAuthStore.js
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useAuthStore = create(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: async (credentials) => {
        try {
          const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials),
          })

          if (response.ok) {
            const { user, token } = await response.json()
            set({ user, token, isAuthenticated: true })
            return { success: true }
          } else {
            return { success: false, error: 'Invalid credentials' }
          }
        } catch (error) {
          return { success: false, error: error.message }
        }
      },

      logout: () => {
        set({ user: null, token: null, isAuthenticated: false })
      },

      updateUser: (userData) => {
        set({ user: { ...get().user, ...userData } })
      },
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({
        user: state.user,
        token: state.token,
        isAuthenticated: state.isAuthenticated,
      }),
    }
  )
)

export default useAuthStore
// app/components/AuthButton.jsx
'use client'
import useAuthStore from '@/store/useAuthStore'

export default function AuthButton() {
  const { user, isAuthenticated, logout } = useAuthStore()

  if (!isAuthenticated) {
    return <a href="/login">Login</a>
  }

  return (
    <div>
      <span>Welcome, {user.name}</span>
      <button onClick={logout}>Logout</button>
    </div>
  )
}

高级功能

1. 中间件组合

// store/useAdvancedStore.js
import { create } from 'zustand'
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'

const useAdvancedStore = create(
  devtools(
    persist(
      subscribeWithSelector((set, get) => ({
        count: 0,
        user: null,

        increment: () => set((state) => ({ count: state.count + 1 })),
        setUser: (user) => set({ user }),
      })),
      {
        name: 'advanced-storage',
      }
    ),
    {
      name: 'advanced-store',
    }
  )
)

// 订阅状态变化
useAdvancedStore.subscribe(
  (state) => state.count,
  (count) => {
    console.log('Count changed:', count)
  }
)

export default useAdvancedStore

2. 类型安全

// store/types.ts
export interface User {
  id: string
  name: string
  email: string
}

export interface AuthState {
  user: User | null
  token: string | null
  isAuthenticated: boolean
  login: (credentials: LoginCredentials) => Promise<LoginResult>
  logout: () => void
  updateUser: (userData: Partial<User>) => void
}

export interface LoginCredentials {
  email: string
  password: string
}

export interface LoginResult {
  success: boolean
  error?: string
}
// store/useAuthStore.ts
import { create } from 'zustand'
import { AuthState, LoginCredentials, LoginResult } from './types'

const useAuthStore = create<AuthState>((set, get) => ({
  user: null,
  token: null,
  isAuthenticated: false,

  login: async (credentials: LoginCredentials): Promise<LoginResult> => {
    // 实现登录逻辑
  },

  logout: () => {
    set({ user: null, token: null, isAuthenticated: false })
  },

  updateUser: (userData: Partial<User>) => {
    const currentUser = get().user
    if (currentUser) {
      set({ user: { ...currentUser, ...userData } })
    }
  },
}))

export default useAuthStore

最佳实践

1. 状态分离

// store/useUserStore.js
const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}))

// store/useCartStore.js
const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}))

// store/useThemeStore.js
const useThemeStore = create((set) => ({
  theme: 'light',
  setTheme: (theme) => set({ theme }),
}))

2. 性能优化

// 使用选择器避免不必要的重渲染
const count = useStore((state) => state.count)
const increment = useStore((state) => state.increment)

// 使用 shallow 比较
import { shallow } from 'zustand/shallow'

const { count, increment } = useStore(
  (state) => ({ count: state.count, increment: state.increment }),
  shallow
)

总结

在 Next.js 中集成状态管理库的要点:

Zustand 集成

  • 基础设置和配置
  • 中间件使用
  • 类型安全

RSC 环境注意事项

  • 不能在 Server Components 中使用
  • 服务器端数据初始化
  • 状态持久化

最佳实践

  • 状态分离
  • 性能优化
  • 类型安全
  • 错误处理

推荐方案

  • Zustand (轻量级)
  • Redux Toolkit (复杂应用)
  • Jotai (原子化状态)