react hooks useRef useImperativeHandle

41 阅读4分钟

useRef / useImperativeHandle:
逃离 render 的状态、打破单向数据流的“合法后门”


核心心智模型

useRef 存的是“不参与渲染的可变引用”

这句话贯穿全文。


文章总览结构

useRef:非渲染状态的容器
useImperativeHandle:子组件主动暴露能力
它们解决的不是“数据管理”
而是:**跨 render 的引用、命令式控制**

1️. 一个你一定写过的“反直觉 Bug”

场景:计数器 + 打印最新值

import Content from "@/layout/Content";
import { Button, Input } from "antd";
import { useEffect, useRef, useState } from "react";

export default function UseRefDemo() {
  const [count, setCount] = useState(0);
  //const countRef = useRef(count);

  const logCount = () => {
    console.log(count);
  };

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);
      //console.log(countRef.current);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return (
    <Content>
      <div>count = {count}</div>
      <div className="m-[10px]">
        <Button
          type="primary"
          onClick={() => {
            setCount((c) => c + 1);
            //countRef.current = count + 1;
          }}
        >
          +1
        </Button>
      </div>
      <div className="m-[10px]">
        <Input
          placeholder="render测试"
          value={count}
          width={200}
          className="w-[200px]!"
        />
      </div>
      <div>
        <Button type="primary" onClick={logCount}>
          log
        </Button>
      </div>
    </Content>
  );
}

现象

  • useEffect 里面得定时器永远打印得是初始值0,原因也很简单,因为useEffect并没有render
  • [count],加一个依赖就可以解决这个问题

image.png

我把代码改为

   console.log("Effect 执行,依赖 count =", count);
   const timer = setInterval(() => {
     console.log(count);
     //   console.log(countRef.current);
   }, 1000);

   return () => clearInterval(timer);
 }, [count]);

然后观察发现

  • 打印
    Effect 执行,依赖 count = 1 index.tsx:14
    Effect 执行,依赖 count = 2 3index.tsx:16 2,
  • useeffect里面得所有逻辑都会执行,因为[count]依赖变化

抛出我的问题,我现在不想useEffect里面得逻辑随着[count]依赖值变化,然后我想打印,拿到最新值,what can i do ?


2️.useRef:不参与渲染的“外挂状态”

useRef 是什么?

const ref = useRef(initialValue);
  • ref 变了 → 组件不会 render
  • render 多少次 → ref 永远是同一个对象
  • useRef 的值变化不参与 React 的更新(render)调度,但它的创建和销毁仍受 React 生命周期管理。跳出三界之外,在五行之中😁

用 useRef 修复问题


export default function UseRefDemo() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);


  useEffect(() => {
    console.log("Effect 执行,依赖 count =", count);
    const timer = setInterval(() => {
      //   console.log(count);
      console.log(countRef.current);
    }, 1000);

    return () => clearInterval(timer);
  }, []);
}

发生了什么?

  • state 驱动 render
  • console.log("Effect 执行,依赖 count =", count)并没有被打印,ref 保存“最新值”,我们只是拿到了最新引用。
  • 读 ref = 读真实状态

⚠️ ref 是绕过 render 的通道

image.png


3️. useRef 的三种“正经用途”

① 访问 DOM

const inputRef = useRef<HTMLInputElement>(null);

<input ref={inputRef} />

inputRef.current?.focus();

② 保存定时器 / WebSocket / 实例

const timerRef = useRef<number | null>(null);

避免:

  • 多次 render 创建多个实例
  • cleanup 找不到旧引用

③ 保存“上一次的值”

const prevCount = useRef(count);

useEffect(() => {
  prevCount.current = count;
});

4️. useRef vs useState

对比项useStateuseRef
变更是否 render
是否参与视图
跨 render 持久化
是否触发更新链
  • ref.current 的变化
    不会触发 render

  • React 不会“追踪” ref 的变化
    不会进入调度 / diff / commit

  • 所以 ref 适合存放不用于驱动 UI 的数据


5️. useImperativeHandle:官方允许的“命令式后门”

问题场景

父组件想:

  • focus 子组件 input
  • 调用子组件方法

但不想通过 props 一路传


错误做法

把 DOM ref 直接暴露给父组件
强耦合 + 泄露实现


6️. useImperativeHandle 正确用法

子组件

import { forwardRef, useImperativeHandle, useRef } from "react";

const Child = forwardRef((props, ref) => { const inputRef = useRef(null);

useImperativeHandle(ref, () => ({ focus() { inputRef.current?.focus(); }, hello() { if (inputRef.current) { inputRef.current.value = "hello"; } }, })); console.log(inputRef.current, "value");

return ( ); });

export default Child;


父组件

import Content from "@/layout/Content";
import { useRef } from "react";
import Child from "./components";

export default function UseImperativeHandleDemo() {
  const childRef = useRef<{ focus: () => void; hello: () => void }>(null);
  console.log(childRef.current, "childRef.current");
  return (
    <Content>
      <Child ref={childRef} />
      <button
        className="border p-2 block mt-4 bg-blue-500 text-white rounded-md"
        onClick={() => childRef.current?.focus()}
      >
        focus child
      </button>

      <button
        className="border p-2 block mt-4 bg-blue-500 text-white rounded-md"
        onClick={() => childRef.current?.hello()}
      >
        hello child
      </button>
    </Content>
  );
}


本质

useImperativeHandle = 控制 ref 暴露的能力边界

不是“暴露全部”,而是:

useImperativeHandle(ref, () => ({ focus() { inputRef.current?.focus(); }, hello() { if (inputRef.current) { inputRef.current.value = "hello"; } }, }));\

  • 子组件通过useImperativeHandle暴露出自身方法,同时子组件绑定ref( ref={inputRef})
  • 1.const childRef = useRef<{ focus: () => void; hello: () => void }>(null);
    2.onClick={() => childRef.current?.hello()}
    父组件通过ref调用子组件方法,而无需关注子组件具体代码实现

image.png


7️. useRef + useCallback:逃离闭包的组合拳

const latestValue = useRef(value);

useEffect(() => {
  latestValue.current = value;
}, [value]);

const handler = useCallback(() => {
  doSomething(latestValue.current);
}, []);

ref 保存最新值
callback 永远稳定

🔥 实战中非常常见


8️. 什么时候不该用?

  • 想靠 ref 驱动 UI
  • 用 ref 替代 state
  • 用 ref 逃避数据流设计

⚠️ ref 是后门,不是主干道


9️. 最终总结

  • useRef 是 不参与 render 的可变容器
  • state 管 UI,ref 管“过程”
  • useImperativeHandle 是 受控的命令式暴露
  • ref 是 React 对“命令式编程”的妥协

🔚 面试必杀句(建议原封不动)

useRef 为什么修改不触发 render?

因为 ref 存的是可变引用,不参与 React 的更新调度,它的存在就是为了绕开 render。