Source profileQuality 64/100

affaan-m/ECC/docs/zh-TW/skills/frontend-patterns/SKILL.md

frontend-patterns

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

Source repository stars
234,327
Declared platforms
0
Static risk flags
1
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

用於 React、Next.js 和高效能使用者介面的現代前端模式。

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-TW/skills/frontend-patterns"
    Safe inspection promptEditorial

    Inspect the Agent Skill "frontend-patterns" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/docs/zh-TW/skills/frontend-patterns/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      元件模式

      Review the “元件模式” section in the pinned source before continuing.

      Review and apply the “元件模式” source section.
    2. 02

      組合優於繼承

      Review the “組合優於繼承” section in the pinned source before continuing.

      Review and apply the “組合優於繼承” source section.
    3. 03

      複合元件

      Review the “複合元件” section in the pinned source before continuing.

      Review and apply the “複合元件” source section.
    4. 04

      Render Props 模式

      Review the “Render Props 模式” section in the pinned source before continuing.

      Review and apply the “Render Props 模式” source section.
    5. 05

      自訂 Hooks 模式

      Review the “自訂 Hooks 模式” section in the pinned source before continuing.

      Review and apply the “自訂 Hooks 模式” source section.

    Permission review

    Static risk signals and limitations

    Network access

    medium · line 99

    The documentation includes network, browsing, or remote request actions.

    fetch(url)

    Network access

    medium · line 198

    The documentation includes network, browsing, or remote request actions.

    () => fetch('/api/markets').then(r => r.json()),

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score64/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    docs/zh-TW/skills/frontend-patterns/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    前端開發模式

    用於 React、Next.js 和高效能使用者介面的現代前端模式。

    元件模式

    組合優於繼承

    // 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>
    

    複合元件

    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 模式

    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

    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

    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

    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 模式

    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
    }
    

    效能優化

    記憶化

    // 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>
      )
    })
    

    程式碼分割與延遲載入

    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>
      )
    }
    

    長列表虛擬化

    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>
      )
    }
    

    表單處理模式

    帶驗證的受控表單

    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="市場名稱"
          />
          {errors.name && <span className="error">{errors.name}</span>}
    
          {/* 其他欄位 */}
    
          <button type="submit">建立市場</button>
        </form>
      )
    }
    

    Error Boundary 模式

    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 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>
    

    動畫模式

    Framer Motion 動畫

    import { motion, AnimatePresence } from 'framer-motion'
    
    // PASS: 列表動畫
    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>
      )
    }
    
    // PASS: 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>
      )
    }
    

    無障礙模式

    鍵盤導航

    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 (
        <div
          role="combobox"
          aria-expanded={isOpen}
          aria-haspopup="listbox"
          onKeyDown={handleKeyDown}
        >
          {/* 下拉選單實作 */}
        </div>
      )
    }
    

    焦點管理

    export 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
    }
    

    記住:現代前端模式能實現可維護、高效能的使用者介面。選擇符合你專案複雜度的模式。

    Alternatives

    Compare before choosing