You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中结合State与Ref使用useCallback的最佳实践疑问

关于useCallback依赖与ref替代方案的疑问与分析

问题背景

我在查阅useCallback相关资料时发现,所有文档都要求把用到的state放进依赖数组,这真的是必须的吗?如果是,那为什么不能在useCallback里直接用ref来规避依赖变化导致的函数引用更新?

根据React官方文档的说明:

使用useCallback需要传入两个参数:

  1. 你希望在重渲染之间缓存的函数定义。
  2. 包含组件内部所有在函数中用到的值的依赖列表。

从规范上我理解这个要求,但从性能角度看,这会导致依赖变化时函数引用更新,进而触发子组件不必要的重渲染,我是不是忽略了什么?


场景示例

基础场景:规范写法的性能问题

按照官方规范编写的组件:

function SomeComponent() {
    const [number, setNumber] = useState(0);
    const increment = useCallback(() => setNumber(number + 1), [number]);
    return <CostlyComponentToRender onClick={increment}>Increment</CostlyComponentToRender >
}

这里每次number变化时,increment的引用都会更新,导致CostlyComponentToRender及其子组件重渲染。我知道这个场景可以用函数式更新setNumber(p => p + 1)来避免依赖,但这不适用于我的实际业务场景。

自定义Hook解决方案

为了规避这个问题,我写了一个useRefState自定义Hook:

export default function useRefState<T>(initialValue: T | null): { state: T | null, setState: Dispatch<SetStateAction<T>>, ref: MutableRefObject<T> } {
    const [state, setState] = useState<T | any>(initialValue);
    const ref = useRef(state);
    useEffect(() => { ref.current = state }, [state]); // 或者直接赋值ref.current=state?哪种开销更小?
    return { state, setState, ref };
}

使用方式如下:

function SomeComponent() {
    const { setState: set, ref } = useRefState(0);
    const increment = useCallback(() => set(ref.current + 1), []);
    return <CostlyComponentToRender onClick={increment}>Increment</CostlyComponentToRender >
}

这样increment的引用永远不会变,子组件也就不需要重渲染,相比收益,这个Hook的开销可以忽略。

实际业务场景对比

规范写法版本:
父组件持有状态,子组件包含触发按钮:

const [macroType, setMacroType] = useState(MacroType.QUEUE);
const [selectedMacro, setSelectedMacro] = useState(/*some obj*/);
const onClickRun = useCallback(() => {
    switch (macroType) {
        case MacroType.QUEUE:
            prepareMacroExecution(selectedMacro);
            break;
        default:
            throw new Error("Unknown macro type");
    }
}, [macroType, selectedMacro]);

这里只要macroType或selectedMacro变化,onClickRun的引用就会更新,即便子组件用了memo,也得重渲染来更新函数引用。

使用ref的优化版本:

const {state:macroType, setState:setMacroType, ref:macroTypeRef} = useRefState(MacroType.QUEUE);
const {state:selectedMacro, setState:setSelectedMacro, ref:selectedMacroRef} = useRefState(/*some obj*/);
const onClickRun = useCallback(() => {
    const type = macroTypeRef.current;
    switch (type) {
        case MacroType.QUEUE:
            prepareMacroExecution(selectedMacroRef.current);
            break;
        default:
            throw new Error("Unknown macro type");
    }
}, []);

现在onClickRun的引用不会变,子组件无需重渲染,功能和之前一致。目前我没发现这个方案的问题,是不是需要权衡ref的开销和子组件重渲染的成本?


核心问题解答

1. state必须放入useCallback依赖吗?

是的,这是React的强制规范。如果不把用到的state放进依赖数组,不仅React的lint规则会报错,更会导致闭包陷阱:函数会捕获组件渲染时的state值,后续state更新后,函数里依然使用旧的state值,导致逻辑错误。

比如你忽略number依赖写useCallback(() => setNumber(number + 1), []),第一次点击会把number变成1,但之后点击都会把1+1变成2——因为函数里的number一直是初始的0,这就是闭包导致的典型问题。

2. 为什么官方不推荐直接用ref替代state依赖?

React的设计逻辑里,state是用于驱动视图更新的核心,而ref是用于访问DOM或保存不需要触发重渲染的临时值的。直接用ref读取state值,相当于绕过了React的状态更新机制,可能带来以下问题:

  • 一致性问题:如果在函数里读取ref.current时,组件还没完成重渲染(比如state刚更新,useEffect还没执行),ref.current可能还是旧值,导致逻辑错误。比如你的自定义Hook里用useEffect同步ref,在state更新后,useEffect是在组件渲染完成后执行的,如果在这期间调用函数,就会拿到旧的ref值。
  • 可维护性问题:这种写法打破了React的状态管理常规,其他开发者接手时可能会困惑,增加理解成本,也容易因为误用ref导致难以排查的bug。

3. 自定义useRefState Hook的优缺点

优点:

  • 确实能稳定函数引用,避免子组件不必要的重渲染,在子组件渲染成本极高的场景下,收益明显。
  • 实现简单,开销很小,大部分场景下性能影响可以忽略。

潜在风险:

  • 同步时机问题:用useEffect同步ref的话,state更新后,ref的更新会滞后于组件渲染。如果在state更新后立即调用函数(比如在setState的回调里调用),会拿到旧的ref值。解决这个问题的话,可以把ref.current = state直接放在渲染阶段(不需要useEffect),因为每次组件渲染时都会执行这行代码,这样ref.current会和state保持同步,且赋值ref是渲染阶段的安全操作。
  • 调试难度增加:因为函数依赖是空数组,React DevTools里无法直观看到函数依赖的状态变化,排查问题时需要额外关注ref的取值。
  • 违背React设计原则:这种写法相当于用ref存储状态的副本,绕开了React的依赖追踪机制,长期来看可能会导致代码变得难以维护。

总结

你的方案在特定场景下是可行的,但需要明确权衡:

  • 如果子组件的渲染成本极高,且状态变化频繁,导致函数引用频繁更新触发重渲染,那么这个方案的收益大于风险。
  • 如果子组件渲染成本不高,或者状态变化不频繁,建议遵循React的规范写法,避免引入额外的复杂度和潜在bug。

内容的提问来源于stack exchange,提问作者JWittmeyer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 10:20:15