affaan-m/ECC

frontend-patterns

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

64CollectingNetwork access
See how to use itView GitHub source
npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-TW/skills/frontend-patterns"
Automated source guideDocumentationDeep source

Source checked Jul 28, 2026·Refresh due Oct 26, 2026

Reorganized from the pinned upstream SKILL.md

Source-grounded documentation guide: frontend-patterns

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

npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-TW/skills/frontend-patterns"
Check the pinned source

The pinned source contains enough sections and task detail for a source-grounded deep guide; automated content is still not an independent test.

1,203 source words · 22 usable sections

Documentation workflow

Read frontend-patterns through these 5 source sections

Sections are extracted automatically from the pinned SKILL.md and link back to the source.

01

元件模式

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

SKILL.md · 元件模式
Review and apply the “元件模式” source section.
02

組合優於繼承

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

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

複合元件

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

SKILL.md · 複合元件
Review and apply the “複合元件” source section.
04

Render Props 模式

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

SKILL.md · Render Props 模式
Review and apply the “Render Props 模式” source section.
05

自訂 Hooks 模式

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

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

SkillSignal prompt templates

Provide the task, context, and acceptance criteria

These prompts were written by SkillSignal from the source structure; they are not upstream text.

Source-grounded prompt

Use for a documentation task while explicitly checking the source sections.

Use frontend-patterns for this documentation task: [task]. Inputs and constraints: [details]. Work through these pinned SKILL.md sections: “元件模式”, “組合優於繼承”, “複合元件”, “Render Props 模式”, “自訂 Hooks 模式”. Cite the concrete requirements that shape each step, do not invent capabilities absent from the source, and verify the result against: [acceptance criteria].

Documentation checklist

Verify each item before delivery

The source section “元件模式” has been checked.

The source section “組合優於繼承” has been checked.

The source section “複合元件” has been checked.

The source section “Render Props 模式” has been checked.

Static permission evidence

Inspect the exact source lines that triggered a signal

These are source excerpts matched by deterministic rules, not findings of malicious behavior, safety, or actual execution.

Choose a different workflow

When another Skill is the better fit

FAQ

What does the frontend-patterns source document cover?

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

How do I install frontend-patterns?

The source record exposes this install command: npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-TW/skills/frontend-patterns". Inspect the command and pinned source before running it.

Which permission-related actions were detected?

Static rules flagged network in the source; the page lists the matching lines and excerpts.

Repository stars
234,327
Repository forks
35,711
Quality
64/100
Source repository last pushed

Quality breakdown

Based on traceable docs and repository signals; stars are not treated as quality.

64/100
Documentation25/30
Specificity11/25
Maintenance18/20
Trust signals10/25
View original Skill.mdThis page is parsed directly from the repository SKILL.md without editorial rewriting. Collected: Jul 28, 2026 · about 1 min

前端開發模式

用於 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
}

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

Source repo
affaan-m/ECC
Skill path
docs/zh-TW/skills/frontend-patterns/SKILL.md
Commit SHA
4e973d3eaf92
Repository license
MIT
Data collected