Sitemap
jimmy.dev

jimmy.dev

Keep going !

React — UseCallback

6 min readMay 5, 2025

--

按照慣例,讓我們先看沒使用 UseCallback 導致的悲慘事件

TodoItem.tsx

import { memo } from "react";

interface TodoItemProps {
id: number;
text: string;
onDelete: (id: number) => void;
}

// 使用 memo 記憶化元件,只有當 props 改變時才會重新渲染
const TodoItem = memo(({ id, text, onDelete }: TodoItemProps) => {
console.log(`TodoItem ${id} 重新渲染了`);

return (
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow mb-2">
<span className="text-gray-700">{text}</span>
<button
onClick={() => onDelete(id)}
className="px-3 py-1 text-sm text-red-600 hover:bg-red-100 rounded"
>
刪除
</button>
</div>
);
});

TodoItem.displayName = "TodoItem";

export default TodoItem;

Todo.tsx

import { useState } from "react";
import TodoItem from "./TodoItem";

interface Todo {
id: number;
text: string;
}

export default function Todo() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: "學習 React" },
{ id: 2, text: "學習 TypeScript" },
{ id: 3, text: "學習 Tailwind CSS" },
]);

// 這個函數在每次組件重新渲染時都會創建新的實例
const handleDelete = (id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
};

return (
<div className="max-w-md mx-auto mt-8 p-4">
<div className="mb-8 text-center">
<p className="text-xl mb-4">計數器: {count}</p>
<button
onClick={() => setCount((c) => c + 1)}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
增加計數
</button>
</div>

<div className="space-y-4">
{todos.map((todo) => (
<TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
onDelete={handleDelete}
/>
))}
</div>
</div>
);
}

雖然增加計數的功能是跟 todo list 無關的,但每次 todo items 還是重新渲染

Get jimmy.dev’s stories in your inbox

Join Medium for free to get updates from this writer.

要修正這問題,就要用 useCallback 包住 handleDelete

使用 useCallback 的 Todo.tsx

import { useState, useCallback } from "react";
import TodoItem from "./TodoItem";

interface Todo {
id: number;
text: string;
}

export default function Todo() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: "學習 React" },
{ id: 2, text: "學習 TypeScript" },
{ id: 3, text: "學習 Tailwind CSS" },
]);

// 使用 useCallback 記憶化函數,只有當 todos 改變時才會重新創建
const handleDelete = useCallback(
(id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos]
);

return (
<div className="max-w-md mx-auto mt-8 p-4">
<div className="mb-8 text-center">
<p className="text-xl mb-4">計數器: {count}</p>
<button
onClick={() => setCount((c) => c + 1)}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
增加計數
</button>
</div>

<div className="space-y-4">
{todos.map((todo) => (
<TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
onDelete={handleDelete}
/>
))}
</div>
</div>
);
}

使用時機

  1. 當函數作為 props 傳給 memo 組件時
  2. 當函數被用在其他 hooks 的依賴陣列中時

最佳實踐 — 上述例子還能再這樣優化

// 好的寫法:使用函數式更新,不需要依賴外部變數
const handleDelete = useCallback((id: number) => {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
}, []); // 空依賴陣列


// 避免的寫法:直接依賴外部變數
const handleDelete = useCallback((id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
}, [todos]); // 需要依賴 todos

--

--

jimmy.dev
jimmy.dev

Written by jimmy.dev

0 followers

Keep going !