React:useRef 超详细教程、forwardRef 详解、useImperativeHandle详解

文章目录

  • [一、React useRef 超详细教程](#一、React useRef 超详细教程)
    • [1. 什么是 useRef?](#1. 什么是 useRef?)
    • [2. 场景一:访问 DOM 节点(最常见)](#2. 场景一:访问 DOM 节点(最常见))
    • [3. 场景二:存储"不需要 UI 感知"的变量](#3. 场景二:存储“不需要 UI 感知”的变量)
    • [4. useRef vs useState:深度对比](#4. useRef vs useState:深度对比)
    • [5. 核心注意事项(避坑指南)](#5. 核心注意事项(避坑指南))
      • [① 不要在渲染期间读写 .current](#① 不要在渲染期间读写 .current)
      • [② 只有在必要时才使用 Ref](#② 只有在必要时才使用 Ref)
      • [③ Ref 无法在函数组件上直接使用](#③ Ref 无法在函数组件上直接使用)
    • [6. 总结](#6. 总结)
  • [二、 forwardRef 详解:打破组件黑盒](#二、 forwardRef 详解:打破组件黑盒)
    • [1. 为什么需要 forwardRef?](#1. 为什么需要 forwardRef?)
    • [2. 如何使用 forwardRef](#2. 如何使用 forwardRef)
    • [3. 进阶用法:结合 useImperativeHandle](#3. 进阶用法:结合 useImperativeHandle)

一、React useRef 超详细教程

在 React 的世界里,useState 负责驱动 UI 更新,而 useRef 则是那个"静默的观察者"。它非常强大,但如果用错了,会让你的代码变得难以维护。

这篇教程将带你深度拆解 useRef 的核心逻辑、应用场景以及它与 useState 的本质区别。


1. 什么是 useRef?

useRef 返回一个可变的 ref 对象 ,其 .current 属性被初始化为传入的参数。它有两个核心特性:

  • 跨渲染持久化:在组件的整个生命周期内,这个对象保持不变。
  • 更新不触发重新渲染 :修改 .current 的值不会 导致组件重新渲染(这是它与 useState 最大的区别)。

2. 场景一:访问 DOM 节点(最常见)

在 React 中,我们通常通过 propsstate 来管理 UI,但有时你需要直接操作底层的 DOM 元素(例如:聚焦输入框、滚动到特定位置、调用浏览器 API)。

代码示例:自动聚焦输入框

javascript 复制代码
import { useRef } from 'react';

function TextInputWithFocusButton() {
  // 1. 初始化 ref,初始值为 null
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // 3. 通过 .current 访问真实的 DOM 节点
    // 当组件挂载后,inputEl.current 将指向真实的 <input> 元素
    if (inputEl.current) {
      inputEl.current.focus();
    }
  };

  return (
    <>
      
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>聚焦输入框</button>
    </>
  );
}

3. 场景二:存储"不需要 UI 感知"的变量

有时候你需要记录一些数据,这些数据在改变时不应该触发页面刷新。比如计时器 ID、前一次的 Props 值,或者记录某种操作的次数。

代码示例:秒表计时器

javascript 复制代码
import { useState, useRef } from 'react';

function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  
  // 使用 useRef 存储 interval ID,因为改变它不需要更新 UI
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    // 将计时器 ID 存入 ref
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    // 停止计时,直接从 ref 中取 ID,不会引起额外的渲染
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>时间:{secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>开始</button>
      <button onClick={handleStop}>停止</button>
    </>
  );
}

4. useRef vs useState:深度对比

特性 useState useRef 普通变量 (let/const)
返回值 [state, setState] { current: ... } 变量本身
修改方式 调用 setState(newValue) 直接修改 ref.current = newValue 直接重新赋值
触发渲染 触发组件 Re-render 不会触发渲染 不会触发渲染
持久性 渲染间持久化(重绘后值保留) 渲染间持久化(重绘后值保留) 无法持久化(每次函数执行都会重置)
用途 存储驱动 UI 显示的数据(状态) 存储 DOM 节点、Timer ID 或不需要展示在页面上的逻辑变量 临时计算、函数内部的局部逻辑
同步/异步 状态更新通常是异步的(在闭包中读取旧值) 修改是同步的,值立即改变 同步修改

5. 核心注意事项(避坑指南)

① 不要在渲染期间读写 .current

React 期望组件是纯函数。如果你在 return 之前直接修改 ref.current,可能会导致难以预测的 Bug。

❌ 错误写法:

javascript 复制代码
function MyComponent() {
  const myRef = useRef(0);
  myRef.current = myRef.current + 1; // 严禁在渲染过程中修改
  return <div>{myRef.current}</div>;
}
  • ✅ 正确写法:
    useEffect 或事件处理函数(Event Handlers)中操作。

② 只有在必要时才使用 Ref

如果你可以通过 stateprops 实现功能,优先使用它们。Ref 相当于 React 的"紧急出口",过度使用会让你的应用逻辑变得难以追踪。

javascript 复制代码
import { useRef, useEffect, useState } from 'react';

function MyComponent() {
  const myRef = useRef(0);
  const [count, setCount] = useState(0);

  useEffect(() => {
    // ✅ 正确:在渲染完成后执行副作用
    myRef.current = myRef.current + 1;
    console.log("当前 Ref 的值是:", myRef.current);
  });

  return (
    <div>
      <p>Ref 值(仅在控制台查看最新): {myRef.current}</p>
      <button onClick={() => setCount(c => c + 1)}>重新渲染组件</button>
    </div>
  );
}

③ Ref 无法在函数组件上直接使用

如果你想给一个函数组件 添加 ref 属性,会报错。

  • 原因:函数组件没有实例。
  • 解决方案 :使用 forwardRef API 将 ref 转发到子组件内部的 DOM。

6. 总结

  • useRef 就像一个"盒子",你在里面放任何东西,React 都会帮你存着,直到组件销毁。
  • 它是操作 DOM 的官方指定通道。
  • 它是存储 "静默变量"(不影响 UI 的变量)的绝佳地点。
  • 关键结论:改 ref 不会刷页面!

二、 forwardRef 详解:打破组件黑盒

在 React 中,组件就像一个黑盒。默认情况下,你不能从父组件直接获取子组件内部的 DOM 节点或组件实例。这种限制是为了保证组件的封装性。

forwardRef(引用转发)就是为了打破这种限制,允许组件像传递普通 Props 一样,将 ref 转发给其子节点。

1. 为什么需要 forwardRef?

假设你封装了一个基础按钮组件 MyButton:

javascript 复制代码
function MyButton(props) {
  return <button className="btn">{props.children}</button>;
}

如果你想在父组件中让这个按钮自动聚焦:

javascript 复制代码
const btnRef = useRef(null);

// ...
<MyButton ref={btnRef}>点击</MyButton>

结果: btnRef.current 会是 null。

原因: React 默认不会把 ref 作为一个 prop 传给组件。ref 属性被 React 特殊处理了,就像 key 一样,不会出现在 props 对象中。


2. 如何使用 forwardRef

forwardRef 接受一个渲染函数,该函数接收两个参数:props 和 ref。

javascript 复制代码
import { forwardRef } from 'react';

const MyButton = forwardRef((props, ref) => {
  return (
    <button ref={ref} className="btn">
      {props.children}
    </button>
  );
});
javascript 复制代码
function Parent() {
  const btnRef = useRef(null);

  const handleClick = () => {
    // 成功获取子组件内部的 button 节点
    btnRef.current.focus(); 
  };

  return (
    <MyButton ref={btnRef} onClick={handleClick}>
      Focus Me
    </MyButton>
  );
}

3. 进阶用法:结合 useImperativeHandle

有时候,你不想把整个 DOM 节点暴露给父组件,而只想暴露特定的方法(例如:只允许父组件调用 focus,但不允许修改样式)。

这时需要配合 useImperativeHandle Hook:

javascript 复制代码
import { forwardRef, useRef, useImperativeHandle } from 'react';

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

  // 自定义暴露给父组件的实例值
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    shake: () => {
      console.log("正在抖动输入框...");
    }
  }));

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

父组件: 现在 ref.current 只有 { focus, shake } 这两个方法,而拿不到真实的 DOM 节点。这符合最小暴露原则。

相关推荐
xuankuxiaoyao1 小时前
vue.js 课程自己编写小游戏
前端·javascript·vue.js
兔子零10241 小时前
当 Codex 成为主力,软件工程的重心已经变了
前端·后端·架构
牛奶1 小时前
网关是怎么当"门卫"的?
前端·后端·负载均衡
天一生水water1 小时前
VUE3入门
javascript
上海合宙LuatOS1 小时前
合宙TCP/UDP web测试工具简介
前端·物联网·tcp/ip·udp·luatos
yqcoder2 小时前
JavaScript 浅拷贝:只复制“第一层”的艺术
开发语言·javascript·ecmascript
yqcoder2 小时前
JavaScript 闭包:函数背后的“背包”
开发语言·javascript·ecmascript
Apifox.2 小时前
Apifox 近期更新|AI Agent Debugger、A2A Debugger、Postman API 导入、Ask AI 侧边栏对话
前端·人工智能·后端·测试工具·测试用例·postman
threelab2 小时前
挑战AI辅助从零构建3D模型编辑器:01基于Vue3 + Three.js的现代化架构设计
javascript·人工智能·3d·前端框架·着色器