Skip to main content

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」時才使用。

正確使用場景

  1. 連接外部系統
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();

return () => connection.disconnect();
}, [roomId]);
}
  1. 數據獲取

直接在 useEffect 中取得資料通常意味著無需預先載入或快取資料。
例如,如果元件卸載後又重新掛載,則必須重新取得資料。

需考慮

如果使用框架、Cleint side fetch ( React Query、useSWR ),可以使用其內建的資料擷取機制代替 useEffect

Race Conditions
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?

  1. 函數依賴處理 → 替換或補強現有的「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 之前,問問自己:

  1. 這個邏輯是因為元件顯示而觸發的嗎? → 可能需要 useEffect
  2. 這個邏輯是因為用戶操作而觸發的嗎? → 應該放在事件處理器中
  3. 這個資料可以從現有的 props/state 計算得出嗎? → 不需要 useEffect
  4. 這個計算很昂貴嗎? → 使用 useMemo
  5. 需要重置元件狀態嗎? → 考慮使用 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 之前,問問自己:

  1. 這個計算真的很昂貴嗎?
    • console.time 測量,如果 < 1ms 通常不需要
    • 記住:用戶的設備可能比你的開發機器慢
  2. 計算結果會傳遞給用 memo 包裝的元件嗎?
    • 是 → 可能需要 useMemo
    • 否 → 通常不需要
  3. 計算結果是否作為其他 Hook 的依賴?
    • 是 → 可能需要 useMemo
    • 否 → 通常不需要
  4. 依賴陣列中的值是否經常改變?
    • 經常改變 → 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

您是否應該在所有地方添加 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 才有意義:

  1. 函數傳遞給用 memo 包裝的元件
  2. 函數作為其他 Hook(如 useEffect、useCallback)的依賴
  3. 函數在自定義 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 之前,問問自己:

  1. 這個函數是否傳遞給用 memo 包裝的元件? → 可能有用
  2. 這個函數是否作為其他 Hook 的依賴? → 可能有用
  3. 這個函數是否在每次渲染時都會重新創建? → 這是正常的,不一定需要快取
  4. 不使用 useCallback 是否真的有效能問題? → 先測量,再優化

記住: 大多數情況下,重新創建函數是沒有問題的。只有在確實需要優化時才使用 useCallback。

我需要 useCallback 嗎?

├─ 這個函數會傳給用 memo 包裝的子元件嗎?
│ ├─ 是 → 可能需要,繼續檢查
│ └─ 否 → 檢查其他條件

├─ 這個函數是其他 Hook 的依賴嗎?
│ ├─ 是 → 可能需要,繼續檢查
│ └─ 否 → 檢查其他條件

├─ 能用update function更新減少依賴嗎?
│ ├─ 是 → 優先使用update function更新
│ └─ 否 → 繼續檢查

├─ 這是自定義 Hook 返回的函數嗎?
│ ├─ 是 → 建議使用 useCallback
│ └─ 否 → 不需要 useCallback

└─ 最終檢查:移除 useCallback 會破壞功能嗎?
├─ 是 → 你的邏輯有問題,不是缺少 useCallback
└─ 否 → 那就不需要 useCallback

除了 useCallback 的其他優化策略

  1. 將狀態下移: 不要把所有狀態都放在頂層元件
  2. 將 JSX 作為 children 傳遞: 避免不必要的重新渲染
  3. 保持渲染邏輯純淨: 避免在渲染中產生副作用
  4. 移除不必要的 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. 進階檢查清單

渲染優化前先問自己: 記住這個順序:

  1. 渲染真的是問題嗎?

    • 用 React DevTools Profiler 測量實際時間
    • 開發模式下的數據不準確,要用 production build 測量
  2. 元件渲染的頻率如何?

    • 高頻更新 → 考慮優化
    • 低頻更新 → 優化可能不值得
  3. 渲染成本有多高?

    • 簡單元件:優化意義不大
    • 複雜計算/大列表:值得優化
  4. 有更簡單的解決方案嗎?

    • 狀態下移
    • 元件拆分
    • children 模式

避免過早優化:

  • 不要預設性地包裝所有元件
  • 不要為了優化而犧牲程式碼可讀性
  • 先讓功能正確,再考慮性能

內容整理自 @ React Docs beta

參考資料

  1. useEffect 參考文件
  2. useMemo 參考文件
  3. useCallback 參考文件
  4. You Might Not Need an Effect
  5. Synchronizing with Effects
  6. When to useMemo and useCallback
  7. Removing Effect Dependencies
  8. Lifecycle of Reactive Effects
  9. Separating Events from Effects
  10. React 文件
  11. A (Mostly) Complete Guide to React Rendering Behavior
  12. A deep dive into Cloudflare’s September 12, 2025 dashboard and API outage