Skip to main content

frontend-patterns

Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.

Jump to install

Source facts

Repository
affaan-m/ECC
Last source activity
June 7, 2026 at 05:26
Detected SKILL.md language
Chinese
Stars
264,820
Forks
39,570

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
frontend-patterns
description
Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
# 前端開發模式 用於 React、Next.js 和高效能使用者介面的現代前端模式。 ## 元件模式 ### 組合優於繼承 ```typescript // PASS: 良好:元件組合 interface CardProps { children: React.ReactNode variant?: 'default' | 'outlined' } export function Card({ children, variant = 'default' }: CardProps) { return <div className={`card card-${variant}`}>{children}</div> } export function CardHeader({ children }: { children: React.ReactNode }) { return <div className="card-header">{children}</div> } export function CardBody({ children }: { children: React.ReactNode }) { return <div className="card-body">{children}</div> } // 使用方式 <Card> <CardHeader>標題</CardHeader> <CardBody>內容</CardBody> </Card> ``` ### 複合元件 ```typescript interface TabsContextValue { activeTab: string setActiveTab: (tab: string) => void } const TabsContext = createContext<TabsContextValue | undefined>(undefined) export function Tabs({ children, defaultTab }: { children: React.ReactNode defaultTab: string }) { const [activeTab, setActiveTab] = useState(defaultTab) return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> {children} </TabsContext.Provider> ) } export function TabList({ children }: { children: React.ReactNode }) { return <div className="tab-list">{children}</div> } export function Tab({ id, children }: { id: string, children: React.ReactNode }) { const context = useContext(TabsContext) if (!context) throw new Error('Tab must be used within Tabs') return ( <button className={context.activeTab === id ? 'active' : ''} onClick={() => context.setActiveTab(id)} > {children} </button> ) } // 使用方式 <Tabs defaultTab="overview"> <TabList> <Tab id="overview">概覽</Tab> <Tab id="details">詳情</Tab> </TabList> </Tabs> ``` ### Render Props 模式 ```typescript interface DataLoaderProps<T> { url: string children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode } export function DataLoader<T>({ url, children }: DataLoaderProps<T>) { const [data, setData] = useState<T | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null) useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)) }, [url]) return <>{children(data, loading, error)}</> } // 使用方式 <DataLoader<Market[]> url="/api/markets"> {(markets, loading, error) => { if (loading) return <Spinner /> if (error) return <Error error={error} /> return <MarketList markets={markets!} /> }} </DataLoader> ``` ## 自訂 Hooks 模式 ### 狀態管理 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<T> { onSuccess?: (data: T) => void onError?: (error: Error) => void enabled?: boolean } export function useQuery<T>( key: string, fetcher: () => Promise<T>, options?: UseQueryOptions<T> ) { const [data, setData] = useState<T | null>(null) const [error, setError] = useState<Error | null>(null) const [loading, setLoading] = useState(false) // 將最新的 fetcher/options 保存在 ref 中,讓 refetch 即使在呼叫端 // 傳入行內函式與物件字面值時也能保持參照穩定。 // 若沒有這麼做,每次渲染都會建立新的 refetch,下方的 effect 會在 // 每次狀態更新後重新執行,造成無限取得迴圈。 const fetcherRef = useRef(fetcher) const optionsRef = useRef(options) useEffect(() => { fetcherRef.current = fetcher optionsRef.current = options }) const refetch = useCallback(async () => { setLoading(true) setError(null) try { const result = await fetcherRef.current() setData(result) optionsRef.current?.onSuccess?.(result) } catch (err) { const error = err as Error setError(error) optionsRef.current?.onError?.(error) } finally { setLoading(false) } }, []) const enabled = options?.enabled !== false useEffect(() => { if (enabled) { refetch() } }, [key, enabled, refetch]) return { data, error, loading, refetch } } // 使用方式 const { data: markets, loading, error, refetch } = useQuery( 'markets', () => fetch('/api/markets').then(r => r.json()), { onSuccess: data => console.log('Fetched', data.length, 'markets'), onError: err => console.error('Failed:', err) } ) ``` ### Debounce Hook ```typescript export function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(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<Action> } | undefined>(undefined) export function MarketProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(reducer, { markets: [], selectedMarket: null, loading: false }) return ( <MarketContext.Provider value={{ state, dispatch }}> {children} </MarketContext.Provider> ) } export function useMarkets() { const context = useContext(MarketContext) if (!context) throw new Error('useMarkets must be used within MarketProvider') return context } ``` ## 效能優化 ### 記憶化 ```typescript // PASS: useMemo 用於昂貴計算 // 排序前先複製 - Array.prototype.sort 會就地修改陣列 const sortedMarkets = useMemo(() => { return [...markets].sort((a, b) => b.volume - a.volume) }, [markets]) // PASS: useCallback 用於傳遞給子元件的函式 const handleSearch = useCallback((query: string) => { setSearchQuery(query) }, []) // PASS: React.memo 用於純元件 export const MarketCard = React.memo<MarketCardProps>(({ market }) => { return ( <div className="market-card"> <h3>{market.name}</h3> <p>{market.description}</p> </div> ) }) ``` ### 程式碼分割與延遲載入 ```typescript import { lazy, Suspense } from 'react' // PASS: 延遲載入重型元件 const HeavyChart = lazy(() => import('./HeavyChart')) const ThreeJsBackground = lazy(() => import('./ThreeJsBackground')) export function Dashboard() { return ( <div> <Suspense fallback={<ChartSkeleton />}> <HeavyChart data={data} /> </Suspense> <Suspense fallback={null}> <ThreeJsBackground /> </Suspense> </div> ) } ``` ### 長列表虛擬化 ```typescript import { useVirtualizer } from '@tanstack/react-virtual' export function VirtualMarketList({ markets }: { markets: Market[] }) { const parentRef = useRef<HTMLDivElement>(null) const virtualizer = useVirtualizer({ count: markets.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, // 預估行高 overscan: 5 // 額外渲染的項目數 }) return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }} > {virtualizer.getVirtualItems().map(virtualRow => ( <div key={virtualRow.index} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)` }} > <MarketCard market={markets[virtualRow.index]} /> </div> ))} </div> </div> ) } ``` ## 表單處理模式 ### 帶驗證的受控表單 ```typescript interface FormData { name: string description: string endDate: string } interface FormErrors { name?: string description?: string endDate?: string } export function CreateMarketForm() { const [formData, setFormData] = useState<FormData>({ name: '', description: '', endDate: '' }) const [errors, setErrors] = useState<FormErrors>({}) 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 ( <form onSubmit={handleSubmit}> <input value={formData.name} onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="市場名稱" />
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub