React中的forwardRef

最近强度大,主要是因为在老项目中加新功能,而我作为最底层的coder,毫无话语权,这个业务还是一个需要跨国沟通(任人宰割),要求我们只能加,而且他们code review还要以他们为准,每次耗时,耗心更无语。但对于早已有自我认知的底层人来说,只要给我时间,不压力我就行,同时学习汲取养分,因此也遇到了一些小技巧。

在 React 中,forwardRef 是一个用于将 ref 从父组件"转发"到子组件内部 DOM 节点或类组件实例的高级 API。

为什么需要 forwardRef?

默认情况下,React 的 ref 属性不会像普通 props 那样传递给函数组件。如果你直接在函数组件上绑定 ref,会收到警告且无法获取内部 DOM。forwardRef 就是为了解决这个问题而生的。

⚠️ 注意 :如果你使用的是 React 19+ref 已经可以作为普通 prop 直接传递,不再强制需要 forwardRef。但考虑到大量存量项目和 React 18 及以下版本,掌握它仍然非常重要。


核心语法

javascript 复制代码
const ChildComponent = React.forwardRef((props, ref) => {
  // props: 父组件传入的普通属性
  // ref:   父组件通过 ref=xxx 传入的引用
  return <div ref={ref}>内容</div>;
});

完整实战示例

下面用一个 "可聚焦输入框" 的场景来详细讲解:

1. 子组件:FancyInput(使用 forwardRef)
javascript 复制代码
import React, { forwardRef } from 'react';

// ✅ 使用 forwardRef 包裹函数组件
const FancyInput = forwardRef(({ label, placeholder, ...restProps }, ref) => {
  return (
    <div className="fancy-input-wrapper">
      <label>{label}</label>
      {/* ✅ 关键:将收到的 ref 绑定到真正的 DOM input 元素上 */}
      <input
        ref={ref}
        placeholder={placeholder}
        className="fancy-input"
        {...restProps}
      />
    </div>
  );
});

// 可选:设置 displayName,方便 DevTools 调试
FancyInput.displayName = 'FancyInput';

export default FancyInput;
2. 父组件:使用 ref 控制子组件
ini 复制代码
import React, { useRef } from 'react';
import FancyInput from './FancyInput';

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

  const handleFocus = () => {
    // ✅ 可以直接访问子组件内部的真实 DOM 节点
    inputRef.current?.focus();
  };

  const handleClear = () => {
    if (inputRef.current) {
      inputRef.current.value = '';
      inputRef.current.focus();
    }
  };

  return (
    <div>
      {/* ✅ ref 像普通属性一样传给被 forwardRef 包裹的组件 */}
      <FancyInput
        ref={inputRef}
        label="用户名"
        placeholder="请输入用户名"
      />
      <button onClick={handleFocus}>聚焦输入框</button>
      <button onClick={handleClear}>清空并聚焦</button>
    </div>
  );
}

进阶用法:暴露自定义方法(useImperativeHandle)

有时你不想暴露整个 DOM,而是只想暴露特定的方法给父组件:

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

const CustomSelect = forwardRef(({ options }, ref) => {
  const selectRef = useRef(null);

  // ✅ 自定义暴露给父组件的实例值
  useImperativeHandle(ref, () => ({
    // 只暴露 getValue 和 reset 两个方法
    getValue: () => selectRef.current?.value,
    reset: () => {
      if (selectRef.current) selectRef.current.selectedIndex = 0;
    },
    // 不暴露 focus、blur 等原生 DOM 方法
  }));

  return (
    <select ref={selectRef}>
      {options.map((opt) => (
        <option key={opt.value} value={opt.value}>
          {opt.label}
        </option>
      ))}
    </select>
  );
});

// 父组件调用
// const selectRef = useRef();
// selectRef.current.getValue()  ✅
// selectRef.current.focus()     ❌ undefined(被隐藏了)

关键注意事项

要点 说明
仅用于函数组件 类组件天然支持 ref,无需 forwardRef
必须绑定到 DOM/类组件 ref 最终要挂在 <div><input> 或类组件上,不能挂在另一个函数组件上(除非那个也用了 forwardRef)
displayName 务必设置,否则 DevTools 中显示为 "ForwardRef",难以调试
不要滥用 优先用 props/callback/state 通信,ref 仅用于命令式操作(聚焦、滚动、测量尺寸、集成第三方库)
React 19 变化 React 19 起 ref 可作为普通 prop,forwardRef 逐步废弃

何时该用 forwardRef?

  • ✅ 封装 UI 库组件(Button、Input、Modal 等),让使用者能获取 DOM
  • ✅ 集成第三方非 React 库(如 ECharts、地图SDK)
  • ✅ 实现自动聚焦、滚动到视图、动画触发等命令式操作
  • ❌ 仅仅为了读取子组件的 state → 应该用 props + 状态提升
  • ❌ 触发子组件重新渲染 → 应该用 props 变化驱动

总结:forwardRef 是 React 组件封装体系中打通"声明式"与"命令式"的桥梁,在构建可复用组件库时几乎是必备技能。

✅React 19+(ref 作为普通 prop)✅

javascript 复制代码
// ref 直接从 props 中解构,无需任何包装
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

💡 核心变化ref 不再是一个特殊的、被 React 内部拦截的属性,它和其他 props(如 classNameonClick)完全平等。


2. 完整实战示例(React 19+)

子组件:FancyInput
javascript 复制代码
// React 19: 直接接收 ref 作为 prop
function FancyInput({ label, placeholder, ref, ...restProps }) {
  return (
    <div className="fancy-input-wrapper">
      <label>{label}</label>
      {/* ref 直接绑定到 DOM 节点 */}
      <input
        ref={ref}
        placeholder={placeholder}
        className="fancy-input"
        {...restProps}
      />
    </div>
  );
}

export default FancyInput;
父组件:使用方式完全不变
javascript 复制代码
import { useRef } from 'react';
import FancyInput from './FancyInput';

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

  const handleFocus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      {/* ✅ 调用方式和之前一模一样,对使用者零感知 */}
      <FancyInput
        ref={inputRef}
        label="用户名"
        placeholder="请输入用户名"
      />
      <button onClick={handleFocus}>聚焦</button>
    </div>
  );
}

3. useImperativeHandle 同样简化

useImperativeHandle 仍然保留,但配合新的 ref-as-prop 模式更自然:

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

// React 19: ref 从 props 获取
function CustomSelect({ options, ref }) {
  const selectRef = useRef(null);

  // ✅ ref 参数直接使用从 props 传入的 ref
  useImperativeHandle(ref, () => ({
    getValue: () => selectRef.current?.value,
    reset: () => {
      if (selectRef.current) selectRef.current.selectedIndex = 0;
    },
  }));

  return (
    <select ref={selectRef}>
      {options.map((opt) => (
        <option key={opt.value} value={opt.value}>{opt.label}</option>
      ))}
    </select>
  );
}

4. ⚠️ 迁移注意事项

注意点 说明
向后兼容 React 19 仍然支持 forwardRef,旧代码无需立即修改
TypeScript 类型 不再需要 ForwardRefRenderFunction 类型,直接用普通组件 Props 接口 + { ref?: Ref<HTMLInputElement> } 即可
displayName 不再需要手动设置 displayName,因为组件就是普通函数,DevTools 直接显示函数名
高阶组件(HOC) 如果你的 HOC 之前用 forwardRef 包裹来透传 ref,现在可以直接把 ref 当作 prop 透传,大幅简化 HOC 实现
最低版本要求 必须升级到 react@^19.0.0react-dom@^19.0.0

TypeScript 示例(React 19+)

typescript 复制代码
import { type Ref } from 'react';

interface FancyInputProps {
  label: string;
  placeholder?: string;
  ref?: Ref<HTMLInputElement>; // ✅ 直接在 props 接口中声明
}

function FancyInput({ label, placeholder, ref }: FancyInputProps) {
  return (
    <div>
      <label>{label}</label>
      <input ref={ref} placeholder={placeholder} />
    </div>
  );
}

📌 总结

React 19 将 ref "去特殊化",使其回归为普通 prop。这意味着:

  • 新代码 :直接使用 ref 作为 prop,忘掉 forwardRef
  • 旧代码:继续正常工作,可在日常重构中逐步替换
  • 心智模型:不再有"哪些 prop 是特殊的"这种认知负担,所有属性一视同仁
相关推荐
小小小小宇1 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端
不知疲倦的老鸟1 小时前
Node.js 库在浏览器里跑不了的教训
react.js·next.js
weedsfly2 小时前
语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌
前端·javascript
JustHappy2 小时前
「软件设计思想杂谈🤔」“切图仔”也能懂编译原理?框架源码也许没那么难。聊聊 Vue 的编译(上)
前端·javascript·vue.js
禅思院2 小时前
路由性能高可用架构实战方案
前端·架构·前端框架