React 開發指南 - 避免常見錯誤
以下列出 hooks 使用上常見錯誤
useEffect
身為 React 開發者,你可能經常使用 useEffect 來處理各種邏輯。
它是一個 React Hook,讓你能夠使元件與外部系統同步,
但很多時候,我們其實不需要 Effect。過度使用 Effect 會讓程式碼變得複雜、效能低下,甚至容易出錯。
什麼時候你真的需要 Effect?
Effect 是 React 的「逃脫機制」(Escape hatch),主要用於與外部系統同步,例如:
- 與第三方 API 整合
- 操作 DOM
- 設置訂閱或計時器
- 與瀏覽器 API 互動
如果你的邏輯只涉及 props 或 state 的變化,那你可能不需要 Effect。
Escape hatch
代表 useEffect 讓元件能夠與 React 渲染流程之外的系統進行同步
常見的 Effect 濫用場景
1. 根據 props 或 state 來更新 state
❌ 避免這樣做:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null); // 會導致額外的重新渲染
}, [items]);
// 每當items發生變化時,List其子元件會先使用舊的selection值來渲染。
// 然後 React 會更新 DOM 並執行 Effect。最後,呼叫setSelection(null)將導致List其子元件重新渲染,重新啟動整個流程。
}
根據 props 或 state 來調整狀態都會使資料流更難以理解和調試
✅ 更好的做法:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ 非常好:在渲染期間計算所需內容
const selection = items.find((item) => item.id === selectedId) ?? null;
// ...
}
如果一個值可以基於現有的 props 或 state 計算得出,不要為了渲染使用 useEffect 轉換數據,而是在渲染期間直接計算這個值。
可以從 React 哲學 中了解什麼值應該作為狀態。
為什麼這樣更好?
- 避免多餘的重新渲染
- 程式碼更簡潔
- 減少出錯機會
2. 處理昂貴的計算
如果計算成本很高,使用 useMemo 而不是 useEffect:
✅ 正確的做法:
function TodoList({ todos, filter }) {
const visibleTodos = useMemo(() => {
return getFilteredTodos(todos, filter);
}, [todos, filter]);
return <ul>{/* 渲染 todos */}</ul>;
}
可以透過以下方式測量計算成本:
console.time("filter array");
const visibleTodos = filterTodos(todos, tab);
console.timeEnd("filter array");
如果記錄的總時間加起來相當可觀(例如,1ms 或更多),則可能需要記住該計算結果 - 官方建議
3. 重置元件狀態
❌ 避免在 Effect 中重置狀態:
function ProfilePage({ userId }) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment(""); // 會導致額外的重新渲染
}, [userId]);
}
✅ 使用 key 來重置元件:
function ProfilePage({ userId }) {
// ✅ 當 key 變化時,該元件的 comment 或其他 state 會自動重置
return <Profile key={userId} userId={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState(""); // 自動重置
}
4. 處理用戶事件
❌ 不要在 Effect 中處理用戶操作:
function ProductPage({ product }) {
useEffect(() => {
if (product.isInCart) {
showNotification("商品已加入購物車!");
}
}, [product]);
}
如果這個邏輯是由某些特定的事件引起的,則其會傳回在對應的事件處理函數中。
如果是由使用者在螢幕上元件看到時引起的,那麼它保留在 Effect 中。
✅ 在事件處理器中處理:
function ProductPage({ product, addToCart }) {
function handleAddToCart() {
addToCart(product);
showNotification("商品已加入購物車!");
}
return <button onClick={handleAddToCart}>加入購物車</button>;
}
5. 不理解 Effect 的「快照」特性
這是最容易被忽視但極其重要的概念:每次渲染都會創建全新的 Effect 函數,它們會「記住」當時的 props 和 state。
基本概念:每個渲染都有自己的一切
function Counter() {
const [count, setCount] = useState(0);
// 第一次渲染時:count = 0
// 第二次渲染時:count = 1
// 第三次渲染時:count = 2
// 每次都是不同的常數!
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
}); // ✅ 沒有依賴陣列,每次渲染都會執行
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
實驗: 快速點擊按鈕 3 次,然後等 3 秒,你會看到:
"You clicked 0 times" // 第一次渲染的 Effect
"You clicked 1 times" // 第二次渲染的 Effect
"You clicked 2 times" // 第三次渲染的 Effect
為什麼會這樣? 每次渲染實際上創建了不同的 Effect 函數:
// 第一次渲染
function Counter() {
const count = 0; // 來自 useState
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${0} times`); // 拿到 0
}, 3000);
});
}
// 第二次渲染
function Counter() {
const count = 1; // 來自 useState
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${1} times`); // 拿到 1
}, 3000);
});
}
若需要讀取最新值而不是快照值
記住: useEffect 內的所有值都是該次渲染的「快照」,這是 React Hooks 的設計特色,不是 bug!
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// 使用update function更新,不依賴當前值
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
}, []); // 空依賴陣列是安全的
}
useEffect 使用情境
記住,Effect 是 React 的「逃脫機制」,只有在需要「跳出 React」時才使用。
正確使用場景
- 連接外部系統
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
直接在 useEffect 中取得資料通常意味著無需預先載入或快取資料。
例如,如果元件卸載後又重新掛載,則必須重新取得資料。
需考慮
如果使用框架、Cleint side fetch ( React Query、useSWR ),可以使用其內建的資料擷取機制代替 useEffect
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
fetchUser(userId).then((data) => {
if (!ignore) {
setUser(data);
}
});
return () => {
ignore = true; // 可以防止快速切換時,舊請求的結果覆蓋新數據。
};
}, [userId]);
}
可參考 What are good alternatives to data fetching in Effects?
- 函數依賴處理 → 替換或補強現有的「useEffect 使用情境」部分
進階技巧: 處理 function 依賴
原則:函數也是數據流的一部分
方法一:移到 Effect 內部
function SearchResults() {
useEffect(() => {
// ✅ 函數在內部,清晰可見所有依賴
function getFetchUrl() {
return "https://api.example.com/search";
}
getFetchUrl(); // 使用函數
}, []);
}
方法二:提升到元件外部
// ✅ 純函數,不依賴元件狀態
function getFetchUrl(query) {
return `https://api.example.com/search?q=${query}`;
}
function SearchResults() {
useEffect(() => {
getFetchUrl("react"); // 安全使用
}, []);
}
方法三:useCallback 包裝
function SearchResults() {
const [query, setQuery] = useState("react");
const getFetchUrl = useCallback(() => {
return `https://api.example.com/search?q=${query}`;
}, [query]); // 明確聲明依賴
useEffect(() => {
getFetchUrl();
}, [getFetchUrl]);
}
檢查清單
在寫 useEffect 之前,問問自己:
- 這個邏輯是因為元件顯示而觸發的嗎? → 可能需要 useEffect
- 這個邏輯是因為用戶操作而觸發的嗎? → 應該放在事件處理器中
- 這個資料可以從現有的 props/state 計算得出嗎? → 不需要 useEffect
- 這個計算很昂貴嗎? → 使用
useMemo - 需要重置元件狀態嗎? → 考慮使用
key
詳細請參考 官方文件
useMemo
useMemo 是一個 React Hook,讓你在重新渲染之間快取計算結果。
雖然它看起來是個很棒的效能優化工具,但很多開發者會過度使用或在錯誤的場景中使用 useMemo,
反而可能導致程式碼複雜度增加,甚至效能下降。
常見的 useMemo 錯誤使用
1. 過度使用 useMemo - 快取不需要的計算
❌ 沒有必要的快取:
function UserProfile({ firstName, lastName }) {
// 🔴 完全不必要的 useMemo - 字串連接本身就很快
const fullName = useMemo(() => {
return firstName + " " + lastName;
}, [firstName, lastName]);
// 🔴 更不必要的快取 - 這種計算幾乎沒有成本
const displayName = useMemo(() => {
return fullName.toUpperCase();
}, [fullName]);
return <div>{displayName}</div>;
}
✅ 更好的做法:
function UserProfile({ firstName, lastName }) {
// ✅ 直接計算即可,非常輕量
const fullName = firstName + " " + lastName;
const displayName = fullName.toUpperCase();
return <div>{displayName}</div>;
}
為什麼不需要? 簡單的字串操作、數學運算、邏輯判斷等都是極輕量的操作,快取它們不會帶來效能提升,反而增加了記憶體開銷。
2. 忘記添加依賴陣列
❌ 缺少依賴陣列:
function ProductList({ products, filter }) {
// 🔴 沒有依賴陣列,每次都會重新計算
const filteredProducts = useMemo(() => {
return products.filter(
(product) =>
product.category === filter.category && product.price >= filter.minPrice
);
}); // 缺少依賴陣列
return <div>{/* 渲染產品 */}</div>;
}
✅ 正確的做法:
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
return products.filter(
(product) =>
product.category === filter.category && product.price >= filter.minPrice
);
}, [products, filter]); // ✅ 包含所有依賴
return <div>{/* 渲染產品 */}</div>;
}
3. 依賴包含每次都變化的物件
❌ 依賴不穩定的物件:
function SearchResults({ query, filters }) {
const searchOptions = {
// 🔴 每次渲染都是新物件
includeInactive: false,
sortBy: "relevance",
...filters, // 展開的物件也可能每次都不同
};
// 🔴 searchOptions 每次都不同,memo 失效
const results = useMemo(() => {
return searchItems(query, searchOptions);
}, [query, searchOptions]);
}
✅ 正確的做法:
function SearchResults({ query, filters }) {
const results = useMemo(() => {
// ✅ 在計算函數內部創建物件
const searchOptions = {
includeInactive: false,
sortBy: "relevance",
...filters,
};
return searchItems(query, searchOptions);
}, [query, filters]); // 只依賴真正變化的值
}
4. 使用 useMemo 快取函數
❌ 用 useMemo 包裝函數:
function ProductPage({ productId, referrer }) {
// 🔴 語法笨拙,有更好的方式
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
✅ 使用 useCallback:
function ProductPage({ productId, referrer }) {
// ✅ useCallback 專門用於快取函數
const handleSubmit = useCallback(
(orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
},
[productId, referrer]
);
return <Form onSubmit={handleSubmit} />;
}
5. 在迴圈中使用 useMemo
❌ 在 map 中調用 Hook:
function ReportList({ items }) {
return (
<article>
{items.map((item) => {
// 🔴 不能在迴圈中調用 useMemo
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure key={item.id}>
<Chart data={data} />
</figure>
);
})}
</article>
);
}
✅ 提取元件:
function ReportList({ items }) {
return (
<article>
{items.map((item) => (
<Report key={item.id} item={item} />
))}
</article>
);
}
function Report({ item }) {
// ✅ 在元件頂層調用 useMemo
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure>
<Chart data={data} />
</figure>
);
}
6. 錯誤的物件回傳語法
❌ 語法錯誤:
function SearchForm({ query }) {
// 🔴 箭頭函數語法錯誤,會回傳 undefined
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
caseSensitive: false,
query: query
}, [query]);
}
✅ 正確的語法:
function SearchForm({ query }) {
// ✅ 方法一:明確的 return 陳述
const searchOptions = useMemo(() => {
return {
matchMode: "whole-word",
caseSensitive: false,
query: query,
};
}, [query]);
// ✅ 方法二:用括號包圍物件
const searchOptions2 = useMemo(
() => ({
matchMode: "whole-word",
caseSensitive: false,
query: query,
}),
[query]
);
}
7. 誤用 useMemo 快取簡單的 JSX
❌ 快取不值得的 JSX:
function UserProfile({ user, theme }) {
const [notifications, setNotifications] = useState([]);
// 🔴 沒有必要快取簡單的 JSX
const userInfo = useMemo(
() => (
<div className={theme}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
),
[user, theme]
);
return (
<div>
{userInfo}
<NotificationList notifications={notifications} />
</div>
);
}
✅ 正確的語法:
function UserProfile({ user, theme }) {
const [notifications, setNotifications] = useState([]);
// ✅ 簡單的 JSX 直接創建即可
const userInfo = (
<div className={theme}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
return (
<div>
{userInfo}
<NotificationList notifications={notifications} />
</div>
);
}
為什麼不需要? 簡單的 JSX 創建成本極低,快取它們反而增加了記憶體開銷和程式碼複雜度。
何時值得快取 JSX?
- 元件渲染成本高(> 1-2ms)
- 測量方法:
console.time
- 測量方法:
- 當父元件有頻繁變化的狀態,但某些子元件不需要因此重新創建
useMemo 使用情境
記住:useMemo 只在少數情況下才真正有價值。
1. 跳過昂貴的重新計算
在 useMemo 中進行的計算速度明顯很慢,而且它的依賴關係很少改變。 故當計算成本確實很高時:
function DataProcessor({ dataset, filters }) {
// ✅ 處理大量數據時,值得使用 useMemo
const processedData = useMemo(() => {
console.time("process data"); // 測量執行時間
const result = dataset
.filter((item) => filters.categories.includes(item.category))
.map((item) => ({
...item,
score: calculateComplexScore(item), // 複雜計算
trends: analyzeHistoricalTrends(item.history), // 耗時分析
}))
.sort((a, b) => b.score - a.score);
console.timeEnd("process data"); // 如果超過 1ms,值得快取
return result;
}, [dataset, filters]);
return <DataVisualization data={processedData} />;
}
測量計算成本的方法:
// 測量特定計算的執行時間
console.time("expensive calculation");
const result = expensiveFunction(data);
console.timeEnd("expensive calculation");
// 如果記錄的時間超過 1ms,考慮使用 useMemo
2. 跳過子元件的重新渲染
配合 React.memo 避免不必要的重新渲染:
function TodoApp({ todos, filter, theme }) {
// ✅ 快取計算結果,避免子元件重新渲染
const visibleTodos = useMemo(() => {
return todos.filter((todo) => {
switch (filter) {
case "active":
return !todo.completed;
case "completed":
return todo.completed;
default:
return true;
}
});
}, [todos, filter]);
return (
<div className={theme}>
{/* TodoList 用 memo 包裝,可以受益於 useMemo */}
<TodoList todos={visibleTodos} />
</div>
);
}
const TodoList = memo(function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
});
記住 Kent C. Dodds 的建議:
不要在沒有測量的情況下使用 React.memo,這些優化有成本,你需要確保知道成本和收益,以判斷是否真的有幫助(而非有害)。
3. 防止 Effect 過度觸發
當物件作為 useEffect 依賴時:
Avoid unnecessary Effects that update state. Most performance problems in React apps are caused by chains of updates originating from Effects that cause your components to render over and over.
❌ 物件依賴導致 Effect 過度執行:
function ChatRoom({ roomId, serverUrl }) {
const [message, setMessage] = useState("");
const options = {
// 🔴 每次渲染都是新物件
serverUrl: serverUrl,
roomId: roomId,
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // 🔴 每次都會重新連接
}
問題: 輸入文字時,即使 roomId 沒變,options 物件每次都是新的,導致聊天室重新連接。
✅ 使用 useMemo 穩定物件引用:
function ChatRoom({ roomId, serverUrl }) {
const [message, setMessage] = useState("");
// ✅ 快取物件,只在依賴改變時創建新物件
const options = useMemo(
() => ({
serverUrl: serverUrl,
roomId: roomId,
}),
[serverUrl, roomId]
);
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ 只在必要時重新連接
}
更好的做法: 將物件移到 Effect 內部
function ChatRoom({ roomId, serverUrl }) {
const [message, setMessage] = useState("");
useEffect(() => {
// ✅ 最簡單的方式:不需要 useMemo
const options = {
serverUrl: serverUrl,
roomId: roomId,
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ 直接依賴原始值
}
4. 作為其他 Hook 的依賴
當快取的值被其他 Hook 使用時:
function SearchInterface({ query, options }) {
// ✅ 第一層:快取搜索參數
const searchParams = useMemo(
() => ({
query: query.trim().toLowerCase(),
includeArchived: options.includeArchived,
sortBy: options.sortBy || "relevance",
}),
[query, options.includeArchived, options.sortBy]
);
// ✅ 第二層:基於穩定的 searchParams 進行計算
const searchResults = useMemo(() => {
return performSearch(searchParams);
}, [searchParams]);
// ✅ 第三層:Effect 也依賴穩定的值
useEffect(() => {
logSearchEvent(searchParams);
}, [searchParams]);
return <SearchResults results={searchResults} />;
}
檢查清單:何時需要 useMemo?
在使用 useMemo 之前,問問自己:
- 這個計算真的很昂貴嗎?
- 用
console.time測量,如果 < 1ms 通常不需要 - 記住:用戶的設備可能比你的開發機器慢
- 用
- 計算結果會傳遞給用 memo 包裝的元件嗎?
- 是 → 可能需要 useMemo
- 否 → 通常不需要
- 計算結果是否作為其他 Hook 的依賴?
- 是 → 可能需要 useMemo
- 否 → 通常不需要
- 依賴陣列中的值是否經常改變?
- 經常改變 → useMemo 效果有限
- 很少改變 → useMemo 可能有用
是否需要 useMemo?
│
├─ 計算是否昂貴?(> 1ms)
│ ├─ 否 → ❌ 不需要 useMemo
│ └─ 是 → 繼續判斷
│
├─ 結果傳給 memo 包裝的元件?
│ ├─ 是 → ✅ 可能需要 useMemo
│ └─ 否 → 繼續判斷
│
├─ 作為其他 Hook 的依賴?
│ ├─ 是 → ✅ 可能需要 useMemo
│ └─ 否 → ❌ 不需要 useMemo
最佳實踐
1. 先測量,再優化
function ExpensiveComponent({ data }) {
// 測量實際執行時間
console.time("expensive-calculation");
const result = processLargeDataset(data);
console.timeEnd("expensive-calculation");
// 如果超過 1ms,再考慮使用 useMemo
const memoizedResult = useMemo(() => {
return processLargeDataset(data);
}, [data]);
}
2. 考慮替代方案
在使用 useMemo 之前,考慮這些更簡單的方案:
狀態下移:
// ❌ 所有狀態都在頂層
function App() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState("all");
const [theme, setTheme] = useState("light");
// 主題改變時,todos 計算也會重新執行
}
// ✅ 將不相關的狀態分離
function App() {
const [theme, setTheme] = useState("light");
return (
<div className={theme}>
<TodoApp /> {/* todos 和 filter 狀態在這裡 */}
</div>
);
}
將內容作為 children 傳遞:
// 利用之前提到的 children 優化技巧
function Layout({ children }) {
const [theme, setTheme] = useState("light");
return (
<div className={theme}>
{children} {/* 不會因為 theme 改變而重新渲染 */}
</div>
);
}
3. 避免過度記憶化
// 🔴 過度優化:每個小計算都用 useMemo
function OverOptimized({ user }) {
const firstName = useMemo(() => user.name.split(" ")[0], [user.name]);
const initials = useMemo(
() => `${firstName[0]}${user.surname[0]}`,
[firstName, user.surname]
);
const displayName = useMemo(
() => `${firstName} ${user.surname}`,
[firstName, user.surname]
);
// 這些計算都很輕量,不需要快取
}
// ✅ 保持簡單
function Optimized({ user }) {
const firstName = user.name.split(" ")[0];
const initials = `${firstName[0]}${user.surname[0]}`;
const displayName = `${firstName} ${user.surname}`;
// 簡單直接,更好維護
}
記住這個原則
useMemo 是效能優化,不是語義保證。
你的程式碼應該在沒有 useMemo 的情況下也能正常工作, 然後再添加 useMemo 來優化效能。
如果移除 useMemo 會破壞你的邏輯, 那問題出在你的程式碼邏輯上,而不是缺少記憶化。
詳細請參考 官方文件
useCallback
useCallback 是一個 React Hook,讓你在重新渲染之間快取函數定義。
但很多開發者會過度使用或誤用 useCallback,反而導致效能問題和程式碼複雜度增加。
常見的 useCallback 錯誤使用
1. 過度使用 useCallback
❌ 沒有必要的快取:
function MyComponent() {
const [count, setCount] = useState(0);
// 🔴 不必要的 useCallback - 沒有任何效能益處
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return <button onClick={handleClick}>Count: {count}</button>;
}
✅ 更好的做法:
function MyComponent() {
const [count, setCount] = useState(0);
// ✅ 直接定義函數即可
const handleClick = () => {
setCount((c) => c + 1);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
為什麼不需要? 因為沒有子元件接收這個函數,也沒有其他 Hook 依賴它。
2. 忘記添加依賴陣列
❌ 忘記依賴陣列:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
}); // 🔴 缺少依賴陣列,每次都會返回新函數
return <ShippingForm onSubmit={handleSubmit} />;
}
✅ 正確的做法:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback(
(orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
},
[productId, referrer]
); // ✅ 包含所有依賴
return <ShippingForm onSubmit={handleSubmit} />;
}
3. 錯誤的依賴管理
❌ 包含不必要的依賴:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback(
(text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]); // 依賴整個 todos 陣列
},
[todos]
); // 🔴 todos 改變時會重新創建函數
}
✅ 使用 update function 更新:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos((todos) => [...todos, newTodo]); // 使用update function更新
}, []); // ✅ 不需要依賴 todos
}
4. 在迴圈中使用 useCallback
❌ 在迴圈中調用 Hook:
function ReportList({ items }) {
return (
<article>
{items.map((item) => {
// 🔴 不能在迴圈中調用 useCallback
const handleClick = useCallback(() => {
sendReport(item);
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
✅ 提取元件:
function ReportList({ items }) {
return (
<article>
{items.map((item) => (
<Report key={item.id} item={item} />
))}
</article>
);
}
function Report({ item }) {
// ✅ 在元件頂層調用 useCallback
const handleClick = useCallback(() => {
sendReport(item);
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
5. 不理解何時真正需要 useCallback
❌ 誤解一:函數重新創建很昂貴
function MyComponent() {
const [count, setCount] = useState(0);
// 🤔 "這個函數每次都會重新創建,我需要快取它"
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return <button onClick={handleClick}>Click me</button>;
}
事實: 在 JavaScript 中創建函數是非常快的操作,重新創建函數本身幾乎沒有效能成本。
❌ 誤解二:所有傳遞給子元件的函數都需要快取
function Parent() {
const [count, setCount] = useState(0);
// 🔴 子元件沒有用 memo 包裝,這個快取沒有意義
const handleReset = useCallback(() => {
setCount(0);
}, []);
return <Child onReset={handleReset} count={count} />;
}
function Child({ onReset, count }) {
return (
<div>
<p>Count: {count}</p>
<button onClick={onReset}>Reset</button>
</div>
);
}
事實: 如果子元件沒有用 memo 包裝,useCallback 沒有任何效果。
✅ 只有在需要時才使用:
function Parent() {
const [count, setCount] = useState(0);
const handleReset = useCallback(() => {
setCount(0);
}, []);
return <Child onReset={handleReset} count={count} />;
}
// ✅ 用 memo 包裝才能受益於 useCallback
const Child = memo(function Child({ onReset, count }) {
return (
<div>
<p>Count: {count}</p>
<button onClick={onReset}>Reset</button>
</div>
);
});
❌ 誤解三:所有傳遞給子元件的函數都需要 useCallback
許多開發者認為只要函數傳遞給子元件就需要快取,但這是錯誤的理解:
function Parent() {
const [count, setCount] = useState(0);
// 🔴 以為傳給子元件就需要快取
const handleUpdate = useCallback((newValue) => {
console.log("Updated:", newValue);
}, []);
const handleReset = useCallback(() => {
setCount(0);
}, []);
return (
<div>
<RegularChild onUpdate={handleUpdate} />
<MemoizedChild onReset={handleReset} />
</div>
);
}
🔴 沒有用 memo 包裝,useCallback 沒有效果
關鍵原則:
- 只有當子元件用 React.memo 包裝時,父元件的 useCallback 才會產生實際效果。
- 沒有 memo 的子元件會在父元件每次重新渲染時都重新渲染,無論函數是否被快取。
function RegularChild({ onUpdate }) {
return <button onClick={() => onUpdate("test")}>Update</button>;
}
// ✅ 用 memo 包裝,useCallback 才有意義
const MemoizedChild = memo(function MemoizedChild({ onReset }) {
return <button onClick={onReset}>Reset</button>;
});
記住這個簡單的規則
只有在滿足以下條件之一時,useCallback 才有意義:
- 函數傳遞給用
memo包裝的元件- 函數作為其他 Hook(如 useEffect、useCallback)的依賴
- 函數在自定義 Hook 中返回給使用者
其他情況下,重新創建函數是完全正常和高效的!
useCallback 使用情境
緩存函數 useCallback 僅在少數情況下才有價值:
1. 跳過子元件的重新渲染
當你需要配合 React.memo 優化效能時:
function ProductPage({ productId, referrer, theme }) {
// ✅ 快取函數以避免不必要的重新渲染
const handleSubmit = useCallback(
(orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
},
[productId, referrer]
);
return (
<div className={theme}>
{/* ShippingForm 用 memo 包裝,可以受益於 useCallback */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// 元件實作...
});
2. 作為其他 Hook 的依賴
當函數被用作 useEffect 或其他 Hook 的依賴時:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState("");
// ✅ 避免 Effect 過度觸發
const createOptions = useCallback(() => {
return {
serverUrl: "https://localhost:1234",
roomId: roomId,
};
}, [roomId]);
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // createOptions 作為依賴
}
更好的做法: 將函數移到 Effect 內部
function ChatRoom({ roomId }) {
const [message, setMessage] = useState("");
useEffect(() => {
// ✅ 更簡單,不需要 useCallback
function createOptions() {
return {
serverUrl: "https://localhost:1234",
roomId: roomId,
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 只依賴 roomId
}
3. 優化自定義 Hook
如果你在寫自定義 Hook,建議包裝返回的函數:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
// ✅ 讓使用者可以優化他們的程式碼
const navigate = useCallback(
(url) => {
dispatch({ type: "navigate", url });
},
[dispatch]
);
const goBack = useCallback(() => {
dispatch({ type: "back" });
}, [dispatch]);
return {
navigate,
goBack,
};
}
4. 使用 updater function 更新減少依賴
在某些情況下,你可以通過 update function 更新來避免使用 useCallback:
❌ 依賴狀態值:
function TodoApp() {
const [todos, setTodos] = useState([]);
// 🔴 因為依賴 todos,每次 todos 改變都會重新創建函數
const addTodo = useCallback(
(text) => {
setTodos([...todos, { id: Date.now(), text }]);
},
[todos]
); // todos 經常改變,useCallback 效果有限
return <AddTodoForm onAdd={addTodo} />;
}
✅ 使用 updater function 更新:
function TodoApp() {
const [todos, setTodos] = useState([]);
// ✅ 使用 updater function更新,不需要依賴 todos
const addTodo = useCallback((text) => {
setTodos((prevTodos) => [...prevTodos, { id: Date.now(), text }]);
}, []); // 空依賴陣列,函數永遠不會重新創建
return <AddTodoForm onAdd={addTodo} />;
}
update function 更新的優勢:
- 減少依賴陣列中的項目
- 讓 useCallback 更有效果
- 避免因狀態頻繁更新導致的函數重新創建
可參考 官方文件
檢查清單:何時需要 useCallback?
在使用 useCallback 之前,問問自己:
- 這個函數是否傳遞給用 memo 包裝的元件? → 可能有用
- 這個函數是否作為其他 Hook 的依賴? → 可能有用
- 這個函數是否在每次渲染時都會重新創建? → 這是正常的,不一定需要快取
- 不使用 useCallback 是否真的有效能問題? → 先測量,再優化
記住: 大多數情況下,重新創建函數是沒有問題的。只有在確實需要優化時才使用 useCallback。
我需要 useCallback 嗎?
│
├─ 這個函數會傳給用 memo 包裝的子元件嗎?
│ ├─ 是 → 可能需要,繼續檢查
│ └─ 否 → 檢查其他條件
│
├─ 這個函數是其他 Hook 的依賴嗎?
│ ├─ 是 → 可能需要,繼續檢查
│ └─ 否 → 檢查其他條件
│
├─ 能用update function更新減少依賴嗎?
│ ├─ 是 → 優先使用update function更新
│ └─ 否 → 繼續檢查
│
├─ 這是自定義 Hook 返回的函數嗎?
│ ├─ 是 → 建議使用 useCallback
│ └─ 否 → 不需要 useCallback
│
└─ 最終檢查:移除 useCallback 會破壞功能嗎?
├─ 是 → 你的邏輯有問題,不是缺少 useCallback
└─ 否 → 那就不需要 useCallback
除了 useCallback 的其他優化策略
- 將狀態下移: 不要把所有狀態都放在頂層元件
- 將 JSX 作為 children 傳遞: 避免不必要的重新渲染
- 保持渲染邏輯純淨: 避免在渲染中產生副作用
- 移除不必要的 useEffect :
大多數效能問題來自 useEffect,與其使用記憶機制,不如將某個物件或函數移到 useEffect 內部或元件外部,這樣通常更簡單。
進階優化技巧
這是一個比 memo、useCallback、useMemo 更基礎但也更有效的優化技巧
1. 利用 children 避免不必要的重新渲染
核心原則:當元件在視覺上包裝其他元件時,讓它接受 JSX 作為 children。
這樣當包裝元件更新自己的狀態時,React 知道其子元件不需要重新渲染。
❌ 常見的錯誤寫法:
問題: 當 count 改變時,即使 Header、Sidebar 等元件與 count 無關,它們也會重新渲染。
function App() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState("light");
return (
<div className={theme}>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* 🔴 這些元件會在 count 改變時重新渲染 */}
<Header />
<Sidebar />
<ExpensiveChart data={someData} />
<Footer />
</div>
);
}
解決方案:將 JSX 作為 children 傳遞
✅ 優化後的寫法:
function App() {
return (
<Layout>
{/* ✅ 這些元件不會因為 Layout 內部狀態改變而重新渲染 */}
<Header />
<Sidebar />
<ExpensiveChart data={someData} />
<Footer />
</Layout>
);
}
function Layout({ children }) {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState("light");
return (
<div className={theme}>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* ✅ children 是已經創建好的 JSX,不會重新渲染 */}
{children}
</div>
);
}
為什麼這樣有效? 錯誤寫法中:
// 每次元件重新渲染時,都會創建新的 JSX 元素
return (
<div>
<Header /> // 新的 React 元素
<Sidebar /> // 新的 React 元素
<ExpensiveChart />
// 新的 React 元素
</div>
);
優化寫法中:
// children 在父元件中已經創建,傳遞給子元件
// 子元件重新渲染時,children 保持相同的 JSX 引用
// React 知道這些元件不需要重新渲染
{
children;
} // 同樣的 JSX 引用
When a component visually wraps other components, let it accept JSX as children. Then, if the wrapper component updates its own state, React knows that its children don’t need to re-render.
2. 進階檢查清單
渲染優化前先問自己: 記住這個順序:
渲染真的是問題嗎?
- 用 React DevTools Profiler 測量實際時間
- 開發模式下的數據不準確,要用 production build 測量
元件渲染的頻率如何?
- 高頻更新 → 考慮優化
- 低頻更新 → 優化可能不值得
渲染成本有多高?
- 簡單元件:優化意義不大
- 複雜計算/大列表:值得優化
有更簡單的解決方案嗎?
- 狀態下移
- 元件拆分
- children 模式
避免過早優化:
- 不要預設性地包裝所有元件
- 不要為了優化而犧牲程式碼可讀性
- 先讓功能正確,再考慮性能
內容整理自 @ React Docs beta
參考資料
- useEffect 參考文件
- useMemo 參考文件
- useCallback 參考文件
- You Might Not Need an Effect
- Synchronizing with Effects
- When to useMemo and useCallback
- Removing Effect Dependencies
- Lifecycle of Reactive Effects
- Separating Events from Effects
- React 文件
- A (Mostly) Complete Guide to React Rendering Behavior
- A deep dive into Cloudflare’s September 12, 2025 dashboard and API outage