React 基础体系 · 第 30/70 篇。示例以 React 19、现代 TypeScript 和主流框架能力为基础;客户端与服务端边界会明确说明。
React Ref 与 useImperativeHandle:焦点、测量和命令式 API
ref 是 React 提供的一条“保留引用通道”:组件提交(commit)到界面后,React 可以把某个 DOM 节点或组件实例写入 ref。它不同于 props 和 state:
props描述父组件传给子组件的输入;state参与渲染,更新后通常会触发重新渲染;ref保存一个可变值,React 不会因为ref.current改变而重新渲染组件。
因此,ref 适合表示“已经提交到界面的对象”或“跨渲染保存但不驱动渲染的值”,典型场景包括:
- 让输入框获得焦点、选中文本;
- 读取 DOM 的尺寸和位置;
- 保存计时器、第三方实例等非渲染资源;
- 子组件向父组件暴露少量命令式操作。
useImperativeHandle 则用于控制父组件通过 ref 能看到什么。它不是让 React 变成命令式框架,而是把一个明确、受控的命令式接口接到声明式组件上。
一、先区分三种“引用”
1. DOM 引用
import { useRef } from 'react';
export function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<input
ref={inputRef}
placeholder="搜索"
onFocus={() => {
console.log(inputRef.current);
}}
/>
);
}
在首次渲染期间,inputRef.current 通常仍然是 null。React 完成 DOM 提交后,才会把对应的 <input> 写入其中;组件卸载时又会将其设回 null。
可以把它抽象成以下时序:
render
│
├─ inputRef.current 仍可能是 null
│
commit:创建或更新 DOM
│
├─ inputRef.current = HTMLInputElement
│
浏览器绘制
│
卸载或节点解绑
│
└─ inputRef.current = null
这意味着以下代码是不安全的:
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
// 错误:render 阶段不能假设 DOM 已经存在
inputRef.current?.focus();
return <input ref={inputRef} />;
}
它可能不会立即抛错,因为可选链会跳过 null,但也不会在输入框真正挂载后自动执行聚焦。应该把操作放入事件处理器、Effect,或者由父组件在合适的时机调用。
2. 保存普通可变值的引用
const timerRef = useRef<number | null>(null);
function start() {
timerRef.current = window.setTimeout(() => {
console.log('done');
}, 1000);
}
timerRef.current 的改变不会触发重新渲染。如果界面需要显示“计时器是否运行”,就不能只修改这个 ref,还需要使用 useState。
这构成一个重要条件:
只有当某个值的变化不需要重新计算 JSX 时,才适合仅保存在 ref 中。
3. 暴露给父组件的命令式句柄
DOM 引用是“把节点交给使用者”;命令式句柄(imperative handle)是“只把预先定义的操作交给使用者”。
例如,父组件不必获得整个 <input>,而只获得:
type SearchBoxHandle = {
focus: () => void;
select: () => void;
measure: () => DOMRect;
};
这样,子组件可以更换内部结构,而父组件仍然只依赖这个接口。
二、React 19 中的 ref 传递方式
在 React 19 中,函数组件可以像接收普通 prop 一样接收 ref:
import {
useImperativeHandle,
useRef,
type Ref,
} from 'react';
type SearchBoxHandle = {
focus: () => void;
};
type SearchBoxProps = {
ref?: Ref<SearchBoxHandle>;
};
function SearchBox({ ref }: SearchBoxProps) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
}), []);
return <input ref={inputRef} />;
}
使用方:
function Page() {
const searchBoxRef = useRef<SearchBoxHandle>(null);
return (
<>
<SearchBox ref={searchBoxRef} />
<button onClick={() => searchBoxRef.current?.focus()}>
聚焦搜索框
</button>
</>
);
}
这里有两个不同的 ref:
inputRef:子组件内部指向真实 DOM 节点;searchBoxRef:父组件持有子组件暴露的SearchBoxHandle。
useImperativeHandle 把前者包装成后者。
在 React 18 及更早的常见写法中,函数组件不能直接通过普通参数接收 ref,通常使用 forwardRef:
import {
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
const SearchBox = forwardRef<SearchBoxHandle, {}>((_props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
}), []);
return <input ref={inputRef} />;
});
如果组件库需要同时兼容 React 18 和 React 19,应根据支持范围保留 forwardRef 方案;只面向 React 19 的代码可以直接声明 ref prop。TypeScript 类型还需要与相应版本的 @types/react 保持一致。
三、useImperativeHandle 的精确定义
API 形式是:
useImperativeHandle(ref, createHandle, dependencies?)
三个参数分别表示:
ref:父组件传入的 ref,可以是对象 ref、回调 ref,或者null;createHandle:返回要暴露给父组件的值的函数;dependencies:创建句柄所依赖的响应式值列表。
例如:
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
}), []);
执行后,父组件的 ref.current 会指向返回的对象,而不是子组件函数本身:
父组件 ref.current
│
▼
{
focus: Function
}
如果没有 useImperativeHandle,把 ref 直接放到某个 DOM 元素上:
<input ref={ref} />
则父组件获得的是 HTMLInputElement。使用 useImperativeHandle 后,父组件获得的是子组件指定的句柄。
依赖数组决定句柄何时重建
假设句柄使用了 disabled:
useImperativeHandle(ref, () => ({
focus() {
if (!disabled) {
inputRef.current?.focus();
}
},
}), [disabled]);
当 disabled 发生变化时,React 会重新创建句柄。
React 使用类似 Object.is(previous, next) 的比较判断依赖是否变化。因此:
const options = { preventScroll: true };
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus(options);
},
}), [options]);
如果 options 在每次渲染时都是新对象,句柄也会在每次渲染时重建。可以把稳定值移出组件、使用 useMemo,或者直接写成字面量调用。
如果句柄方法只读取 inputRef.current,不读取会变化的 props 或 state,则可以使用空依赖数组:
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
}), []);
这里不会产生旧闭包问题,因为方法每次执行时都读取当前的 inputRef.current。相反,下面的代码会捕获创建句柄时的 value:
function CounterInput({
value,
ref,
}: {
value: string;
ref?: React.Ref<{ logValue: () => void }>;
}) {
useImperativeHandle(ref, () => ({
logValue() {
console.log(value);
},
}), []); // 错误:value 不在依赖中
}
父组件之后调用 logValue() 时,可能得到旧的 value。修复方式是声明依赖:
useImperativeHandle(ref, () => ({
logValue() {
console.log(value);
},
}), [value]);
或者让方法读取一个始终同步的 ref。通常前者更直接,也更容易让代码和 React 的响应式数据流保持一致。
四、焦点控制:完整的可运行示例
下面的组件实现三个命令:
focus():让输入框获得焦点;select():选中输入框中的全部文本;measure():读取输入框当前的几何信息。
import {
useImperativeHandle,
useRef,
type Ref,
} from 'react';
export type SearchBoxHandle = {
focus: () => void;
select: () => void;
measure: () => DOMRect | null;
};
type SearchBoxProps = {
initialValue?: string;
ref?: Ref<SearchBoxHandle>;
};
export function SearchBox({
initialValue = '',
ref,
}: SearchBoxProps) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus({
preventScroll: true,
});
},
select() {
inputRef.current?.select();
},
measure() {
return inputRef.current?.getBoundingClientRect() ?? null;
},
}), []);
return (
<input
ref={inputRef}
defaultValue={initialValue}
aria-label="搜索"
/>
);
}
父组件:
import { useRef } from 'react';
import {
SearchBox,
type SearchBoxHandle,
} from './SearchBox';
export function SearchPage() {
const searchBoxRef = useRef<SearchBoxHandle>(null);
function handleMeasure() {
const rect = searchBoxRef.current?.measure();
if (!rect) {
console.log('输入框尚未挂载,或已经卸载');
return;
}
console.log({
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
});
}
return (
<main>
<SearchBox
ref={searchBoxRef}
initialValue="React Ref"
/>
<div>
<button onClick={() => searchBoxRef.current?.focus()}>
聚焦
</button>
<button onClick={() => searchBoxRef.current?.select()}>
全选
</button>
<button onClick={handleMeasure}>
测量
</button>
</div>
</main>
);
}
调用路径是:
点击“聚焦”
│
├─ 父组件读取 searchBoxRef.current
├─ 调用 SearchBoxHandle.focus()
├─ 子组件读取 inputRef.current
└─ 浏览器将焦点设置到 input 元素
点击“测量”
│
├─ 父组件调用 SearchBoxHandle.measure()
├─ 子组件执行 input.getBoundingClientRect()
└─ 返回当前布局对应的 DOMRect
?. 不是多余的防御代码。父组件可能在以下时刻调用命令:
- 子组件尚未完成挂载;
- 条件渲染刚刚隐藏了子组件;
- 页面正在切换路由;
- 某个异步回调晚于组件卸载执行。
因此,句柄应当定义清楚“目标不存在时怎么办”。上面的实现选择返回 null 或静默不执行;另一种设计是抛出错误,但这只有在“不存在一定表示程序错误”时才合适。
五、测量 DOM:什么时候读才可靠
getBoundingClientRect() 返回元素相对于视口的矩形:
const rect = element.getBoundingClientRect();
rect.left; // 视口左边缘到元素左边缘的距离
rect.top; // 视口上边缘到元素上边缘的距离
rect.width; // 元素布局宽度
rect.height; // 元素布局高度
这些值通常是 CSS 像素,可以是小数;它们受布局、滚动、缩放和 CSS transform 影响。它不是文档坐标。如果需要文档坐标,常见计算是:
const documentLeft = rect.left + window.scrollX;
const documentTop = rect.top + window.scrollY;
在事件中测量
用户点击按钮时,DOM 已经存在,因此直接测量通常成立:
function handleClick() {
const rect = inputRef.current?.getBoundingClientRect();
console.log(rect?.width);
}
在 DOM 更新后测量
如果某个 state 改变会影响尺寸,需要在 DOM 提交之后读取:
import {
useLayoutEffect,
useRef,
useState,
} from 'react';
export function ResizablePanel() {
const [large, setLarge] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const element = panelRef.current;
if (!element) return;
const rect = element.getBoundingClientRect();
console.log('提交后的尺寸:', rect.width, rect.height);
}, [large]);
return (
<>
<button onClick={() => setLarge((value) => !value)}>
切换尺寸
</button>
<div
ref={panelRef}
style={{
width: large ? 400 : 200,
height: 100,
}}
/>
</>
);
}
useLayoutEffect 在 DOM 更新后、浏览器绘制前运行,适合“先测量,再决定同步布局”的场景,例如定位浮层。它可能阻塞绘制,因此不能把所有副作用都放进去;如果不需要避免首帧闪烁,普通 useEffect 通常更合适。
测量的因果链是:
setLarge(true)
│
├─ React 重新渲染 JSX
├─ React 提交 style.width = 400px
├─ useLayoutEffect 读取 getBoundingClientRect()
└─ 得到新布局的尺寸
如果在 setLarge(true) 之后、React 提交之前立即读取 DOM:
setLarge(true);
const width = panelRef.current?.getBoundingClientRect().width;
这里读到的可能仍是旧尺寸,因为 state 更新不会在这行代码中同步完成一次完整的渲染和提交。不要把“发起更新”和“DOM 已更新”当成同一时刻。
持续观察尺寸
如果尺寸会因窗口变化、字体加载或父布局变化而改变,仅在某次点击时测量不够。可以使用 ResizeObserver:
import { useLayoutEffect, useRef, useState } from 'react';
export function MeasuredBox() {
const boxRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
const element = boxRef.current;
if (!element) return;
const observer = new ResizeObserver(([entry]) => {
setWidth(entry.contentRect.width);
});
observer.observe(element);
return () => {
observer.disconnect();
};
}, []);
return (
<>
<div ref={boxRef} style={{ resize: 'horizontal', overflow: 'auto' }}>
拖动边缘改变尺寸
</div>
<p>内容宽度:{width}px</p>
</>
);
}
这里使用 state 是因为宽度要显示在 JSX 中。若只需要在命令执行时读取一次,就不必把每次测量结果存入 state。
六、回调 ref:需要在“节点绑定时”执行逻辑
对象 ref 的形式是:
const nodeRef = useRef<HTMLDivElement>(null);
return <div ref={nodeRef} />;
如果需要在节点挂载或解绑的瞬间执行逻辑,可以使用回调 ref:
import { useCallback } from 'react';
const setNode = useCallback((node: HTMLDivElement | null) => {
if (node) {
console.log('节点已绑定', node);
} else {
console.log('节点已解绑');
}
}, []);
return <div ref={setNode} />;
React 会在节点绑定时调用 setNode(node),解绑时调用 setNode(null)。在 React 19 中,回调 ref 还支持返回清理函数的形式;不过如果需要兼容更早版本,显式处理 null 是更通用的写法。
useCallback 的作用不是让 DOM 更快,而是稳定回调函数身份。若每次渲染都产生新的 ref 回调,React 可能先用 null 清理旧回调,再用节点调用新回调:
// 每次 render 都是新函数,可能导致反复解绑和绑定
return <div ref={(node) => console.log(node)} />;
这在普通场景中通常只是额外调用,但如果回调中注册了观察器或第三方实例,就可能造成重复初始化或清理时序错误。
七、命令式 API 的边界:暴露操作,不暴露内部结构
直接暴露 DOM 节点很方便:
useImperativeHandle(ref, () => inputRef.current);
但这会让父组件依赖子组件的内部实现。若子组件以后从 <input> 改成由多个元素组成,或者加入遮罩层,这个接口就会变得脆弱。
更稳定的方式是暴露语义化命令:
type DialogHandle = {
open: () => void;
close: () => void;
focusConfirmButton: () => void;
};
父组件只表达意图:
dialogRef.current?.open();
而不是操作内部节点:
dialogRef.current?.querySelector('.confirm')?.focus();
命令式 API 的设计边界可以形式化为:
父组件依赖 = 暴露句柄的类型和行为
父组件不依赖 = 子组件内部 DOM 层级和节点数量
句柄中的每个方法都应该有明确的前置条件和失败行为。例如:
type MenuHandle = {
open: () => void;
close: () => void;
focusFirstItem: () => boolean;
};
focusFirstItem() 返回 false 表示菜单当前没有可聚焦项,比无条件假设节点存在更容易测试和诊断。
一个带状态的句柄示例
import {
useImperativeHandle,
useRef,
useState,
type Ref,
} from 'react';
type DialogHandle = {
open: () => void;
close: () => void;
};
type DialogProps = {
ref?: Ref<DialogHandle>;
};
export function Dialog({ ref }: DialogProps) {
const [open, setOpen] = useState(false);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => ({
open() {
setOpen(true);
},
close() {
setOpen(false);
},
}), []);
return (
<div hidden={!open} role="dialog" aria-modal="true">
<p>对话框内容</p>
<button
ref={closeButtonRef}
onClick={() => setOpen(false)}
>
关闭
</button>
</div>
);
}
这里 open() 不是直接修改 DOM 的 hidden 属性,而是调用 setOpen(true),让 React 通过 state 重新渲染。命令式 API 只作为“触发状态转换”的入口,实际界面仍由 state 和 JSX 描述。
如果打开后还需要聚焦关闭按钮,不能在 setOpen(true) 后立即假设按钮已经可用:
open() {
setOpen(true);
closeButtonRef.current?.focus(); // 可能仍为 null,或元素仍处于 hidden
}
更可靠的方式是监听 open:
import { useEffect } from 'react';
useEffect(() => {
if (open) {
closeButtonRef.current?.focus();
}
}, [open]);
状态变化先导致渲染和提交,Effect 再访问已经存在的节点。若必须在绘制前完成聚焦或测量,可以根据视觉要求使用 useLayoutEffect,但仍要处理节点为空的情况。
八、Ref 与 React 的渲染模型
React 的渲染可能被开始、暂停、重新计算或丢弃。渲染阶段只是计算“应该是什么”,提交阶段才会把结果应用到 DOM 并更新 ref。
因此,不应在 render 中执行依赖真实 DOM 的操作:
function BadComponent() {
const ref = useRef<HTMLDivElement>(null);
// 不应在这里测量、聚焦或修改 DOM
const width = ref.current?.getBoundingClientRect().width;
return <div ref={ref}>{width}</div>;
}
这段代码还存在逻辑循环:width 不是由本次 JSX 提交后得到的尺寸,渲染结果无法可靠反映当前 DOM 的测量值。
正确的数据流是:
DOM 提交
│
├─ 读取 ref
├─ 测量 DOM
├─ setState(测量结果)
└─ 下一次渲染显示结果
例如:
function WidthLabel() {
const boxRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number | null>(null);
useLayoutEffect(() => {
const box = boxRef.current;
if (!box) return;
setWidth(box.getBoundingClientRect().width);
}, []);
return (
<>
<div ref={boxRef}>内容</div>
<span>{width === null ? '测量中' : `${width}px`}</span>
</>
);
}
开发环境下的 Strict Mode 可能让某些初始化和清理路径更容易暴露问题。不要用“只初始化一次”的假设掩盖缺少清理的资源;观察器、事件监听器和第三方实例都应在对应的清理阶段释放。
九、对象 ref 与回调 ref 的差异
对象 ref
const ref = useRef<HTMLButtonElement>(null);
<button ref={ref} />;
特点:
- 使用方通过
ref.current读取当前值; - 适合大多数 DOM 节点引用;
- React 会在提交和解绑时更新
current; current改变不会触发渲染。
回调 ref
const setButton = useCallback((button: HTMLButtonElement | null) => {
// 绑定或解绑时立即收到通知
}, []);
<button ref={setButton} />;
特点:
- 可以在绑定时立即执行自定义逻辑;
- 适合动态节点、第三方库初始化和资源清理;
- 必须考虑
null,因为节点会卸载或替换; - 回调身份不稳定时,可能产生额外的解绑和绑定。
useImperativeHandle 接受二者
父组件可以这样写:
const handleRef = useRef<SearchBoxHandle>(null);
<SearchBox ref={handleRef} />;
也可以这样写:
<SearchBox
ref={(handle) => {
if (handle) {
handle.focus();
}
}}
/>
组件内部不需要知道父组件使用的是对象 ref 还是回调 ref;React 负责把 useImperativeHandle 返回的值交给对应的 ref。
十、客户端与服务端边界
焦点和测量都依赖浏览器 DOM:
focus()是浏览器行为;getBoundingClientRect()只能在真实 DOM 存在时读取;ResizeObserver是浏览器 API;- ref 不应作为服务端与客户端之间需要序列化传输的数据。
在使用 React Server Components 的框架中,执行这些逻辑的组件必须位于客户端边界内。例如,在支持该约定的框架中,文件顶部通常需要:
'use client';
示意:
'use client';
import { useRef } from 'react';
export function ClientSearchPage() {
const ref = useRef<SearchBoxHandle>(null);
return <SearchBox ref={ref} />;
}
服务端渲染可以输出初始 HTML,但服务端不能执行:
window
document
element.focus()
element.getBoundingClientRect()
如果在服务端渲染阶段直接访问这些对象,会出现 window is not defined、document is not defined 或 hydration 相关问题。把 DOM 操作放在事件处理器、useEffect 或 useLayoutEffect 中,只能解决“执行时机”问题;组件本身仍必须处于允许使用客户端 Hook 和浏览器 API 的边界内。
十一、常见失败表现与诊断方法
1. ref.current 一直是 null
可能原因:
- 读取发生在首次 render 阶段;
- 目标节点被条件渲染隐藏或卸载;
ref没有真正传到子组件;- React 18 代码遗漏了
forwardRef; - 父组件调用早于子组件挂载;
- 使用了服务端组件而不是客户端组件。
可以按以下路径检查:
console.log('父组件句柄:', ref.current);
在子组件中检查内部节点:
console.log('内部节点:', inputRef.current);
如果父句柄存在但内部节点为空,说明句柄已经创建,但调用时机或条件渲染不满足。如果父句柄本身为空,则应检查 ref 传递和挂载时机。
2. 命令方法使用旧数据
表现:
界面已经显示新值,但 ref.current.logValue() 打印旧值
典型原因是依赖数组遗漏:
useImperativeHandle(ref, () => ({
logValue: () => console.log(value),
}), []);
修复:
useImperativeHandle(ref, () => ({
logValue: () => console.log(value),
}), [value]);
如果句柄需要保持对象身份稳定,也可以让方法通过一个 ref 读取最新值,但这会增加同步代码,只有在确实需要稳定句柄身份时才值得使用。
3. 测量值为零或不符合预期
常见原因包括:
display: none;
此时元素没有参与布局,矩形尺寸通常为零。其他原因还包括:
- 元素尚未提交;
- 测量发生在错误的 state 更新时机;
- 父元素尺寸尚未稳定;
- CSS transform 改变了视觉矩形;
- 字体尚未加载完成;
- 元素被替换或已经卸载;
- 测量的是外层包装元素,而不是实际内容元素。
诊断时应同时记录:
const element = ref.current;
console.log({
element,
connected: element?.isConnected,
rect: element?.getBoundingClientRect(),
computedDisplay: element
? getComputedStyle(element).display
: null,
});
这些信息可以区分“节点不存在”“节点不在布局中”和“节点存在但尺寸计算不同”三类问题。
4. 父组件过度依赖内部 DOM
如果父组件频繁调用:
ref.current?.querySelector(...)
ref.current?.classList.add(...)
ref.current?.style...
通常说明组件边界泄漏。此时应重新定义句柄,使其暴露业务操作,例如:
type DatePickerHandle = {
focusInput: () => void;
openCalendar: () => void;
clear: () => void;
};
这不是为了隐藏所有 DOM,而是为了让父组件依赖稳定行为,而不是依赖偶然的节点结构。
十二、何时不应该使用 Ref
如果问题可以通过 props 和 state 表达,就不必引入命令式 API。
不推荐:
// 父组件通过 ref 命令子组件改变显示状态
panelRef.current?.show();
更适合:
<Panel visible={visible} />
因为 visible 是可追踪、可组合、可测试的数据流。命令式句柄更适合不可自然表达为 JSX 输入的动作:
- 将焦点移动到某个 DOM 节点;
- 读取浏览器布局;
- 调用播放器、地图、编辑器等第三方实例;
- 触发组件内部必须保留封装的短暂操作。
可以用一个判断条件概括:
如果需求是“界面应该呈现什么” → 优先 props/state
如果需求是“对已存在对象执行什么动作” → 可以考虑 ref
十三、生产取舍
ref 的核心保证是:在 React 提交完成后,React 会把关联对象写入 ref,并在解绑时清除或通知回调;它不保证:
- 首次 render 时 ref 已经可用;
- 调用句柄时目标一定存在;
ref.current的变化会触发渲染;- 由 ref 读取的布局值会自动保持更新;
- 服务端可以执行浏览器操作;
- 句柄中的方法自动拥有最新的 props 和 state。
useImperativeHandle 的价值在于建立一个受控接口:
内部 DOM / 实例
│
▼
useImperativeHandle
│
▼
有限、可类型检查的命令式句柄
│
▼
父组件在事件或 Effect 中调用
焦点控制应在节点提交后执行;测量应在正确的布局时机执行;状态变化仍应由 state 驱动;句柄方法则应明确空节点、卸载、旧闭包和服务端边界。掌握这几条因果关系后,ref 就不再只是“绕过 React 的 DOM 指针”,而会成为声明式组件中一个边界清晰的外部操作接口。
系列导航与关联阅读
- 系列入口:React 完整学习路线:从渲染与 Hooks 到服务端组件和生产架构
- 上一篇:React useLayoutEffect 与 useInsertionEffect:时机、测量和闪烁
- 下一篇:React Portal:事件、焦点、层级、可访问性和弹窗架构
官方资料
本文依据 React 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论