React.forwardRef 完整指南
📌 快速概览
React.forwardRef 是一个 React API,用于将 ref 从父组件转发到子组件的 DOM 元素。
核心问题
在 React 中,props 不能直接传递 ref。下面的代码会 失效:
// ❌ 不能这样用
const MyButton = (props) => {
return <button ref={props.ref}>Click</button>; // 不工作
};
export default MyButton;
解决方案
使用 React.forwardRef 解决这个问题:
// ✅ 正确用法
const MyButton = React.forwardRef((props, ref) => {
return <button ref={ref}>Click</button>; // 正常工作
});
export default MyButton;
🎯 使用场景
1. 管理焦点(最常见)
场景:需要从父组件控制子组件 input 的焦点
// ❌ 问题代码
const TextInput = (props) => {
return <input />;
};
const Parent = () => {
const inputRef = useRef(null);
const handleFocus = () => {
// ❌ 这不会工作,inputRef.current 是组件实例,不是 DOM
inputRef.current.focus();
};
return (
<>
<TextInput ref={inputRef} />
<button onClick={handleFocus}>Focus Input</button>
</>
);
};
✅ 解决方案:
const TextInput = React.forwardRef((props, ref) => {
return <input ref={ref} />;
});
const Parent = () => {
const inputRef = useRef(null);
const handleFocus = () => {
// ✅ 现在可以正常工作
inputRef.current.focus();
};
return (
<>
<TextInput ref={inputRef} />
<button onClick={handleFocus}>Focus Input</button>
</>
);
};
2. 触发组件方法
场景:需要从父组件调用子组件的方法
// 可伸缩面板组件
const Collapse = React.forwardRef((props, ref) => {
const contentRef = useRef(null);
useImperativeHandle(ref, () => ({
// 暴露给父组件的方法
expand: () => {
contentRef.current.style.height = "auto";
},
collapse: () => {
contentRef.current.style.height = "0";
},
}));
return (
<div>
<div ref={contentRef} style={{ height: 0, overflow: "hidden" }}>
{props.children}
</div>
</div>
);
});
const Parent = () => {
const collapseRef = useRef(null);
return (
<div>
<Collapse ref={collapseRef}>
<p>This is collapsible content</p>
</Collapse>
<button onClick={() => collapseRef.current?.expand()}>Expand</button>
<button onClick={() => collapseRef.current?.collapse()}>Collapse</button>
</div>
);
};
3. 获取 DOM 属性或调用 DOM 方法
场景:获取 input 的值、video 的当前时间等
const VideoPlayer = React.forwardRef((props, ref) => {
return (
<video ref={ref} controls>
<source src={props.src} />
</video>
);
});
const PlayerController = () => {
const videoRef = useRef(null);
const handlePlay = () => videoRef.current?.play();
const handlePause = () => videoRef.current?.pause();
const handleSeek = (time) => {
videoRef.current.currentTime = time;
};
return (
<div>
<VideoPlayer ref={videoRef} src="video.mp4" />
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
<button onClick={() => handleSeek(30)}>Jump to 30s</button>
</div>
);
};
4. 组件库设计(最重要)
场景:创建可复用的 UI 组件库,允许用户访问底层 DOM
这正是 Button 组件的使用场景:
// Button 组件
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
type?: 'primary' | 'secondary';
loading?: boolean;
disabled?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { type = 'primary', loading, disabled, ...rest } = props;
return (
<button
ref={ref}
className={`btn btn-${type}`}
disabled={disabled || loading}
{...rest}
>
{loading && <Spinner />}
{props.children}
</button>
);
}
);
// 使用
const App = () => {
const buttonRef = useRef(null);
const handleClick = () => {
// 可以直接访问 button DOM 方法
buttonRef.current?.blur();
buttonRef.current?.focus();
};
return (
<>
<Button ref={buttonRef} type="primary">
Submit
</Button>
<button onClick={handleClick}>Control Button</button>
</>
);
};
💡 forwardRef API 详解
基础语法
const MyComponent = React.forwardRef((props, ref) => {
return <div ref={ref}>{props.children}</div>;
});
// 在 TypeScript 中
const MyComponent = React.forwardRef<HTMLDivElement, MyProps>(
(props, ref) => {
return <div ref={ref}>{props.children}</div>;
}
);
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
props | object | 组件的所有 props |
ref | Ref | 父组件传入的 ref |
返回值
返回一个新的 React 组件,可以接收 ref prop。
📚 详细代码示例
示例 1:受控输入框
// CustomInput.tsx
import React, { forwardRef, useRef, useImperativeHandle } from 'react';
interface CustomInputProps {
placeholder?: string;
defaultValue?: string;
}
interface CustomInputHandle {
focus: () => void;
blur: () => void;
clear: () => void;
getValue: () => string;
setValue: (value: string) => void;
}
const CustomInput = forwardRef<CustomInputHandle, CustomInputProps>(
(props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
// 使用 useImperativeHandle 暴露自定义方法
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
},
getValue: () => inputRef.current?.value || '',
setValue: (value: string) => {
if (inputRef.current) {
inputRef.current.value = value;
}
}
}));
return (
<input
ref={inputRef}
placeholder={props.placeholder}
defaultValue={props.defaultValue}
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px'
}}
/>
);
}
);
CustomInput.displayName = 'CustomInput';
export default CustomInput;
// 使用方式
const App = () => {
const inputRef = useRef<CustomInputHandle>(null);
return (
<div>
<CustomInput ref={inputRef} placeholder="Enter text" />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
<button onClick={() => inputRef.current?.clear()}>Clear</button>
<button onClick={() => {
const value = inputRef.current?.getValue();
console.log('Value:', value);
}}>Get Value</button>
<button onClick={() => inputRef.current?.setValue('Hello')}>
Set Value
</button>
</div>
);
};
示例 2:时刻表格组件
// Table.tsx
import React, { forwardRef, useRef, useImperativeHandle } from 'react';
interface TableHandle {
scrollToTop: () => void;
scrollToBottom: () => void;
scroll: (offset: number) => void;
getScrollPosition: () => number;
}
interface TableProps {
data: Array<any>;
columns: Array<any>;
}
const Table = forwardRef<TableHandle, TableProps>(({ data, columns }, ref) => {
const tableRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
scrollToTop: () => {
if (tableRef.current) {
tableRef.current.scrollTop = 0;
}
},
scrollToBottom: () => {
if (tableRef.current) {
tableRef.current.scrollTop = tableRef.current.scrollHeight;
}
},
scroll: (offset: number) => {
if (tableRef.current) {
tableRef.current.scrollTop += offset;
}
},
getScrollPosition: () => {
return tableRef.current?.scrollTop || 0;
}
}));
return (
<div
ref={tableRef}
style={{
height: '300px',
overflow: 'auto',
border: '1px solid #ddd'
}}
>
<table style={{ width: '100%' }}>
<thead>
<tr>
{columns.map(col => (
<th key={col.key}>{col.title}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, idx) => (
<tr key={idx}>
{columns.map(col => (
<td key={col.key}>{row[col.key]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
});
Table.displayName = 'Table';
export default Table;
// 使用
const App = () => {
const tableRef = useRef<TableHandle>(null);
return (
<div>
<Table
ref={tableRef}
columns={[
{ key: 'name', title: 'Name' },
{ key: 'age', title: 'Age' }
]}
data={[
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 }
]}
/>
<button onClick={() => tableRef.current?.scrollToTop()}>
Scroll to Top
</button>
<button onClick={() => tableRef.current?.scrollToBottom()}>
Scroll to Bottom
</button>
</div>
);
};
示例 3:Modal 对话框
// Modal.tsx
import React, { forwardRef, useRef, useImperativeHandle, useState } from 'react';
interface ModalHandle {
open: () => void;
close: () => void;
isOpen: () => boolean;
}
interface ModalProps {
title: string;
children: React.ReactNode;
}
const Modal = forwardRef<ModalHandle, ModalProps>(
({ title, children }, ref) => {
const [isVisible, setIsVisible] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setIsVisible(true),
close: () => setIsVisible(false),
isOpen: () => isVisible
}));
if (!isVisible) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
minWidth: '400px'
}}>
<h2>{title}</h2>
{children}
<button onClick={() => setIsVisible(false)}>Close</button>
</div>
</div>
);
}
);
Modal.displayName = 'Modal';
export default Modal;
// 使用
const App = () => {
const modalRef = useRef<ModalHandle>(null);
return (
<div>
<button onClick={() => modalRef.current?.open()}>Open Modal</button>
<Modal ref={modalRef} title="Confirm Action">
<p>Are you sure?</p>
</Modal>
</div>
);
};
🔑 关键要点
1. 必须搭配 useImperativeHandle
当需要暴露自定义方法时,使用 useImperativeHandle:
useImperativeHandle(ref, () => ({
// 暴露的方法
method1: () => { ... },
method2: () => { ... }
}));
2. TypeScript 类型声明
// 定义 Ref 的类型
type InputHandle = {
focus: () => void;
blur: () => void;
};
// 定义 Props 的类型
type InputProps = {
placeholder?: string;
};
// 声明组件
const Input = forwardRef<InputHandle, InputProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur()
}));
return <input ref={inputRef} {...props} />;
});
3. displayName 属性
添加 displayName 便于调试和 React DevTools 识别:
CustomComponent.displayName = "CustomComponent";
4. 不要过度使用
❌ 不好的做法:过度暴露 DOM API
// 不推荐:暴露所有 DOM 方法
useImperativeHandle(ref, () => inputRef.current);
✅ 好的做法:只暴露必要的接口
// 推荐:只暴露需要的方法
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
setValue: (value) => { ... }
}));
🏆 最佳实践
1. 规范化命名
// ✅ 好
const Button = forwardRef<HTMLButtonElement, ButtonProps>(...);
// ❌ 不好
const Btn = forwardRef((props, ref) => ...);
2. 完整的 TypeScript 支持
// ✅ 推荐
interface ComponentHandle {
method1: () => void;
method2: (param: string) => void;
}
interface ComponentProps {
prop1: string;
}
const Component = forwardRef<ComponentHandle, ComponentProps>(
(props, ref) => { ... }
);
// ❌ 不推荐
const Component = forwardRef((props, ref) => { ... });
3. 添加 displayName
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
return <button ref={ref}>{props.children}</button>;
});
Button.displayName = 'Button'; // ✅ 添加这一行
4. 优雅降级
// 使用可选链操作符
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = "";
}
},
}));
⚠️ 常见问题
Q1: 为什么 forwardRef 中不能直接使用 ref?
答:因为 ref 不是 prop,它是特殊的 API。React 会将其单独处理。
Q2: forwardRef 性能影响大吗?
答:性能影响微乎其微,可以忽略。
Q3: 函数组件可以直接接收 ref 吗?
答:不能。必须使用 forwardRef 包装。
// ❌ 不能
const MyComponent = (props, ref) => <div ref={ref} />;
// ✅ 必须这样
const MyComponent = forwardRef((props, ref) => <div ref={ref} />);
Q4: 什么时候应该使用 forwardRef?
答:以下情况使用:
- ✅ 需要访问 DOM 元素(焦点、滚动等)
- ✅ 需要触发 DOM 方法(play、pause 等)
- ✅ 创建 UI 组件库
- ✅ 需要集成第三方 DOM 库
不应该使用:
- ❌ 仅为了传递 props(使用 children 或 props)
- ❌ 管理数据状态(使用 state)
🎓 实战案例(Button 组件)
我们的 Button 组件为什么要使用 forwardRef:
// src/button/index.tsx
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props: ButtonProps, ref) => {
const {
type = "normal",
size = "medium",
// ... 其他 props
} = props;
return (
<button
{...others}
ref={ref} // ✅ 转发 ref 到 DOM button
className={cls}
// ... 其他属性
>
{children}
</button>
);
}
);
使用示例:
// 父组件
const App = () => {
const buttonRef = useRef < HTMLButtonElement > null;
const handleClick = () => {
// 现在可以直接控制按钮元素
buttonRef.current?.focus();
buttonRef.current?.blur();
// 获取 button 属性
console.log(buttonRef.current?.disabled);
console.log(buttonRef.current?.innerText);
};
return (
<>
<Button ref={buttonRef} type="primary">
Click me
</Button>
<button onClick={handleClick}>Control Button</button>
</>
);
};
📖 参考资源
✨ 总结
| 特性 | 说明 |
|---|---|
| 用途 | 将 ref 从父组件转发到子组件 DOM 元素 |
| 何时使用 | 访问 DOM、调用 DOM 方法、创建组件库 |
| 搭配工具 | useImperativeHandle(暴露自定义方法) |
| 性能 | 无明显影响 |
| 可维护性 | 高(清晰的公开接口) |
| 复杂性 | 低(API 简单易用) |
forwardRef 是创建专业级 React 组件库的必需工具! 🚀