React useMemo 與 useCallback
useMemo 和 useCallback 是 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,它會自動進行這些優化。在未來,你可能不需要手動使用 useMemo 和 useCallback。
但在 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>
)
}