React useMemo 與 useCallback

useMemouseCallback 是 React 提供的兩個效能優化 Hook,用來避免不必要的計算和函式重新建立。

useMemo - 快取計算結果

useMemo 會快取計算結果,只有當依賴改變時才會重新計算。

基本語法

import { useMemo } from 'react'

const cachedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

使用情境:昂貴的計算

import { useState, useMemo } from 'react'

function FilteredList({ items, query }) {
  // 只有當 items 或 query 改變時才重新過濾
  const filteredItems = useMemo(() => {
    console.log('執行過濾')
    return items.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()))
  }, [items, query])

  return (
    <ul>
      {filteredItems.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

使用情境:避免子元件不必要的重新渲染

import { useState, useMemo, memo } from 'react'

// 使用 memo 包裝的子元件只有在 props 改變時才會重新渲染
const ExpensiveChild = memo(function ExpensiveChild({ data }) {
  console.log('ExpensiveChild 渲染')
  return <div>{JSON.stringify(data)}</div>
})

function Parent() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')

  // 使用 useMemo 確保 data 物件只在 name 改變時才重新建立
  const data = useMemo(() => ({ name }), [name])

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {/* 當 count 改變時,ExpensiveChild 不會重新渲染 */}
      <ExpensiveChild data={data} />
    </div>
  )
}

useCallback - 快取函式

useCallback 會快取函式本身,只有當依賴改變時才會建立新的函式。

基本語法

import { useCallback } from 'react'

const cachedFn = useCallback(() => {
  doSomething(a, b)
}, [a, b])

useCallback 等同於

// 這兩個是等價的
useCallback(fn, deps)
useMemo(() => fn, deps)

使用情境:傳遞給 memo 子元件的函式

import { useState, useCallback, memo } from 'react'

const Button = memo(function Button({ onClick, children }) {
  console.log('Button 渲染:', children)
  return <button onClick={onClick}>{children}</button>
})

function Parent() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('')

  // 不使用 useCallback,每次渲染都會建立新函式
  // const handleClick = () => setCount(c => c + 1);

  // 使用 useCallback,函式不會重新建立
  const handleClick = useCallback(() => {
    setCount((c) => c + 1)
  }, [])

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <p>Count: {count}</p>
      {/* 輸入文字時,Button 不會重新渲染 */}
      <Button onClick={handleClick}>+1</Button>
    </div>
  )
}

使用情境:作為 useEffect 的依賴

function SearchResults({ query }) {
  const [results, setResults] = useState([])

  // 使用 useCallback 確保 fetchData 只在 query 改變時重新建立
  const fetchData = useCallback(async () => {
    const response = await fetch(`/api/search?q=${query}`)
    const data = await response.json()
    setResults(data)
  }, [query])

  useEffect(() => {
    fetchData()
  }, [fetchData])

  return (
    <ul>
      {results.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

什麼時候不需要使用?

不需要 useMemo 的情況

// ❌ 不需要:簡單的計算
const double = useMemo(() => count * 2, [count])
// ✅ 直接計算即可
const double = count * 2

// ❌ 不需要:原始值不會造成子元件重新渲染的問題
const name = useMemo(() => firstName + ' ' + lastName, [firstName, lastName])
// ✅ 直接計算即可
const name = firstName + ' ' + lastName

不需要 useCallback 的情況

// ❌ 不需要:子元件沒有用 memo 包裝
const handleClick = useCallback(() => {
  setCount((c) => c + 1)
}, [])
// 子元件每次都會重新渲染,useCallback 沒有意義

// ❌ 不需要:函式沒有傳遞給子元件或作為依賴
const handleClick = useCallback(() => {
  console.log('clicked')
}, [])
// ✅ 直接定義即可
const handleClick = () => {
  console.log('clicked')
}

使用原則

先測量,再優化

不要預先優化。先讓程式正確運作,如果發現效能問題,再使用這些 Hook:

// 1. 先寫出正確的程式碼
function MyComponent({ items }) {
  const sorted = items.sort((a, b) => a.name.localeCompare(b.name))
  return <List items={sorted} />
}

// 2. 如果發現效能問題,再加上 useMemo
function MyComponent({ items }) {
  const sorted = useMemo(() => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items])
  return <List items={sorted} />
}

React Compiler 自動優化

React 團隊正在開發 React Compiler,它會自動進行這些優化。在未來,你可能不需要手動使用 useMemouseCallback

但在 Compiler 普及之前,了解這些 Hook 仍然很重要。

完整範例:搜尋與排序

import { useState, useMemo, useCallback, memo } from 'react'

// 商品卡片元件
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
  console.log('ProductCard 渲染:', product.name)

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

// 主元件
function ProductList({ products }) {
  const [query, setQuery] = useState('')
  const [sortBy, setSortBy] = useState('name')
  const [cart, setCart] = useState([])

  // 過濾和排序 - 昂貴的計算
  const filteredAndSortedProducts = useMemo(() => {
    console.log('過濾和排序')

    let result = products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))

    result.sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name)
      }
      return a.price - b.price
    })

    return result
  }, [products, query, sortBy])

  // 快取加入購物車函式
  const handleAddToCart = useCallback((product) => {
    setCart((prev) => [...prev, product])
  }, [])

  return (
    <div>
      <div className="controls">
        <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="搜尋商品..." />
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="name">依名稱排序</option>
          <option value="price">依價格排序</option>
        </select>
      </div>

      <div className="cart-summary">購物車: {cart.length} 件商品</div>

      <div className="product-grid">
        {filteredAndSortedProducts.map((product) => (
          <ProductCard key={product.id} product={product} onAddToCart={handleAddToCart} />
        ))}
      </div>
    </div>
  )
}