ref、useRef 和 forwardRef之间关系
在 React 中,ref 是一个核心概念,它提供了一种在组件中访问和操作 DOM 节点或 React 元素的方式,是 React 声明式范式之外的一种"命令式"操作手段。
简单来说,ref、useRef 和 forwardRef 三者紧密协作,共同解决了"如何直接操作 DOM"以及"如何在组件间传递这种操作能力"的问题。
核心概念解析
1. ref:引用
ref 本身是一个特殊的属性,它可以附加到任何 React 元素上。它的作用是创建一个"引用",这个引用对象有一个 current 属性,React 会自动将 current 指向对应的 DOM 节点或组件实例。
2. useRef:创建引用的 Hook
useRef 是一个 React Hook,主要在函数组件中使用。它有两个核心用途:
- 访问 DOM 节点 :创建一个引用对象,并将其通过
ref属性绑定到 JSX 元素上,从而在组件中获取对该 DOM 元素的直接引用。 - 存储可变值 :创建一个在组件整个生命周期内都保持不变的"容器",用于存储任何可变值(如定时器ID、上一个状态值等)。修改
useRef返回对象的current属性不会触发组件的重新渲染 ,这是它与useState的关键区别。
示例:使用 useRef 实现输入框自动聚焦
jsx
import { useRef, useEffect } from 'react';
function InputField() {
// 1. 创建 ref 对象
const inputRef = useRef(null);
useEffect(() => {
// 2. 组件挂载后,通过 .current 访问并操作 DOM
inputRef.current.focus();
}, []);
// 3. 将 ref 对象绑定到 input 元素
return <input ref={inputRef} type="text" />;
}
3. forwardRef:转发引用的工具
forwardRef 是一个高阶函数,用于将 ref 从父组件转发到子组件内部的某个 DOM 元素上。
为什么需要它?
默认情况下,函数组件没有实例,因此无法直接接收 ref。如果你在父组件中尝试将一个 ref 传给一个普通的函数组件,这个 ref 会"消失",无法到达组件内部。forwardRef 就是为了解决这个问题而生的,它像一个"桥梁",打通了组件边界,让父组件的 ref 能够穿透子组件,直接绑定到子组件内部的特定 DOM 元素上。
示例:使用 forwardRef 让父组件控制子组件的 DOM
jsx
import { forwardRef, useRef } from 'react';
// 1. 子组件使用 forwardRef 包装,并接收 ref 作为第二个参数
const FancyInput = forwardRef((props, ref) => {
return (
<div>
<label>{props.label}</label>
{/* 2. 将接收到的 ref 绑定到内部的 input 元素 */}
<input ref={ref} {...props} />
</div>
);
});
function ParentComponent() {
// 3. 父组件创建 ref
const inputRef = useRef(null);
const handleClick = () => {
// 4. 父组件可以直接操作子组件内部的 input 元素
inputRef.current.focus();
};
return (
<div>
<FancyInput ref={inputRef} label="用户名:" />
<button onClick={handleClick}>聚焦输入框</button>
</div>
);
}
关联与区别
useRef 和 forwardRef 通常协同工作,但它们扮演的角色截然不同。
useRef负责创建引用的容器。forwardRef负责传递这个引用容器。
它们的协作流程可以概括为:父组件用 useRef 创建 ref → 通过 ref 属性传给子组件 → 子组件用 forwardRef 接收并转发 ref → 最终将 ref 绑定到内部的 DOM 元素上。
下表清晰地展示了三者的区别:
| 特性 | ref (属性) | useRef (Hook) | forwardRef (函数) |
|---|---|---|---|
| 核心作用 | 附加到 JSX 元素上,用于获取其引用。 | 在函数组件中创建一个可变的引用对象。 | 转发 ref,使其能穿透函数组件。 |
| 使用场景 | 任何 React 元素(DOM 或组件)。 | 函数组件内部,用于访问 DOM 或存储可变值。 | 封装可复用的函数组件,并需要暴露其内部 DOM 时。 |
| 主要目的 | 建立引用关系。 | 提供一个持久化的引用容器。 | 打通组件边界,实现引用的透传。 |
总而言之,ref 是建立引用的机制,useRef 是在函数组件中实现这一机制的工具,而 forwardRef 则是解决函数组件无法直接接收 ref 这一限制的"传送门"。
forwardRef<HTMLButtonElement, Props>解释
forwardRef<HTMLButtonElement, Props> 是 TypeScript 泛型语法,指定这个组件转发的 ref 类型和 props 类型。
拆解
tsx
forwardRef<HTMLButtonElement, Props>(renderFn)
↑ ↑
ref 的类型 props 的类型
| 泛型参数 | 含义 |
|---|---|
HTMLButtonElement |
暴露给外部的 ref 指向什么 DOM 元素 |
Props |
组件接收什么 props |
例子
tsx
interface Props {
label: string;
onClick: () => void;
}
// 这个组件的 ref 会指向 <button> DOM 元素
const Button = forwardRef<HTMLButtonElement, Props>((props, ref) => {
return (
<button ref={ref} onClick={props.onClick}>
{props.label}
</button>
);
});
// 外部使用
const ref = useRef<HTMLButtonElement>(null);
<Button ref={ref} label='点我' />;
为什么需要指定
tsx
// 不指定 - 外部不知道 ref 指向什么
const A = forwardRef((props, ref) => ...);
// 指定 - 外部知道 ref 指向 HTMLButtonElement
const B = forwardRef<HTMLButtonElement, Props>((props, ref) => ...);
一句话 :forwardRef<RefType, PropsType> 告诉 TypeScript:这个组件的 ref 指向 RefType,接收的 props 是 PropsType。
相关概念
ForwardRefExoticComponent
"Exotic" 是 React 内部术语,表示一种"不普通"的组件。forwardRef 或 memo 包装后的组件类型就是 ForwardRefExoticComponent。
| 普通组件 | Exotic 组件 |
|---|---|
FC, Component |
forwardRef, memo 包装后的类型 |
RefAttributes
RefAttributes<T> 是 React 用来给组件附加 ref 类型信息的内置类型。
tsx
interface RefAttributes<T> {
ref?: Ref<T> | null; // ref 接受的值
}
RefAttributes<T> 就是让组件类型带上 ref 能力的标记类型,通常配合 forwardRef 使用。