React useContext

useContext 是一個讓你在元件中讀取 Context 的 Hook。Context 提供了一種在元件樹中共享資料的方式,而不需要透過 props 一層一層傳遞。

什麼時候需要 Context?

當你有一些資料需要被很多元件使用時,例如:

  • 當前的使用者資訊
  • 主題(深色/淺色模式)
  • 語言設定
  • 全域狀態

如果不用 Context,你需要透過 props 一層一層往下傳(prop drilling),這會讓程式碼變得冗長且難以維護。

基本用法

使用 Context 分為三個步驟:

1. 建立 Context

import { createContext } from 'react'

// 建立 Context,可以給一個預設值
const ThemeContext = createContext('light')

2. 提供 Context 值

在父元件中使用 Context 包裹子元件,並提供值:

function App() {
  const [theme, setTheme] = useState('dark')

  return (
    // 使用 Context 作為 Provider
    <ThemeContext value={theme}>
      <Header />
      <Main />
      <Footer />
    </ThemeContext>
  )
}
在 React 19 中,可以直接使用 <Context value={...}> 而不需要 <Context.Provider value={...}>

3. 使用 Context 值

在任何子元件中使用 useContext 讀取值:

import { useContext } from 'react'

function ThemedButton() {
  // 讀取 Context 值
  const theme = useContext(ThemeContext)

  return <button className={theme}>我是 {theme} 主題的按鈕</button>
}

完整範例:主題切換

import { createContext, useContext, useState } from 'react'

// 1. 建立 Context
const ThemeContext = createContext(null)

// 2. 建立 Provider 元件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  function toggleTheme() {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
  }

  return <ThemeContext value={{ theme, toggleTheme }}>{children}</ThemeContext>
}

// 3. 建立自訂 Hook(推薦)
function useTheme() {
  const context = useContext(ThemeContext)
  if (context === null) {
    throw new Error('useTheme 必須在 ThemeProvider 內使用')
  }
  return context
}

// 4. 使用 Context 的元件
function ThemedButton() {
  const { theme, toggleTheme } = useTheme()

  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        padding: '10px 20px',
        border: '1px solid #ccc',
      }}
    >
      切換主題(目前:{theme})
    </button>
  )
}

function Header() {
  const { theme } = useTheme()

  return (
    <header
      style={{
        backgroundColor: theme === 'light' ? '#f0f0f0' : '#222',
        color: theme === 'light' ? '#333' : '#fff',
        padding: '20px',
      }}
    >
      <h1>網站標題</h1>
      <ThemedButton />
    </header>
  )
}

// 5. App 元件
function App() {
  return (
    <ThemeProvider>
      <Header />
      <main>
        <p>主要內容</p>
        <ThemedButton />
      </main>
    </ThemeProvider>
  )
}

建立自訂 Hook 封裝 Context

為了更好的開發體驗,建議建立自訂 Hook 來封裝 Context 的使用:

// contexts/AuthContext.jsx
import { createContext, useContext, useState } from 'react'

const AuthContext = createContext(null)

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null)

  function login(userData) {
    setUser(userData)
  }

  function logout() {
    setUser(null)
  }

  return <AuthContext value={{ user, login, logout }}>{children}</AuthContext>
}

// 自訂 Hook
export function useAuth() {
  const context = useContext(AuthContext)
  if (context === null) {
    throw new Error('useAuth 必須在 AuthProvider 內使用')
  }
  return context
}

使用方式:

// App.jsx
import { AuthProvider } from './contexts/AuthContext'

function App() {
  return (
    <AuthProvider>
      <Router />
    </AuthProvider>
  )
}

// UserProfile.jsx
import { useAuth } from './contexts/AuthContext'

function UserProfile() {
  const { user, logout } = useAuth()

  if (!user) {
    return <p>請先登入</p>
  }

  return (
    <div>
      <p>歡迎,{user.name}!</p>
      <button onClick={logout}>登出</button>
    </div>
  )
}

多個 Context

你可以使用多個 Context 來管理不同類型的資料:

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          <MainContent />
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  )
}

在元件中使用:

function Dashboard() {
  const { user } = useAuth()
  const { theme } = useTheme()
  const { language } = useLanguage()

  return (
    <div className={theme}>
      <h1>
        {language === 'zh' ? '歡迎' : 'Welcome'}, {user.name}
      </h1>
    </div>
  )
}

Context 更新機制

當 Context 的值改變時,所有使用該 Context 的元件都會重新渲染。如果只是部分值改變,但你只用到其他值,元件仍然會重新渲染。

優化策略:拆分 Context

如果不同的值更新頻率不同,考慮拆分成多個 Context:

// ❌ 問題:theme 和 user 在同一個 Context
// 當 user 改變時,只使用 theme 的元件也會重新渲染
const AppContext = createContext({ theme: 'light', user: null })

// ✅ 解法:拆分成多個 Context
const ThemeContext = createContext('light')
const UserContext = createContext(null)

實際範例:購物車

import { createContext, useContext, useReducer } from 'react'

// 建立 Context
const CartContext = createContext(null)

// Reducer 處理購物車邏輯
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      const existingItem = state.find((item) => item.id === action.payload.id)
      if (existingItem) {
        return state.map((item) =>
          item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item
        )
      }
      return [...state, { ...action.payload, quantity: 1 }]

    case 'REMOVE_ITEM':
      return state.filter((item) => item.id !== action.payload)

    case 'CLEAR_CART':
      return []

    default:
      return state
  }
}

// Provider 元件
export function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, [])

  function addItem(item) {
    dispatch({ type: 'ADD_ITEM', payload: item })
  }

  function removeItem(id) {
    dispatch({ type: 'REMOVE_ITEM', payload: id })
  }

  function clearCart() {
    dispatch({ type: 'CLEAR_CART' })
  }

  const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0)

  return (
    <CartContext value={{ cart, addItem, removeItem, clearCart, total }}>{children}</CartContext>
  )
}

// 自訂 Hook
export function useCart() {
  const context = useContext(CartContext)
  if (context === null) {
    throw new Error('useCart 必須在 CartProvider 內使用')
  }
  return context
}

// 使用範例
function ProductCard({ product }) {
  const { addItem } = useCart()

  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addItem(product)}>加入購物車</button>
    </div>
  )
}

function CartSummary() {
  const { cart, total, removeItem, clearCart } = useCart()

  return (
    <div>
      <h2>購物車</h2>
      {cart.length === 0 ? (
        <p>購物車是空的</p>
      ) : (
        <>
          <ul>
            {cart.map((item) => (
              <li key={item.id}>
                {item.name} x {item.quantity} - ${item.price * item.quantity}
                <button onClick={() => removeItem(item.id)}>移除</button>
              </li>
            ))}
          </ul>
          <p>總計: ${total}</p>
          <button onClick={clearCart}>清空購物車</button>
        </>
      )}
    </div>
  )
}