Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
.claude/skills/loulanyue-frontend-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 113% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 165% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 70% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 127% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 379% | 0% |
用於 React、Next.js 和高效能使用者介面的現代前端模式。
typescript// ✅ 良好:元件組合 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>
typescriptinterface 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>
typescriptinterface 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>
typescriptexport function useToggle(initialValue = false): [boolean, () => void] { const [value, setValue] = useState(initialValue) const toggle = useCallback(() => { setValue(v => !v) }, []) return [value, toggle] } // 使用方式 const [isOpen, toggleOpen] = useToggle()
typescriptinterface 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) 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('Fetched', data.length, 'markets'), onError: err => console.error('Failed:', err) } )
typescriptexport 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])
typescriptinterface 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// ✅ 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<MarketCardProps>(({ market }) => { return ( <div className="market-card"> <h3>{market.name}</h3> <p>{market.description}</p> </div> ) })
typescriptimport { lazy, Suspense } from 'react' // ✅ 延遲載入重型元件 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> ) }
typescriptimport { 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> ) }
typescriptinterface 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="市場名稱" /> {errors.name && <span className="error">{errors.name}</span>} {/* 其他欄位 */} <button type="submit">建立市場</button> </form> ) }
typescriptinterface 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 boundary caught:', error, errorInfo) } render() { if (this.state.hasError) { return ( <div className="error-fallback"> <h2>發生錯誤</h2> <p>{this.state.error?.message}</p> <button onClick={() => this.setState({ hasError: false })}> 重試 </button> </div> ) } return this.props.children } } // 使用方式 <ErrorBoundary> <App /> </ErrorBoundary>
typescriptimport { motion, AnimatePresence } from 'framer-motion' // ✅ 列表動畫 export function AnimatedMarketList({ markets }: { markets: Market[] }) { return ( <AnimatePresence> {markets.map(market => ( <motion.div key={market.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} > <MarketCard market={market} /> </motion.div> ))} </AnimatePresence> ) } // ✅ Modal 動畫 export function Modal({ isOpen, onClose, children }: ModalProps) { return ( <AnimatePresence> {isOpen && ( <> <motion.div className="modal-overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} /> <motion.div className="modal-content" initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }} > {children} </motion.div> </> )} </AnimatePresence> ) }
typescriptexport 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 ( <div role="combobox" aria-expanded={isOpen} aria-haspopup="listbox" onKeyDown={handleKeyDown} > {/* 下拉選單實作 */} </div> ) }
typescriptexport function Modal({ isOpen, onClose, children }: ModalProps) { const modalRef = useRef<HTMLDivElement>(null) const previousFocusRef = useRef<HTMLElement | null>(null) useEffect(() => { if (isOpen) { // 儲存目前聚焦的元素 previousFocusRef.current = document.activeElement as HTMLElement // 聚焦 modal modalRef.current?.focus() } else { // 關閉時恢復焦點 previousFocusRef.current?.focus() } }, [isOpen]) return isOpen ? ( <div ref={modalRef} role="dialog" aria-modal="true" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()} > {children} </div> ) : null }
記住:現代前端模式能實現可維護、高效能的使用者介面。選擇符合你專案複雜度的模式。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,675 | 10,135 | -31% | 1 | 1 | 0% | 2,881 | 6,137 | +113% | 0 | 0 | — |
case-02 | fail→fail | 17,036 | 12,823 | -25% | 1 | 1 | 0% | 3,384 | 6,818 | +101% | 0 | 0 | — |
case-03 | fail→fail | 15,308 | 11,414 | -25% | 1 | 1 | 0% | 3,179 | 6,365 | +100% | 0 | 0 | — |
case-04 | fail→pass | 11,525 | 10,170 | -12% | 1 | 1 | 0% | 2,318 | 6,150 | +165% | 0 | 0 | — |
case-05 | fail→fail | 16,619 | 14,770 | -11% | 1 | 1 | 0% | 3,271 | 7,038 | +115% | 0 | 0 | — |
case-06 | pass→pass | 5,974 | 6,850 | +15% | 1 | 1 | 0% | 1,110 | 5,314 | +379% | 0 | 0 | — |
case-07 | pass→pass | 10,205 | 9,226 | -10% | 1 | 1 | 0% | 1,784 | 5,823 | +226% | 0 | 0 | — |
case-08 | fail→fail | 17,560 | 13,983 | -20% | 1 | 1 | 0% | 3,681 | 7,137 | +94% | 0 | 0 | — |
case-09 | pass→pass | 16,062 | 18,329 | +14% | 1 | 1 | 0% | 2,992 | 7,763 | +159% | 0 | 0 | — |
case-10 | fail→fail | 18,724 | 16,226 | -13% | 1 | 1 | 0% | 3,516 | 7,299 | +108% | 0 | 0 | — |
case-11 | pass→pass | 15,993 | 14,764 | -8% | 1 | 1 | 0% | 3,384 | 7,264 | +115% | 0 | 0 | — |
case-12 | pass→pass | 16,319 | 15,666 | -4% | 1 | 1 | 0% | 3,189 | 7,528 | +136% | 0 | 0 | — |
case-13 | fail→fail | 14,748 | 10,869 | -26% | 1 | 1 | 0% | 2,812 | 6,226 | +121% | 0 | 0 | — |
case-14 | fail→pass | 21,442 | 16,077 | -25% | 1 | 1 | 0% | 4,363 | 7,407 | +70% | 0 | 0 | — |
case-15 | fail→fail | 23,043 | 21,806 | -5% | 1 | 1 | 0% | 4,827 | 8,804 | +82% | 0 | 0 | — |
case-16 | fail→fail | 16,370 | 13,880 | -15% | 1 | 1 | 0% | 3,360 | 6,990 | +108% | 0 | 0 | — |
case-17 | pass→pass | 9,442 | 6,588 | -30% | 1 | 1 | 0% | 1,827 | 5,383 | +195% | 0 | 0 | — |
case-18 | pass→pass | 6,152 | 5,287 | -14% | 1 | 1 | 0% | 1,096 | 5,097 | +365% | 0 | 0 | — |
case-19 | pass→pass | 8,690 | 7,657 | -12% | 1 | 1 | 0% | 1,436 | 5,424 | +278% | 0 | 0 | — |
case-20 | fail→pass | 15,732 | 15,826 | +1% | 1 | 1 | 0% | 3,074 | 6,984 | +127% | 0 | 0 | — |
case-21 | pass→pass | 13,912 | 11,880 | -15% | 1 | 1 | 0% | 2,627 | 6,395 | +143% | 0 | 0 | — |
case-22 | pass→pass | 11,872 | 12,571 | +6% | 1 | 1 | 0% | 2,004 | 6,367 | +218% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.