关于 react 中 useCallback 的使用问题?

一般情况下当函数作为依赖项时,应该使用 useCallback 来保证引用的稳定。例如组件的 props 或者是 useEffect 的依赖项。那假如函数被封装到一个 hook 中,那是都包一个 useCallback 好还是都不包,让使用者自已决定是否包?

阅读 258
avatarAI BotBETA

在自定义 Hook 中主动使用 useCallback 是更好的实践,但需要遵循特定规则:

  1. 封装原则:Hook 应该自包含地管理自己的依赖关系,返回稳定引用

    // 正确示例:在自定义 Hook 内部处理稳定性
    function useCustomHook() {
      const stableFn = useCallback(() => {
     // 逻辑代码
      }, [/* 正确依赖 */]);
      
      return stableFn;
    }
  2. 需要暴露依赖的场景
  3. 当 Hook 函数依赖外部参数时,应该将参数加入依赖数组
  4. 如果 Hook 内部操作依赖外部状态,需要明确要求使用者传递稳定值
  5. 例外情况
  6. 函数需要故意保持最新闭包时(如实时事件处理)
  7. 函数内部依赖频繁变化且无法通过依赖数组优化时
  8. 使用者注意事项

    // 错误用法:直接传递内联函数
    const unstableValue = { id: 42 };
    useCustomHook(unstableValue); // 会导致依赖数组失效
    
    // 正确用法:使用者主动稳定依赖
    const stableValue = useMemo(() => ({ id: 42 }), []);
    useCustomHook(stableValue);

最佳实践路径:

  1. 在自定义 Hook 内部对所有返回函数使用 useCallback
  2. 明确声明所有依赖项(使用 eslint-plugin-react-hooks 验证)
  3. 在文档中说明 Hook 的稳定性保证和依赖要求
  4. 如果 Hook 的参数可能是动态值,在文档中提示使用 useMemo/useCallback 的必要性

这种设计既保证了 Hook 的封装性,又通过明确的依赖声明让使用者能够正确配合使用。

3 个回答

建议采用两个版本的函数。

function useCustomHook() {
  // ...其他逻辑
  
  // 未记忆化版本
  const handleSomething = () => {
    // 函数逻辑
  };
  
  // 记忆化版本
  const handleSomethingCallback = useCallback(handleSomething, [/* 相关依赖 */]);
  
  return { 
    handleSomething,          // 普通版本
    handleSomethingCallback   // 记忆化版本
  };
}

或者通过选项参数提供灵活性:

function useCustomHook({ memoized = false } = {}) {
  // ...其他逻辑
  
  const handleSomething = () => {
    // 函数逻辑
  };
  
  return { 
    handleSomething: memoized 
      ? useCallback(handleSomething, [/* 相关依赖 */]) 
      : handleSomething
  };
}

这样可以在提供便利的同时保持灵活性,让使用者能根据自己的需求选择是否使用记忆化版本的函数。

如果你的 hook 返回的函数很可能被用作依赖项,那么默认使用 useCallback 是更友好的选择。

在hook 内用 useCallback

import { useState, useCallback } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);
  
  const decrement = useCallback(() => {
    setCount(prev => prev - 1);
  }, []);
  
  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);
  
  return {
    count,
    increment,
    decrement,
    reset
  };
}

页面里:

function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(0);
   
  useEffect(() => 
    console.log('Counter setup');
    
    return () => {
      // 清理逻辑
    };
  }, [increment]); // 依赖项
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
      <ChildComponent onIncrement={increment} />
    </div>
  );
}

不在 hook 内部用 useCallback

import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  // 不使用 useCallback
  const increment = () => {
    setCount(prev => prev + 1);
  };
  
  const decrement = () => {
    setCount(prev => prev - 1);
  };
  
  const reset = () => {
    setCount(initialValue);
  };
  
  return {
    count,
    increment,
    decrement,
    reset
  };
}

页面里:

function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(0);
  
  const stableIncrement = useCallback(increment, [increment]);
  const stableDecrement = useCallback(decrement, [decrement]);
  const stableReset = useCallback(reset, [reset]);
  
  useEffect(() => {
    console.log('Counter setup');
    //引用
  }, [stableIncrement]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
      
      <ChildComponent onIncrement={stableIncrement} />
    </div>
  );
}

最简单的做法:

  1. 如果函数的声明本身没什么开销,比如你定义一个没有依赖的函数,根据传入的参数计算得到结果。那么你就不需要用 useCallback,每次都声明就好,函数声明的开销其实很低。
  2. 如果函数里要用当前 hook 的状态,但都是不变的,或者 useRef,那么也不需要。
  3. 只有当你的 hook 会被多次调用(即调用 hook 的组件会被有状态更新),导致内部依赖不稳定时,才需要用到 useCallback
  4. 这个原则,可以用 JS 闭包来推导出来。即每次组件状态变化,都会导致渲染函数被执行,继而导致内部状态被新变量重新引用,于是跟变量相关的函数都需要被重新声明。
  5. 我认为理解最后这点最为重要,原理很简单,但理解了原理,才能作出推论。
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏