# 狀態管理架構指南 ## 🎯 狀態管理策略 DramaLing 採用分層狀態管理策略: ``` ┌─────────────────────────────────────┐ │ Global State (Zustand) │ ← 用戶資料、主題設定 ├─────────────────────────────────────┤ │ Server State (TanStack Query) │ ← API 數據、快取 ├─────────────────────────────────────┤ │ Component State (useState) │ ← UI 狀態、表單 └─────────────────────────────────────┘ ``` ## 📦 技術選型 | 狀態類型 | 工具 | 使用場景 | |---------|------|---------| | **全局狀態** | Zustand | 用戶認證、主題、設定 | | **服務端狀態** | TanStack Query | API 數據、快取管理 | | **表單狀態** | React Hook Form | 複雜表單驗證 | | **組件狀態** | useState/useReducer | 簡單 UI 狀態 | ## 🔧 安裝配置 ```bash # 狀態管理核心 npm install zustand npm install @tanstack/react-query npm install react-hook-form zod npm install @hookform/resolvers ``` ## 🏗️ Zustand 全局狀態 ### 1. Store 結構 ```typescript // src/store/index.ts import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' import { createAuthSlice, AuthSlice } from './slices/authSlice' import { createSettingsSlice, SettingsSlice } from './slices/settingsSlice' import { createFlashcardSlice, FlashcardSlice } from './slices/flashcardSlice' export type StoreState = AuthSlice & SettingsSlice & FlashcardSlice export const useStore = create()( devtools( persist( (...a) => ({ ...createAuthSlice(...a), ...createSettingsSlice(...a), ...createFlashcardSlice(...a), }), { name: 'dramaling-storage', partialize: (state) => ({ // 只持久化部分數據 theme: state.theme, language: state.language, }), } ) ) ) ``` ### 2. Auth Slice 範例 ```typescript // src/store/slices/authSlice.ts import { StateCreator } from 'zustand' import { User } from '@/types' export interface AuthSlice { // State user: User | null isAuthenticated: boolean isLoading: boolean // Actions setUser: (user: User | null) => void login: (email: string, password: string) => Promise logout: () => Promise checkAuth: () => Promise } export const createAuthSlice: StateCreator = (set, get) => ({ user: null, isAuthenticated: false, isLoading: true, setUser: (user) => { set({ user, isAuthenticated: !!user }) }, login: async (email, password) => { set({ isLoading: true }) 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('Login failed') const { user, token } = await response.json() localStorage.setItem('token', token) set({ user, isAuthenticated: true }) } catch (error) { console.error('Login error:', error) throw error } finally { set({ isLoading: false }) } }, logout: async () => { try { await fetch('/api/auth/logout', { method: 'POST' }) localStorage.removeItem('token') set({ user: null, isAuthenticated: false }) } catch (error) { console.error('Logout error:', error) } }, checkAuth: async () => { const token = localStorage.getItem('token') if (!token) { set({ isLoading: false, isAuthenticated: false }) return } try { const response = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` }, }) if (response.ok) { const user = await response.json() set({ user, isAuthenticated: true }) } else { localStorage.removeItem('token') set({ user: null, isAuthenticated: false }) } } catch (error) { console.error('Auth check error:', error) } finally { set({ isLoading: false }) } }, }) ``` ### 3. 使用 Store ```typescript // src/components/Header.tsx import { useStore } from '@/store' export function Header() { const { user, logout, isAuthenticated } = useStore() return (
{isAuthenticated ? (
Welcome, {user?.name}
) : ( Login )}
) } ``` ## 🔄 TanStack Query 服務端狀態 ### 1. Query Client 配置 ```typescript // src/lib/query-client.ts import { QueryClient } from '@tanstack/react-query' export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 分鐘 gcTime: 1000 * 60 * 10, // 10 分鐘(原 cacheTime) retry: 3, refetchOnWindowFocus: false, }, mutations: { retry: 1, }, }, }) ``` ### 2. Provider 設置 ```typescript // src/app/providers.tsx 'use client' import { QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { queryClient } from '@/lib/query-client' export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### 3. API Hooks ```typescript // src/hooks/api/useFlashcards.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { flashcardApi } from '@/lib/api/flashcards' // 查詢 Hook export function useFlashcards(deckId?: string) { return useQuery({ queryKey: ['flashcards', deckId], queryFn: () => flashcardApi.getFlashcards(deckId), enabled: !!deckId, }) } // 創建 Hook export function useCreateFlashcard() { const queryClient = useQueryClient() return useMutation({ mutationFn: flashcardApi.createFlashcard, onSuccess: (data, variables) => { // 更新快取 queryClient.invalidateQueries({ queryKey: ['flashcards', variables.deckId] }) // 樂觀更新 queryClient.setQueryData( ['flashcards', variables.deckId], (oldData: any) => [...oldData, data] ) }, }) } // 批量操作 Hook export function useGenerateFlashcards() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (text: string) => { const response = await fetch('/api/flashcards/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }) if (!response.ok) throw new Error('Generation failed') return response.json() }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['flashcards'] }) }, }) } ``` ### 4. 使用 Query Hooks ```typescript // src/app/flashcards/page.tsx 'use client' import { useFlashcards, useCreateFlashcard } from '@/hooks/api/useFlashcards' export default function FlashcardsPage() { const { data: flashcards, isLoading, error } = useFlashcards('deck-1') const createMutation = useCreateFlashcard() if (isLoading) return if (error) return const handleCreate = async (data: FlashcardInput) => { try { await createMutation.mutateAsync(data) toast.success('Flashcard created!') } catch (error) { toast.error('Failed to create flashcard') } } return (
{flashcards?.map(card => ( ))}
) } ``` ## 📝 React Hook Form 表單狀態 ### 1. 表單配置與驗證 ```typescript // src/components/forms/FlashcardForm.tsx import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' // 驗證 Schema const flashcardSchema = z.object({ word: z.string().min(1, 'Word is required'), translation: z.string().min(1, 'Translation is required'), example: z.string().optional(), difficulty: z.enum(['easy', 'medium', 'hard']), tags: z.array(z.string()).optional(), }) type FlashcardFormData = z.infer export function FlashcardForm({ onSubmit }: { onSubmit: (data: FlashcardFormData) => void }) { const { register, handleSubmit, formState: { errors, isSubmitting }, reset, watch, setValue, } = useForm({ resolver: zodResolver(flashcardSchema), defaultValues: { difficulty: 'medium', tags: [], }, }) // 監聽特定欄位 const difficulty = watch('difficulty') const onFormSubmit = async (data: FlashcardFormData) => { await onSubmit(data) reset() // 重置表單 } return (
{errors.word && {errors.word.message}}
{errors.translation && {errors.translation.message}}