react-use-callback
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact: useCallback Best Practices
React: useCallback 最佳实践
Core Principle
核心原则
useCallback caches a function definition between re-renders until its dependencies change.
Only use for specific performance optimizations - not by default.
useCallbackuseCallback会在组件重渲染之间缓存函数定义,直到其依赖项发生变化。
仅在特定的性能优化场景下使用——不要默认就使用它。
useCallbackWhen to Use useCallback
何时使用useCallback
1. Passing Callbacks to Memoized Children
1. 向记忆化子组件传递回调
When passing a function to a component wrapped in :
memo()jsx
import { useCallback, memo } from 'react';
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
// Expensive rendering logic
return <button onClick={onClick}>Click me</button>;
});
function Parent({ productId }) {
// Without useCallback, handleClick would be a new function every render
// causing ExpensiveChild to re-render unnecessarily
const handleClick = useCallback(() => {
console.log('Clicked:', productId);
}, [productId]);
return <ExpensiveChild onClick={handleClick} />;
}当向被包裹的组件传递函数时:
memo()jsx
import { useCallback, memo } from 'react';
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
// 开销较大的渲染逻辑
return <button onClick={onClick}>点击我</button>;
});
function Parent({ productId }) {
// 如果不使用useCallback,每次渲染时handleClick都会是一个新函数
// 导致ExpensiveChild不必要地重渲染
const handleClick = useCallback(() => {
console.log('已点击:', productId);
}, [productId]);
return <ExpensiveChild onClick={handleClick} />;
}2. Function as Effect Dependency
2. 函数作为Effect的依赖项
When a function is used inside :
useEffectjsx
function ChatRoom({ roomId }) {
const createOptions = useCallback(() => {
return { serverUrl: 'https://localhost:1234', roomId };
}, [roomId]);
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]);
}Better alternative: Move the function inside the effect:
jsx
function ChatRoom({ roomId }) {
useEffect(() => {
// Function defined inside effect - no useCallback needed
function createOptions() {
return { serverUrl: 'https://localhost:1234', roomId };
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}当函数被用在内部时:
useEffectjsx
function ChatRoom({ roomId }) {
const createOptions = useCallback(() => {
return { serverUrl: 'https://localhost:1234', roomId };
}, [roomId]);
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]);
}更好的替代方案: 将函数定义在effect内部:
jsx
function ChatRoom({ roomId }) {
useEffect(() => {
// 函数定义在effect内部——无需使用useCallback
function createOptions() {
return { serverUrl: 'https://localhost:1234', roomId };
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}3. Custom Hook Return Values
3. 自定义Hook的返回值
Always wrap functions returned from custom hooks:
jsx
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 };
}始终对自定义Hook返回的函数进行包裹:
jsx
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. Reducing State Dependencies
4. 减少状态依赖项
Use updater functions to eliminate state dependencies:
jsx
// Before: todos is a dependency
const handleAddTodo = useCallback((text) => {
setTodos([...todos, { id: nextId++, text }]);
}, [todos]);
// After: No todos dependency needed
const handleAddTodo = useCallback((text) => {
setTodos(todos => [...todos, { id: nextId++, text }]);
}, []);使用更新器函数来消除状态依赖:
jsx
// 之前:todos是依赖项
const handleAddTodo = useCallback((text) => {
setTodos([...todos, { id: nextId++, text }]);
}, [todos]);
// 之后:无需todos依赖项
const handleAddTodo = useCallback((text) => {
setTodos(todos => [...todos, { id: nextId++, text }]);
}, []);When NOT to Use useCallback
何时不使用useCallback
1. Child Is Not Memoized
1. 子组件未被记忆化
Without , provides no benefit:
memo()useCallbackjsx
// useCallback is pointless here
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// Child will re-render anyway when Parent re-renders
return <Child onClick={handleClick} />;
}如果没有,不会带来任何收益:
memo()useCallbackjsx
// 这里的useCallback毫无意义
function Parent() {
const handleClick = useCallback(() => {
console.log('已点击');
}, []);
// 当Parent重渲染时,Child无论如何都会重渲染
return <Child onClick={handleClick} />;
}2. Coarse Interactions
2. 粗粒度交互
Apps with page-level navigation don't benefit from memoization:
jsx
// Overkill for simple navigation
function App() {
const [page, setPage] = useState('home');
// Not needed - page transitions are inherently expensive anyway
const navigate = useCallback((page) => setPage(page), []);
return <Navigation onNavigate={navigate} />;
}带有页面级导航的应用无法从记忆化中获益:
jsx
// 简单导航场景下使用纯属多余
function App() {
const [page, setPage] = useState('home');
// 不需要——页面切换本身开销就很大
const navigate = useCallback((page) => setPage(page), []);
return <Navigation onNavigate={navigate} />;
}3. When Better Alternatives Exist
3. 存在更好的替代方案
Accept JSX as children:
jsx
// Instead of memoizing onClick
function Panel({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
// Children don't re-render when Panel's state changes
<Panel>
<ExpensiveComponent />
</Panel>Keep state local:
jsx
// Don't lift state higher than necessary
function SearchForm() {
// Local state doesn't trigger parent re-renders
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}接受JSX作为子元素:
jsx
// 不要去记忆化onClick
function Panel({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>切换</button>
{isOpen && children}
</div>
);
}
// 当Panel的状态变化时,子元素不会重渲染
<Panel>
<ExpensiveComponent />
</Panel>保持状态本地化:
jsx
// 不要不必要地提升状态
function SearchForm() {
// 本地状态不会触发父组件重渲染
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}Anti-Patterns to Avoid
需要避免的反模式
Missing Dependency Array
缺失依赖数组
jsx
// Returns a new function every render
const handleClick = useCallback(() => {
doSomething();
}); // Missing dependency array!
// Correct
const handleClick = useCallback(() => {
doSomething();
}, []);jsx
// 每次渲染都会返回一个新函数
const handleClick = useCallback(() => {
doSomething();
}); // 缺失依赖数组!
// 正确写法
const handleClick = useCallback(() => {
doSomething();
}, []);useCallback in Loops
在循环中使用useCallback
jsx
// Can't call hooks in loops
function List({ items }) {
return items.map(item => {
// WRONG
const handleClick = useCallback(() => sendReport(item), [item]);
return <Chart key={item.id} onClick={handleClick} />;
});
}
// Correct: Extract to component
function List({ items }) {
return items.map(item => (
<Report key={item.id} item={item} />
));
}
function Report({ item }) {
const handleClick = useCallback(() => sendReport(item), [item]);
return <Chart onClick={handleClick} />;
}
// Alternative: Wrap Report in memo instead
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return <Chart onClick={handleClick} />;
});jsx
// 不能在循环中调用Hook
function List({ items }) {
return items.map(item => {
// 错误写法
const handleClick = useCallback(() => sendReport(item), [item]);
return <Chart key={item.id} onClick={handleClick} />;
});
}
// 正确写法:提取为独立组件
function List({ items }) {
return items.map(item => (
<Report key={item.id} item={item} />
));
}
function Report({ item }) {
const handleClick = useCallback(() => sendReport(item), [item]);
return <Chart onClick={handleClick} />;
}
// 替代方案:用memo包裹Report
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return <Chart onClick={handleClick} />;
});useCallback vs useMemo
useCallback vs useMemo
| Hook | Caches | Use Case |
|---|---|---|
| The function itself | Callback props |
| Result of calling function | Computed values |
jsx
// Equivalent
const memoizedFn = useCallback(fn, deps);
const memoizedFn = useMemo(() => fn, deps);| Hook | 缓存内容 | 使用场景 |
|---|---|---|
| 函数本身 | 回调属性 |
| 函数调用的结果 | 计算值 |
jsx
// 等价写法
const memoizedFn = useCallback(fn, deps);
const memoizedFn = useMemo(() => fn, deps);Quick Reference
快速参考
DO
建议做
- Use with wrapped children
memo() - Use when function is an effect dependency
- Wrap custom hook return functions
- Use updater functions to reduce dependencies
- 与包裹的子组件配合使用
memo() - 当函数作为effect依赖项时使用
- 对自定义Hook返回的函数进行包裹
- 使用更新器函数来减少依赖项
DON'T
不建议做
- Add everywhere "just in case"
- Use without on child component
memo() - Use when you can restructure code instead
- Forget the dependency array
- 为了“以防万一”而到处添加
- 在子组件未被包裹时使用
memo() - 当可以通过重构代码来替代时使用
- 忘记添加依赖数组
Performance Debugging
性能调试
When memoization isn't working, debug dependencies:
jsx
const handleSubmit = useCallback((orderDetails) => {
// ...
}, [productId, referrer]);
console.log([productId, referrer]);Check in browser console:
js
Object.is(temp1[0], temp2[0]); // First dependency same?
Object.is(temp1[1], temp2[1]); // Second dependency same?当记忆化不起作用时,调试依赖项:
jsx
const handleSubmit = useCallback((orderDetails) => {
// ...
}, [productId, referrer]);
console.log([productId, referrer]);在浏览器控制台中检查:
js
Object.is(temp1[0], temp2[0]); // 第一个依赖项是否相同?
Object.is(temp1[1], temp2[1]); // 第二个依赖项是否相同?Future: React Compiler
未来方向:React Compiler
React Compiler automatically memoizes values and functions, reducing the need for manual calls. Consider using the compiler to handle memoization automatically.
useCallbackReact Compiler会自动记忆化值和函数,减少手动调用的需求。可以考虑使用该编译器来自动处理记忆化工作。
useCallback