Ref 和 forwardRef

React 里的"逃生舱",直接操作 DOM

先说结论:Ref 就是绕开 React 的声明式数据流,直接拿到 DOM 节点或组件实例。

Ref 是什么?

正常情况 React 通过 props 控制 UI,Ref 让你可以直接访问底层 DOM:

function MyInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus(); // 直接调用 DOM 方法
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>聚焦</button>
    </>
  );
}

forwardRef 到底干了什么?

为什么函数组件不能直接用 ref?

首先得搞懂 ref 的本质:ref 是一个"容器",专门用来存 DOM 节点或组件实例的引用

类组件可以有实例(因为是 new ClassName() 创建的),但函数组件只是一个函数执行过程,没有 this,没有实例。

// ❌ 函数组件没有实例,所以不能直接加 ref
function MyInput() {
  return <input />;
}
<MyInput ref={inputRef} />; // 报错!

React 会报错:Function components cannot be given refs

forwardRef 做了什么?

forwardRef 做的事情很简单:它让函数组件能接收到从父组件传进来的 ref,然后转发给内部的 DOM 节点。

const MyInput = forwardRef((props, ref) => {
  // 这里有两个参数:
  // props - 正常的 props
  // ref - 父组件传进来的 ref

  return <input ref={ref} />;
});

<MyInput ref={inputRef} />;

底层原理:

  1. 父组件 <MyInput ref={inputRef} /> 传了一个 ref 进来
  2. forwardRef 把这个 ref 作为第二个参数传递给函数组件
  3. 函数组件内部把这个 ref 绑定到 <input ref={ref} />
  4. 最终 inputRef.current 指向了这个 input DOM 节点

说白了就是一层转发:父组件的 ref -> forwardRef 包装层 -> 传给内部 DOM。

useImperativeHandle 干了什么?

有时候我们不想把整个 DOM 暴露出去,只想让父组件调用几个特定方法:

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

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    reset: () => (inputRef.current.value = ""),
  }));

  return <input ref={inputRef} />;
});

底层原理:

  1. useImperativeHandle 替换了原来的 ref 对象
  2. 父组件拿到的 ref.current 不再是 DOM 节点,而是你返回的这个对象
  3. 只能调用你暴露的方法,访问不到真实的 DOM

为什么这么做?

  • 安全:不让父组件随意修改 DOM 的样式
  • 封装:只暴露必要的接口,隐藏内部实现

createRef vs useRef

  • createRef:每次渲染返回新对象,存不住数据(类组件用)
  • useRef:整个生命周期保持同一引用,像类组件的 this.xxx
// ❌ createRef 在函数组件里每次都是新的
const ref = createRef();

// ✅ useRef 保持同一个引用
const ref = useRef(null);

总结

概念作用
ref存 DOM 节点或组件实例的引用
forwardRef让函数组件能接收并转发 ref
useImperativeHandle替换 ref 为自定义对象,只暴露特定方法
useRef在函数组件中保持引用持久化

记住:Ref 是"逃生舱",能用 props 解决的就别用 Ref。