目 录CONTENT

文章目录

React(十四)Ref 完全指南

React(十四)Ref 完全指南

目标:彻底搞懂 React 19 中 Ref 是什么、怎么用、什么时候用、容易踩哪些坑。
本文面向课堂教学,从生活案例到代码示例层层递进,可直接作为 React 课程的配套资料。


目录

  1. Ref 是什么?一句话说清
  2. 生活案例:Ref 像不像你的"遥控器"
  3. Ref 与 state 的核心差异
  4. Ref 的四种使用姿势
    • 4.1 字符串 Ref(已废弃,了解即可)
    • 4.2 回调函数 Ref
    • 4.3 React.createRef()(类组件首选)
    • 4.4 useRef()(函数组件首选)
  5. useRef 进阶:保存跨渲染的可变变量
  6. forwardRef:让父组件拿到子组件的 DOM
  7. useImperativeHandle:只暴露你想暴露的
  8. 实战:循环渲染中给每个元素绑定独立 Ref
  9. Ref 使用注意事项与避坑指南
  10. 课堂思考题

1. Ref 是什么?一句话说清

在 React 中,组件通过 propsstate 来管理数据与 UI。但有些时候,你确实需要直接接触"幕后"的东西:

  • 一个输入框,想让页面一加载就自动聚焦;
  • 一个视频播放器,想点击按钮播放/暂停;
  • 想知道某个元素当前有多高、多宽;
  • 想记住某个定时器 ID,方便后面清除;
  • 想知道上一次渲染时的 props 是什么。

这些场景的共同点是:它们都不需要让组件重新渲染,但需要保留对某个"东西"的引用

Ref 就是 React 给你的一把"钥匙":用来直接访问 DOM 元素、类组件实例,或者保存一个跨渲染周期都不变的可变值。


2. 生活案例:Ref 像不像你的"遥控器"?

想象你家里有一台电视:

  • state 像电视的"当前状态":开关、音量、频道。状态变了,屏幕画面会重新显示(重新渲染)。
  • Ref 像电视"遥控器":它本身不会改变电视节目,但它让你可以直接操作电视——按开机、调音量、暂停播放。

你不需要让"遥控器本身"也变成屏幕上的画面,但你需要一直拿着它。这就是 Ref 的核心直觉:

Ref 不触发渲染,但能在需要时帮你"够到"真正要操作的对象。


3. Ref 与 state 的核心差异

特性 state ref
是否会触发重新渲染 ✅ 会 ❌ 不会
用途 保存影响 UI 的数据 保存 DOM 引用、类实例、跨渲染的可变值
读取方式 直接读取当前值 通过 ref.current 读取
典型使用场景 表单内容、计数器、列表数据 聚焦、媒体播放、获取元素尺寸、保存定时器 ID
在函数组件中 useState useRef
在类组件中 this.state this.属性React.createRef()

关键结论:如果你修改某个值后希望界面立刻更新,用 state;如果你只是需要"幕后保留一个值"或"操作 DOM",用 ref


4. Ref 的四种使用姿势

React 历史上出现过几种创建 Ref 的方式,下面按"从旧到新"的顺序讲解,方便你读懂老代码,也写对新代码。

4.1 字符串 Ref(已废弃,了解即可)

基础用法

在非常老的 React 代码中,你可能会看到这种写法:

import React, { Component } from "react";

class OldWay extends Component {
  componentDidMount() {
    // 通过 this.refs.名字 拿到 DOM 节点
    this.refs.myInput.focus();
  }

  render() {
    return (
      <div>
        {/* 字符串 ref:给 ref 属性传入一个字符串 */}
        <input type="text" ref="myInput" />
        <button onClick={() => this.refs.myInput.focus()}>聚焦输入框</button>
      </div>
    );
  }
}

export default OldWay;

代码拆解

代码 含义
ref="myInput" 给这个 input 起名叫 myInput
this.refs.myInput 在整个组件中通过这个名字拿到真实的 DOM 节点
this.refs.myInput.focus() 调用浏览器原生方法,让输入框获得焦点

为什么已废弃?

  • 字符串 Ref 依赖 this.refs 这个对象,内部实现复杂,难以优化;
  • 在函数组件中无法优雅使用;
  • 不支持多个组件共享同一个 Ref 对象,扩展性差。

⚠️ 官方状态:字符串 Ref 在 React 16.3 起已经被标记为遗留 API(legacy),不推荐在新代码中使用。课程中的老项目可能还能跑,但新项目请用 createRefuseRef


4.2 回调函数 Ref

什么是回调函数 Ref?

ref 属性传入一个函数,React 会在合适的时机把这个 DOM 节点作为参数传给你。

import React, { Component } from "react";

class CallbackRefDemo extends Component {
  constructor(props) {
    super(props);
    // 先声明一个实例属性,用来保存 DOM 引用
    this.inputNode = null;
  }

  render() {
    return (
      <div>
        {/*
          回调函数 Ref:
          React 在挂载时把 DOM 节点传进来,
          我们在函数内部把它保存到 this.inputNode。
        */}
        <input
          type="text"
          ref={(node) => {
            this.inputNode = node;
          }}
        />
        <button onClick={() => this.inputNode && this.inputNode.focus()}>
          聚焦
        </button>
      </div>
    );
  }
}

export default CallbackRefDemo;

React 什么时候调用这个回调函数?

时机 传入的参数
组件挂载完成 真实的 DOM 节点
组件卸载前 null
ref 所绑定的节点类型切换(比如从 <input> 变成 <div> 先传 null,再传新节点

这个回调函数会执行两次的场景通常出现在 React 的 StrictMode 下,它会故意挂载-卸载一次来检测副作用。如果你在回调函数里有"只能执行一次"的副作用,要特别注意。

课堂思考题 1

如果你把回调函数 Ref 写成 ref={(node) => { this.inputNode = node; console.log('绑定了') }},当组件被卸载时,控制台会打印什么?为什么?


4.3 React.createRef()(类组件首选)

为什么用它?

类组件不适合用 useRef,所以 React 提供了 React.createRef(),在构造函数里创建,在 JSX 中绑定,整个组件通过 this.xxxRef.current 访问。

import React, { Component } from "react";

class CreateRefDemo extends Component {
  constructor(props) {
    super(props);
    // 1. 在构造函数中创建 Ref 对象
    this.inputRef = React.createRef();
    // 也可以创建多个 Ref
    this.boxRef = React.createRef();
  }

  componentDidMount() {
    // 3. 在挂载完成后通过 current 读取 DOM
    console.log("input DOM:", this.inputRef.current);
    console.log("box 尺寸:", this.boxRef.current?.getBoundingClientRect());
  }

  handleFocus = () => {
    // 通过 current 调用原生方法
    this.inputRef.current.focus();
  };

  render() {
    return (
      <div>
        {/* 2. 通过 ref 属性绑定 */}
        <input type="text" ref={this.inputRef} />
        <div
          ref={this.boxRef}
          style={{ width: 100, height: 50, background: "skyblue" }}
        >
          我是一个盒子
        </div>
        <button onClick={this.handleFocus}>聚焦输入框</button>
      </div>
    );
  }
}

export default CreateRefDemo;

代码流程图

构造函数 React.createRef() 创建 Ref
        ↓
render() 中把 ref 绑定到 JSX 元素
        ↓
React 把真实 DOM 放到 ref.current
        ↓
通过 this.inputRef.current 操作 DOM

类组件 Ref 还可以指向组件实例

如果把 ref 绑定在自定义类组件上,ref.current 拿到的是这个类组件的实例,而不是它的 DOM。

import React, { Component } from "react";

// 子类组件
class ChildCounter extends Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  getCount = () => this.state.count;

  render() {
    return <div>子组件计数:{this.state.count}</div>;
  }
}

// 父类组件
class Parent extends Component {
  childRef = React.createRef();

  handleClick = () => {
    // 通过 ref.current 直接调用子组件实例的方法
    this.childRef.current.increment();
    console.log("子组件当前值:", this.childRef.current.getCount());
  };

  render() {
    return (
      <div>
        {/* 把 ref 绑定到类组件上 */}
        <ChildCounter ref={this.childRef} />
        <button onClick={this.handleClick}>让子组件加 1</button>
      </div>
    );
  }
}

export default Parent;

⚠️ 注意:父组件直接调用子组件实例的方法,会破坏 React 的"单向数据流"原则。教学中了解机制即可,实际项目应优先用 props 回调通信,必要时才使用 ref


4.4 useRef()(函数组件首选)

4.4.1 访问 DOM 元素

import React, { useRef, useEffect } from "react";

function FocusInput() {
  // 1. 创建 Ref 对象,初始值设为 null
  const inputRef = useRef(null);

  useEffect(() => {
    // 3. 挂载完成后通过 current 拿到 DOM
    inputRef.current.focus();
  }, []);
  // 空依赖数组表示只在组件挂载后执行一次

  return (
    <div>
      {/* 2. 绑定 ref */}
      <input type="text" ref={inputRef} placeholder="我会自动聚焦" />
    </div>
  );
}

export default FocusInput;

代码拆解

代码 含义
useRef(null) 创建一个 ref 对象,初始时 currentnull
ref={inputRef} 把这个 ref 对象"挂"到 input 上
inputRef.current 挂载后指向真实的 <input> DOM

4.4.2 保存跨渲染周期的可变变量

import React, { useRef, useEffect, useState } from "react";

function TimerDemo() {
  const [count, setCount] = useState(0);
  // 用 ref 保存定时器 ID,这样它不会导致重新渲染
  const timerIdRef = useRef(null);

  const start = () => {
    // 如果已经启动,先不清除,避免重复启动(这里只是演示)
    timerIdRef.current = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  };

  const stop = () => {
    // 通过 ref.current 拿到定时器 ID 并清除
    clearInterval(timerIdRef.current);
    timerIdRef.current = null;
  };

  useEffect(() => {
    // 组件卸载时清理定时器
    return () => clearInterval(timerIdRef.current);
  }, []);

  return (
    <div>
      <p>已经运行了 {count} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

export default TimerDemo;

为什么不用 state 保存定时器 ID?

因为 setState 会触发重新渲染,而定时器 ID 只是"幕后数据",用户不需要看到它。用 ref 更合适。


5. useRef 进阶:保存普通可变状态

useRef 不仅能保存 DOM,还能保存任何可变值。下面通过对比 stateref 的运行效果,来理解它们的区别。

5.1 示例:点击按钮只更新 ref,不更新 state

import React, { useRef, useState } from "react";

function RefVsState() {
  // state:修改会触发重新渲染
  const [stateCount, setStateCount] = useState(0);
  // ref:修改不会触发重新渲染
  const refCount = useRef(0);

  const addState = () => {
    setStateCount(stateCount + 1);
  };

  const addRef = () => {
    refCount.current += 1;
    // 这行修改不会导致 React 重新渲染
    // 所以页面上的数字不会立刻变
    console.log("refCount.current =", refCount.current);
  };

  const showRef = () => {
    // 只有重新渲染后,界面上才会显示最新的 refCount.current
    alert(`refCount 实际值是 ${refCount.current}`);
  };

  return (
    <div>
      <p>stateCount(会触发渲染): {stateCount}</p>
      <p>refCount(不会触发渲染): {refCount.current}</p>
      <button onClick={addState}>state +1</button>
      <button onClick={addRef}>ref +1</button>
      <button onClick={showRef}>查看 ref 真实值</button>
    </div>
  );
}

export default RefVsState;

5.2 运行效果分析

操作 界面是否更新 原因
点击 "state +1" ✅ 更新 setState 触发重新渲染
点击 "ref +1" ❌ 不更新 修改 ref.current 不会触发渲染
点击 "查看 ref 真实值" 弹窗显示实际值 虽然界面没更新,但值确实变了
点击任意 state 按钮后再看 ref 显示最新值 重新渲染时读取了最新的 ref.current

💡 教学小结ref 像是一个"秘密笔记本",你可以随时往上写东西,但 React 不会因为笔记本内容变了就重新画黑板。只有 state 变了,React 才会重新画黑板。


6. forwardRef:让父组件拿到子组件的 DOM

6.1 为什么不能直接在函数组件上绑 ref?

在函数组件中,如果你这样写:

// 子函数组件
function MyInput() {
  return <input type="text" />;
}

// 父组件
function Parent() {
  const inputRef = useRef(null);

  useEffect(() => {
    // ❌ 错误!inputRef.current 不会指向内部的 input
    console.log(inputRef.current);
  }, []);

  return <MyInput ref={inputRef} />;
}

React 会报错或无法拿到子组件内部的 DOM。因为函数组件本身没有实例,React 不知道你要把 ref 转发给谁。

6.2 使用 forwardRef 转发 ref

import React, { useRef, forwardRef, useEffect } from "react";

// 子组件用 forwardRef 包裹,接收第二个参数 ref
const MyInput = forwardRef((props, ref) => {
  // 把父组件传下来的 ref 绑定到内部的 input 上
  return <input type="text" ref={ref} placeholder="我是被转发的 input" />;
});

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

  useEffect(() => {
    // ✅ 正确!inputRef.current 指向子组件内部的 input
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <MyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>聚焦子组件输入框</button>
    </div>
  );
}

export default Parent;

6.3 代码拆解

代码 含义
forwardRef((props, ref) => {...}) 让函数组件接收父组件传来的 ref
ref={ref} 把 ref 继续往下传给具体的 DOM
<MyInput ref={inputRef} /> 父组件像平常一样写 ref

核心规则:在函数组件中,如果父组件需要拿到子组件内部的 DOM,必须让子组件用 forwardRef 包裹并显式转发 ref。


7. useImperativeHandle:只暴露你想暴露的

7.1 问题:直接暴露 DOM 会泄露细节

使用 forwardRef 后,父组件能拿到子组件的全部 DOM。如果子组件内部结构很复杂,父组件可能误用某些 DOM 方法,导致组件难以维护。

7.2 只暴露有限的方法

useImperativeHandle 搭配 forwardRef,可以让父组件只拿到你允许它使用的方法。

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

// 子组件:只暴露 focus 和 clear 方法,不暴露整个 input
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  // 自定义父组件通过 ref 能拿到什么
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = "";
    },
  }));

  return (
    <input type="text" ref={inputRef} placeholder="我只暴露 focus 和 clear" />
  );
});

function Parent() {
  const fancyRef = useRef(null);

  useEffect(() => {
    // ✅ 可以调用子组件暴露的 focus
    fancyRef.current.focus();
    // ❌ 下面这行会报错,因为父组件拿不到 value
    // console.log(fancyRef.current.value);
  }, []);

  return (
    <div>
      <FancyInput ref={fancyRef} />
      <button onClick={() => fancyRef.current.focus()}>聚焦</button>
      <button onClick={() => fancyRef.current.clear()}>清空</button>
    </div>
  );
}

export default Parent;

7.3 最佳实践

能用 props 传回调解决的问题,不要用 ref。如果确实需要父组件调用子组件的某些能力,优先用 forwardRef + useImperativeHandle,并且只暴露最少的方法,不要把整个 DOM 都交给父组件。


8. 实战:循环渲染中给每个元素绑定独立 Ref

8.1 常见错误:一个 ref 绑定多个元素

// ❌ 错误示例:不能在循环中把同一个 ref 反复赋值
function BadList() {
  const itemRef = useRef(null);

  return (
    <ul>
      {["a", "b", "c"].map((item) => (
        <li key={item} ref={itemRef}>
          {item}
        </li>
      ))}
    </ul>
  );
}

上面的代码中,itemRef.current 最终只会指向最后一个 <li>,前面的全部丢失了。

8.2 正确做法:用数组或 Map 保存多个 ref

import React, { useRef, useEffect } from "react";

function GoodList() {
  const items = ["苹果", "香蕉", "橙子"];
  // 用一个数组来保存每个 li 的 ref
  const itemRefs = useRef([]);

  useEffect(() => {
    // 拿到每个 li 的高度
    itemRefs.current.forEach((li, index) => {
      if (li) {
        console.log(`第 ${index} 个水果高度:`, li.offsetHeight);
      }
    });
  }, []);

  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={item}
          // 回调函数 ref:把当前 DOM 存到数组对应位置
          ref={(node) => {
            itemRefs.current[index] = node;
          }}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}

export default GoodList;

8.3 使用 Map 处理 key 不连续的情况

如果列表的 key 不是从 0 开始的索引,用 Map 更稳妥:

import React, { useRef, useEffect } from "react";

function MapList() {
  const items = [
    { id: "a", name: "苹果" },
    { id: "b", name: "香蕉" },
    { id: "c", name: "橙子" },
  ];
  // 用 Map 保存 id -> DOM 的映射
  const itemRefs = useRef(new Map());

  useEffect(() => {
    // 根据 id 读取某个 DOM
    const appleLi = itemRefs.current.get("a");
    console.log("苹果 li:", appleLi);
  }, []);

  return (
    <ul>
      {items.map((item) => (
        <li
          key={item.id}
          ref={(node) => {
            // 注意:卸载时 node 会变成 null,需要删除
            if (node) {
              itemRefs.current.set(item.id, node);
            } else {
              itemRefs.current.delete(item.id);
            }
          }}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
}

export default MapList;

💡 课堂提示:循环中给每个元素绑定 ref 时,要么用数组(按索引存),要么用 Map(按 key 存)。千万不要用同一个 ref 对象绑定多个元素。


9. Ref 使用注意事项与避坑指南

9.1 ref.current 什么时候才能拿到值?

错误示例:渲染阶段直接访问 ref

import React, { useRef } from "react";

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

  // ❌ 错误:此时组件还没挂载,inputRef.current 是 null
  console.log("渲染时读取:", inputRef.current);

  return <input type="text" ref={inputRef} />;
}

正确做法:在挂载完成后访问

import React, { useRef, useEffect } from "react";

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

  useEffect(() => {
    // ✅ 正确:组件挂载完成后,ref.current 指向真实 DOM
    console.log("挂载后读取:", inputRef.current);
    inputRef.current.focus();
  }, []);

  return <input type="text" ref={inputRef} />;
}

类组件中的正确时机

import React, { Component } from "react";

class RightTimingClass extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  componentDidMount() {
    // ✅ 类组件在 componentDidMount 中访问 ref
    console.log(this.inputRef.current);
    this.inputRef.current.focus();
  }

  render() {
    // ❌ render 中不要访问 ref.current,可能是 null
    return <input type="text" ref={this.inputRef} />;
  }
}

口诀ref.current 在挂载前是 null,挂载后才是真实 DOM。函数组件用 useEffect 空依赖数组,类组件用 componentDidMount


9.2 不要直接修改 React 已经管理的 DOM 属性

React 通过虚拟 DOM 和 diff 算法管理界面。如果你直接修改 DOM 的某些属性,React 下一次渲染时可能会覆盖你的修改,或者造成状态不一致。

❌ 不推荐:直接修改 value

import React, { useRef } from "react";

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

  const handleClick = () => {
    // ❌ 不推荐:直接修改 value,可能和 React 的受控组件冲突
    inputRef.current.value = "我被直接修改了";
  };

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleClick}>直接改 value</button>
    </div>
  );
}

✅ 推荐:用 state 管理 value

import React, { useState } from "react";

function GoodControlledInput() {
  const [value, setValue] = useState("");

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <button onClick={() => setValue("通过 state 修改")}>安全修改</button>
    </div>
  );
}

✅ ref 适合的原生场景

场景 为什么适合用 ref
自动聚焦 focus() React 没有原生 focus API
获取元素尺寸 getBoundingClientRect() 需要真实 DOM 的几何信息
滚动到某个位置 scrollIntoView() 需要操作原生滚动行为
媒体播放 play() / pause() <video><audio> 的原生控制
选择文本 select() 需要调用原生 DOM 方法
测量元素 动画、布局计算需要真实尺寸

9.3 条件渲染下的 ref 绑定

问题:ref 绑定的元素可能时有时无

import React, { useRef, useEffect, useState } from "react";

function ConditionalRef() {
  const [show, setShow] = useState(true);
  const boxRef = useRef(null);

  useEffect(() => {
    // 当 show 为 false 时,boxRef.current 是 null
    console.log("boxRef.current:", boxRef.current);
  }, [show]);

  return (
    <div>
      {show && <div ref={boxRef}>条件渲染的盒子</div>}
      <button onClick={() => setShow(!show)}>切换显示</button>
    </div>
  );
}

export default ConditionalRef;

使用条件渲染时,必须判断 ref.current 是否存在,再调用 DOM 方法。建议写法:if (boxRef.current) { boxRef.current.focus(); }


9.4 函数组件不能直接绑 ref,必须配合 forwardRef

❌ 错误示例

function MyInput() {
  return <input type="text" />;
}

function Parent() {
  const inputRef = useRef(null);
  // 这样写,inputRef.current 不会指向内部 input
  return <MyInput ref={inputRef} />;
}

✅ 修正示例

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

const MyInput = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

function Parent() {
  const inputRef = useRef(null);
  return <MyInput ref={inputRef} />;
}

9.5 React DevTools 中的 forwardRef 显示

当你使用 forwardRef 时,React DevTools 中组件名可能会显示为 ForwardRefAnonymous,不利于调试。

解决方案:给组件起 displayName

import React, { forwardRef } from "react";

const MyInput = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

// 给 DevTools 显示一个友好的名字
MyInput.displayName = "MyInput";

export default MyInput;

同时,如果你用 useImperativeHandle 只暴露了有限方法,父组件在 DevTools 中也看不到子组件内部 DOM,进一步降低了耦合。


10. 课堂思考题

10.1 字符串 Ref 与回调 Ref 小节

  1. 为什么字符串 Ref 被官方废弃了?你在老项目中遇到过哪些字符串 Ref 的写法?
  2. 回调 Ref 接收到的参数什么时候是 null?这个机制对清理副作用有什么帮助?

10.2 createRef 与类组件实例小节

  1. 如果把 ref 绑定到自定义类组件上,ref.current 指向什么?它和绑定到原生 DOM 元素有什么区别?
  2. 在类组件中,为什么 React.createRef() 通常放在 constructor 里创建?

10.3 useRef 与 state 对比小节

  1. 修改 ref.current 后,界面会立即更新吗?为什么?
  2. 什么情况下应该优先使用 ref 而不是 state?举出三个具体例子。

10.4 forwardRef 与 useImperativeHandle 小节

  1. 函数组件为什么不能直接在父组件中通过 ref 访问内部 DOM?
  2. 使用 useImperativeHandle 的好处是什么?它避免了什么问题?

10.5 循环与条件渲染小节

  1. 循环渲染中给每个元素绑定 ref 时,为什么不能使用同一个 ref 对象?
  2. 条件渲染下使用 ref,需要做哪些防御性判断?

11. 面试高频题解析

Q1:Ref 和 state 的区别是什么?

  • state 是 React 管理的数据,修改后会触发组件重新渲染;
  • ref 是 React 提供的引用容器,修改 ref.current 不会触发重新渲染;
  • state 用于影响 UI 的数据,ref 用于保存 DOM 引用、类实例或跨渲染的可变值。

Q2:为什么修改 ref.current 不会触发重新渲染?

:因为 ref 不是 React 渲染依赖的一部分。useRef 返回的对象在每次渲染中都是同一个引用,React 不会追踪 ref.current 的变化。它就像是一个"不受 React 监控的变量",你可以自由读写,但 React 不会因为它的变化而重新执行渲染。


Q3:函数组件中如何获取子组件内部的 DOM?

:需要让子组件用 forwardRef 包裹,并在子组件内部把接收到的 ref 转发给具体的 DOM 元素。父组件中通过 useRef 创建 ref,然后像普通 props 一样传给子组件。如果需要限制暴露能力,可以结合 useImperativeHandle 只暴露指定方法。


Q4:useRef 除了保存 DOM 还能做什么?

  • 保存定时器 ID、事件监听句柄等不需要触发渲染的值;
  • 保存上一次渲染时的 propsstate
  • 保存任意需要在组件生命周期内持续存在、但不需要显示在 UI 上的可变数据;
  • 配合 forwardRefuseImperativeHandle 向父组件暴露子组件能力。

Q5:使用 ref 操作 DOM 时有哪些禁忌?

  • 不要直接修改 React 已经受控的属性(如受控 input 的 value),应使用 state + onChange
  • 不要直接修改 innerHTML 等会绕过 React 虚拟 DOM 的属性;
  • ref 只适合 React 无法覆盖的原生操作,如 focus()play()getBoundingClientRect()scrollIntoView() 等;
  • 在挂载完成前访问 ref.current 会得到 null,需要判断时机。

Q6:什么是回调 Ref 的"两次调用"问题?

:在 React 的严格模式(StrictMode)下,某些 ref 回调函数可能会被调用两次:第一次传入 DOM 节点,第二次传入 null,然后再传入 DOM 节点。这是 React 为了检测副作用故意做的。如果你在回调 Ref 里有"只能执行一次"的副作用(比如初始化第三方库),要额外注意清理逻辑。


附录:React 19 中 Ref 相关 API 速查表

API 适用场景 示例
useRef(initialValue) 函数组件创建 ref const ref = useRef(null)
React.createRef() 类组件创建 ref this.ref = React.createRef()
forwardRef 函数组件转发 ref const Child = forwardRef((props, ref) => ...)
useImperativeHandle 自定义暴露给父组件的方法 useImperativeHandle(ref, () => ({ focus }))
回调 Ref 需要动态绑定或清理副作用 ref={(node) => { this.node = node }}
字符串 Ref 已废弃,不推荐 ref="myInput"

本文档配合 React 19 官方文档编写,所有示例均符合 React 19 规范。学习过程中如有疑问,欢迎在课堂或讨论区提出。

0

评论区