--- name: frontend-patterns description: 針對 React、Next.js、狀態管理、效能優化以及 UI 最佳實踐的前端開發模式。 --- # 前端開發模式 (Frontend Development Patterns) 適用於 React、Next.js 及高效能使用者介面的現代化前端開發模式。 ## 何時啟用 - 建構 React 組件 (如:組合、Props 傳遞、渲染)。 - 管理狀態 (useState, useReducer, Zustand, Context)。 - 實作資料獲取 (SWR, React Query, Server Components)。 - 優化效能 (Memoization, 虛擬化、程式碼分割)。 - 處理表單 (驗證、受控輸入、Zod Schema)。 - 處理客戶端路由與導覽。 - 建構具備無障礙功能 (A11y) 與響應式設計的 UI 模式。 ## 組件模式 (Component Patterns) ### 組合優於繼承 (Composition Over Inheritance) ```typescript // ✅ 推薦:組件組合 interface CardProps { children: React.ReactNode variant?: 'default' | 'outlined' } export function Card({ children, variant = 'default' }: CardProps) { return
{children}
} export function CardHeader({ children }: { children: React.ReactNode }) { return
{children}
} export function CardBody({ children }: { children: React.ReactNode }) { return
{children}
} // 用法 標題 內容 ``` ### 複合組件 (Compound Components) ```typescript interface TabsContextValue { activeTab: string setActiveTab: (tab: string) => void } const TabsContext = createContext(undefined) export function Tabs({ children, defaultTab }: { children: React.ReactNode defaultTab: string }) { const [activeTab, setActiveTab] = useState(defaultTab) return ( {children} ) } export function TabList({ children }: { children: React.ReactNode }) { return
{children}
} export function Tab({ id, children }: { id: string, children: React.ReactNode }) { const context = useContext(TabsContext) if (!context) throw new Error('Tab 必須在 Tabs 內使用') return ( ) } // 用法 概覽 詳情 ``` ### 渲染 Props 模式 (Render Props Pattern) ```typescript interface DataLoaderProps { url: string children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode } export function DataLoader({ url, children }: DataLoaderProps) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)) }, [url]) return <>{children(data, loading, error)} } // 用法 url="/api/markets"> {(markets, loading, error) => { if (loading) return if (error) return return }} ``` ## 自定義 Hook 模式 ### 狀態管理 Hook ```typescript export function useToggle(initialValue = false): [boolean, () => void] { const [value, setValue] = useState(initialValue) const toggle = useCallback(() => { setValue(v => !v) }, []) return [value, toggle] } // 用法 const [isOpen, toggleOpen] = useToggle() ``` ### 非同步資料獲取 Hook ```typescript interface UseQueryOptions { onSuccess?: (data: T) => void onError?: (error: Error) => void enabled?: boolean } export function useQuery( key: string, fetcher: () => Promise, options?: UseQueryOptions ) { const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const refetch = useCallback(async () => { setLoading(true) setError(null) try { const result = await fetcher() setData(result) options?.onSuccess?.(result) } catch (err) { const error = err as Error setError(error) options?.onError?.(error) } finally { setLoading(false) } }, [fetcher, options]) useEffect(() => { if (options?.enabled !== false) { refetch() } }, [key, refetch, options?.enabled]) return { data, error, loading, refetch } } // 用法 const { data: markets, loading, error, refetch } = useQuery( 'markets', () => fetch('/api/markets').then(r => r.json()), { onSuccess: data => console.log('已獲取', data.length, '筆市場資料'), onError: err => console.error('失敗:', err) } ) ``` ### 防抖 (Debounce) Hook ```typescript export function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value) useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value) }, delay) return () => clearTimeout(handler) }, [value, delay]) return debouncedValue } // 用法 const [searchQuery, setSearchQuery] = useState('') const debouncedQuery = useDebounce(searchQuery, 500) useEffect(() => { if (debouncedQuery) { performSearch(debouncedQuery) } }, [debouncedQuery]) ``` ## 狀態管理模式 ### Context + Reducer 模式 ```typescript interface State { markets: Market[] selectedMarket: Market | null loading: boolean } type Action = | { type: 'SET_MARKETS'; payload: Market[] } | { type: 'SELECT_MARKET'; payload: Market } | { type: 'SET_LOADING'; payload: boolean } function reducer(state: State, action: Action): State { switch (action.type) { case 'SET_MARKETS': return { ...state, markets: action.payload } case 'SELECT_MARKET': return { ...state, selectedMarket: action.payload } case 'SET_LOADING': return { ...state, loading: action.payload } default: return state } } const MarketContext = createContext<{ state: State dispatch: Dispatch } | undefined>(undefined) export function MarketProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(reducer, { markets: [], selectedMarket: null, loading: false }) return ( {children} ) } export function useMarkets() { const context = useContext(MarketContext) if (!context) throw new Error('useMarkets 必須在 MarketProvider 內使用') return context } ``` ## 效能優化 (Performance Optimization) ### 備忘錄化 (Memoization) ```typescript // ✅ 使用 useMemo 處理成本高昂的計算 const sortedMarkets = useMemo(() => { return markets.sort((a, b) => b.volume - a.volume) }, [markets]) // ✅ 使用 useCallback 處理傳遞給子組件的函式 const handleSearch = useCallback((query: string) => { setSearchQuery(query) }, []) // ✅ 使用 React.memo 處理純組件 export const MarketCard = React.memo(({ market }) => { return (

{market.name}

{market.description}

) }) ``` ### 程式碼分割與延遲載入 (Code Splitting & Lazy Loading) ```typescript import { lazy, Suspense } from 'react' // ✅ 延遲載入重量級組件 const HeavyChart = lazy(() => import('./HeavyChart')) const ThreeJsBackground = lazy(() => import('./ThreeJsBackground')) export function Dashboard() { return (
}>
) } ``` ### 長列表虛擬化 (Virtualization) ```typescript import { useVirtualizer } from '@tanstack/react-virtual' export function VirtualMarketList({ markets }: { markets: Market[] }) { const parentRef = useRef(null) const virtualizer = useVirtualizer({ count: markets.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, // 預估行高 overscan: 5 // 額外渲染的項目數量 }) return (
{virtualizer.getVirtualItems().map(virtualRow => (
))}
) } ``` ## 表單處理模式 ### 帶驗證的受控表單 (Controlled Form) ```typescript interface FormData { name: string description: string endDate: string } interface FormErrors { name?: string description?: string endDate?: string } export function CreateMarketForm() { const [formData, setFormData] = useState({ name: '', description: '', endDate: '' }) const [errors, setErrors] = useState({}) const validate = (): boolean => { const newErrors: FormErrors = {} if (!formData.name.trim()) { newErrors.name = '名稱為必填項' } else if (formData.name.length > 200) { newErrors.name = '名稱必須在 200 字元以內' } if (!formData.description.trim()) { newErrors.description = '描述為必填項' } if (!formData.endDate) { newErrors.endDate = '結束日期為必填項' } setErrors(newErrors) return Object.keys(newErrors).length === 0 } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!validate()) return try { await createMarket(formData) // 成功處理 } catch (error) { // 錯誤處理 } } return (
setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="市場名稱" /> {errors.name && {errors.name}} {/* 其他欄位 */}
) } ``` ## 錯誤邊界模式 (Error Boundary Pattern) ```typescript interface ErrorBoundaryState { hasError: boolean error: Error | null } export class ErrorBoundary extends React.Component< { children: React.ReactNode }, ErrorBoundaryState > { state: ErrorBoundaryState = { hasError: false, error: null } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error } } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('錯誤邊界擷取到錯誤:', error, errorInfo) } render() { if (this.state.hasError) { return (

發生了一些錯誤

{this.state.error?.message}

) } return this.props.children } } // 用法 ``` ## 動畫模式 ### Framer Motion 動畫 ```typescript import { motion, AnimatePresence } from 'framer-motion' // ✅ 列表動畫 export function AnimatedMarketList({ markets }: { markets: Market[] }) { return ( {markets.map(market => ( ))} ) } // ✅ 互動彈窗 (Modal) 動畫 export function Modal({ isOpen, onClose, children }: ModalProps) { return ( {isOpen && ( <> {children} )} ) } ``` ## 無障礙功能 (Accessibility) 模式 ### 鍵盤導覽 (Keyboard Navigation) ```typescript export function Dropdown({ options, onSelect }: DropdownProps) { const [isOpen, setIsOpen] = useState(false) const [activeIndex, setActiveIndex] = useState(0) const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault() setActiveIndex(i => Math.min(i + 1, options.length - 1)) break case 'ArrowUp': e.preventDefault() setActiveIndex(i => Math.max(i - 1, 0)) break case 'Enter': e.preventDefault() onSelect(options[activeIndex]) setIsOpen(false) break case 'Escape': setIsOpen(false) break } } return (
{/* 下拉選單實作內容 */}
) } ``` ### 焦點管理 (Focus Management) ```typescript export function Modal({ isOpen, onClose, children }: ModalProps) { const modalRef = useRef(null) const previousFocusRef = useRef(null) useEffect(() => { if (isOpen) { // 儲存目前獲取焦點的元素 previousFocusRef.current = document.activeElement as HTMLElement // 將焦點移至彈窗 modalRef.current?.focus() } else { // 關閉時恢復焦點 previousFocusRef.current?.focus() } }, [isOpen]) return isOpen ? (
e.key === 'Escape' && onClose()} > {children}
) : null } ``` **請記住**:現代前端模式能讓使用者介面更易於維護且更具效能。請根據專案的複雜度選擇適合的模式。