react-use-callback

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React: useCallback Best Practices

React: useCallback 最佳实践

Core Principle

核心原则

useCallback caches a function definition between re-renders until its dependencies change.
Only use
useCallback
for specific performance optimizations - not by default.
useCallback会在组件重渲染之间缓存函数定义,直到其依赖项发生变化。
仅在特定的性能优化场景下使用
useCallback
——不要默认就使用它。

When 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
useEffect
:
jsx
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]);
}
当函数被用在
useEffect
内部时:
jsx
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
memo()
,
useCallback
provides no benefit:
jsx
// 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()
useCallback
不会带来任何收益:
jsx
// 这里的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

HookCachesUse Case
useCallback(fn, deps)
The function itselfCallback props
useMemo(() => fn, deps)
Result of calling functionComputed values
jsx
// Equivalent
const memoizedFn = useCallback(fn, deps);
const memoizedFn = useMemo(() => fn, deps);
Hook缓存内容使用场景
useCallback(fn, deps)
函数本身回调属性
useMemo(() => fn, deps)
函数调用的结果计算值
jsx
// 等价写法
const memoizedFn = useCallback(fn, deps);
const memoizedFn = useMemo(() => fn, deps);

Quick Reference

快速参考

DO

建议做

  • Use with
    memo()
    wrapped children
  • 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
    memo()
    on child component
  • 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
useCallback
calls. Consider using the compiler to handle memoization automatically.
React Compiler会自动记忆化值和函数,减少手动调用
useCallback
的需求。可以考虑使用该编译器来自动处理记忆化工作。

References

参考资料