React + TypeScript Ref 类型完全指南:RefObject、RefCallback 与 Ref 联合类型(React 19 篇)
2026/9/19 8:46:28 网站建设 项目流程

React + TypeScript Ref 类型完全指南:RefObject、RefCallback 与 Ref 联合类型(React 19 篇)

【免费下载链接】reactCheatsheets for experienced React developers getting started with TypeScript项目地址: https://gitcode.com/gh_mirrors/reactt/react-typescript-cheatsheet

本文以 React TypeScript Cheatsheet 仓库的 Ref 参考文档 为核心,系统讲解@types/react中与 ref 相关的三个核心类型——RefObject<T>RefCallback<T>Ref<T>——的定义、使用场景与相互关联,并结合仓库中的 Hooks、forwardRef/createRef、ComponentProps 等文档与源码印证实际用法。读完本文,你将能准确为useRefcreateRef、内联 ref 回调以及组件ref属性选型并写出类型安全的代码,理解 React 19 中"ref 即普通 prop"的变迁对类型系统带来的简化。

一、先看全景:三个类型一张表

@types/react内置了三个密切相关的 ref 类型。理解它们如何拼合在一起,是正确书写 ref 类型的关键——尤其在 React 19 中,ref已经是函数组件的普通 prop,这组类型的使用方式也随之发生了重要变化。

类型它是什么何时使用
RefObject<T>一个带current: T字段的对象。useRefcreateRef的返回类型。把它传给ref={…}以读写.current
RefCallback<T>一个接收实例(卸载时接收null)的函数。内联ref={node => …}回调。在 React 19+ 中可以返回清理函数。
Ref<T>RefCallback<T> \| RefObject<T \| null> \| null的联合类型。作为接收父组件传入 ref 时的prop 类型——父组件可能传任意一种形式。
interface RefObject<T> { current: T; } type RefCallback<T> = (instance: T | null) => void | (() => void); type Ref<T> = RefCallback<T> | RefObject<T | null> | null;

三个类型的分工可以概括为:RefObject是"容器"、RefCallback是"回调"、Ref<T>是"收口"——当你只需要对外接收 ref 时,永远用最宽的Ref<T>

二、RefObject<T>useRefcreateRef的返回类型

RefObject<T>useRefcreateRef的返回值类型。它的.current字段类型取决于你传入的初始值:

import { useRef } from "react"; const inputRef = useRef<HTMLInputElement>(null); // ^? RefObject<HTMLInputElement | null> const idRef = useRef(0); // ^? RefObject<number>

仓库 Hooks 文档明确指出:当前@types/reactuseRef始终返回RefObject<T>,且必须提供初始值,返回的.current类型由初始值推导。

2.1 传入null初始值:由 React 托管.current

当你显式传入泛型并以null作为初始值时,React 会在挂载阶段替你写入 DOM 节点。TypeScript 将.current类型为T | null,因此使用前必须做空值检查

useEffect(() => { inputRef.current?.focus(); }, []);

在 Hooks 文档的 DOM ref 示例中,给出了更严谨的守卫式写法——先抛出异常再放心使用:

function Foo() { const divRef = useRef<HTMLDivElement>(null); useEffect(() => { // ref.current 可能为 null:元素可能是条件渲染的,也可能忘记绑定 ref if (!divRef.current) throw Error("divRef is not assigned"); // 此时 divRef.current 一定是 HTMLDivElement doSomethingWith(divRef.current); }); // 把 ref 交给元素,让 React 替你管理 return <div ref={divRef}>etc</div>; }

如果确定divRef.current永远不会为 null,也可以用非空断言null!绕过空值检查,但要清楚这是在主动放弃类型安全:一旦忘记给元素绑定 ref,或 ref 元素被条件渲染,就会在运行时抛错。

2.2MutableRefObject<T>:已弃用的旧类型

MutableRefObject<T>仍然存在于@types/react中,仅出于向后兼容保留,并标记为@deprecated——请一律改用RefObject<T>。这在 Hooks 文档的useRef一节中亦有明确说明。

2.3 可变值 ref:React 不托管.current

若要跨渲染保存可变值且不希望改动触发重渲染,直接传入初始值即可。此时.current由你手动读写,React 不会介入:

function Foo() { const intervalRef = useRef<number | null>(null); useEffect(() => { intervalRef.current = window.setInterval(() => { /* ... */ }, 1000); return () => { if (intervalRef.current !== null) clearInterval(intervalRef.current); }; }, []); return ( <button onClick={() => { /* 清理 intervalRef */ }} > Cancel timer </button> ); }

这里用useRef<number | null>(null)明确表达了"初始为 null、之后写入 number"的联合类型,配合空值检查即可安全读写。

三、RefCallback<T>:节点挂载/卸载瞬间执行代码

回调 ref 适合在 DOM 节点挂载或卸载的那一刻执行代码。回调在挂载时收到节点,卸载时收到null

<div ref={(node) => { if (node) console.log("mounted", node); else console.log("unmounted"); }} />

3.1 React 19 的清理函数(cleanup function)

在 React 19 中,ref 回调可以返回一个清理函数——React 会调用它,而不是像旧版那样再次以null调用回调。这使 ref 回调与useEffect的清理机制保持对称:

<div ref={(node) => { const observer = new IntersectionObserver(/* ... */); observer.observe(node); return () => observer.disconnect(); }} />

如果回调没有返回任何内容,React 会回退到旧行为:卸载时以null再次调用该回调。从类型定义type RefCallback<T> = (instance: T | null) => void | (() => void)可以印证:返回值是"无返回值"或"清理函数"二者的联合。

四、Ref<T>:接收 ref 的 prop 类型

Ref<T>是你在接收ref 作为 prop 时应使用的类型,因为调用方可能传入RefObject或回调中的任意一种:

import { Ref } from "react"; type FancyInputProps = { ref?: Ref<HTMLInputElement>; placeholder?: string; }; function FancyInput({ ref, placeholder }: FancyInputProps) { return <input ref={ref} placeholder={placeholder} className="fancy" />; }

在 React 19 中,这样写就足够了——ref是普通 prop,不再需要forwardRef包裹。仓库 patterns_by_usecase 文档中的FancyButton示例同样遵循这一模式:

import { Ref, ReactNode } from "react"; type Props = { children: ReactNode; type: "submit" | "button"; ref?: Ref<HTMLButtonElement>; }; export const FancyButton = ({ ref, children, type }: Props) => ( <button ref={ref} className="MyCustomButtonClass" type={type}> {children} </button> );

4.1 把 ref 转发到非根元素

如果接收到的 ref 并不属于根元素,仍然可以把它继续向下传递——只要T匹配,Ref<T>可以赋值给任意元素的refprop:

type LabelledInputProps = { label: string; ref?: Ref<HTMLInputElement>; }; function LabelledInput({ label, ref }: LabelledInputProps) { return ( <label> {label} <input ref={ref} /> </label> ); }

这里ref没有落在根元素<label>上,而是传给了内部的<input>,类型完全成立。

4.2 与forwardRef的对比:React 19 前/后

对于仍维护 React ≤ 18 代码的读者,仓库 forward-create-ref 文档给出了旧式forwardRef写法,其ref参数类型是ForwardedRef<T>

import { forwardRef, ReactNode } from "react"; interface Props { children?: ReactNode; type: "submit" | "button"; } export type Ref = HTMLButtonElement; export const FancyButton = forwardRef<Ref, Props>((props, ref) => ( <button ref={ref} className="MyClassName" type={props.type}> {props.children} </button> ));

同一文档还给出 React 19 下的两条新路线:用ComponentPropsWithRef<"input">直接继承原生元素全部 props(含 ref),或像上文那样用Ref<T>显式声明。这也呼应了 ComponentProps 文档中的说明:React 19 下ComponentProps<T>通常已够用,因为ref对函数组件而言就是普通 prop;只有需要从展开中剥离ref时才用ComponentPropsWithoutRef<T>

五、useImperativeHandleRef<T>的组合

React 19 下useImperativeHandle直接接收refprop,无需forwardRef。仓库 Hooks 文档给出了完整的双向类型推导示例:

// Countdown.tsx import { useImperativeHandle, Ref } from "react"; export type CountdownHandle = { start: () => void; }; type CountdownProps = { ref?: Ref<CountdownHandle>; }; const Countdown = ({ ref }: CountdownProps) => { useImperativeHandle(ref, () => ({ // start() 在此处获得类型推导 start() { alert("Start"); }, })); return <div>Countdown</div>; };
// 使用 Countdown 的父组件 import { useEffect, useRef } from "react"; import Countdown, { CountdownHandle } from "./Countdown.tsx"; function App() { const countdownEl = useRef<CountdownHandle>(null); useEffect(() => { if (countdownEl.current) { // start() 在调用侧同样获得类型推导 countdownEl.current.start(); } }, []); return <Countdown ref={countdownEl} />; }

注意countdownEl的类型是RefObject<CountdownHandle | null>(因为以null初始化),父组件调用前仍需空值判断,这与本文 2.1 节的规则完全一致。

六、相关类型速查

除了上述三兄弟,@types/react还提供几个相关的 ref 类型,逐一说明:

  • ForwardedRef<T>——旧式forwardRef渲染函数收到的ref参数类型。仅在你仍使用forwardRef时相关;优先改用 prop 上的Ref<T>
  • LegacyRef<T>——Ref<T>@deprecated别名。字符串 ref(string refs)已不再受支持。
  • ComponentRef<T>——某组件或元素所接受的 ref 类型,例如ComponentRef<"input">就是HTMLInputElement。当你想拿到 ref 类型又不愿手写完整名称时很有用。
  • RefAttributes<T>——{ ref?: Ref<T> }这样的 props 形状。很少需要直接使用,它被ComponentPropsWithRef交叉组合进来。

配合 ComponentProps 文档中的ComponentPropsWithRef<T>(等于ComponentProps<T>外加 ref)与ComponentPropsWithoutRef<T>(剥离任何refprop),你可以在"继承原生元素 props"与"精确控制 ref 类型"两种策略间自由选择,这也是 forward-create-ref 文档中 Option 1(ComponentPropsWithRef)与 Option 2(显式Ref<T>)两种写法的选型依据。

七、泛型组件中的 ref 处理

泛型组件由于泛型参数会阻断自动类型推导,通常需要手动处理 ref。仓库 forward-create-ref 文档提供了三种方案,其中 Option 1(包装组件)最直观,直接通过 props 传入Ref<T>

interface ClickableListProps<T> { items: T[]; onSelect: (item: T) => void; mRef?: React.Ref<HTMLUListElement> | null; } export function ClickableList<T>(props: ClickableListProps<T>) { return ( <ul ref={props.mRef}> {props.items.map((item, i) => ( <li key={i}> <button onClick={() => props.onSelect(item)}>Select</button> {item} </li> ))} </ul> ); }

这里React.Ref<HTMLUListElement>的使用正是本文核心类型的直接应用:它既接受父组件传RefObject,也接受传回调。文档建议:Option 1 通常已足够且更清晰;确需forwardRef行为时用 Option 2(重声明forwardRef);需要同时支持泛型与完整forwardRef类型推导的进阶库场景用 Option 3(调用签名)。

八、核心结论

  • 选型口诀useRef/createRef的返回值用RefObject<T>;内联回调用RefCallback<T>(React 19 可返回清理函数);组件对外接收 ref 一律声明为ref?: Ref<T>
  • React 19 的关键变化ref成为函数组件的普通 prop,Ref<T>直接可作 prop 类型,绝大多数场景不再需要forwardRefForwardedRefLegacyRef退居"旧代码兼容"位置。
  • 类型安全红线:以null初始化的RefObject<T | null>在使用前必须空值检查;null!非空断言是主动放弃类型安全的逃生舱。
  • 延伸阅读:仓库内的 Hooks(useRefuseImperativeHandle实战)、forward-create-ref(React 19 新写法与旧式forwardRef对照、泛型 ref 三方案)、ComponentProps(ComponentPropsWithRef/WithoutRef选型)以及 patterns_by_usecase(包装/镜像 HTML 元素)共同构成了完整的 React 19 + TypeScript ref 知识体系。

【免费下载链接】reactCheatsheets for experienced React developers getting started with TypeScript项目地址: https://gitcode.com/gh_mirrors/reactt/react-typescript-cheatsheet

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询