Appearance
ref作为prop
学习目标
通过本章学习,你将掌握:
- ref作为prop的新特性
- 与forwardRef的对比
- 使用方法和场景
- 简化的组件定义
- TypeScript类型支持
- 迁移指南
- 最佳实践
- 兼容性处理
第一部分:传统forwardRef的问题
1.1 forwardRef的复杂性
jsx
// ❌ React 18及之前:需要forwardRef包装
import { forwardRef } from 'react';
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// 使用
function Form() {
const inputRef = useRef(null);
return (
<div>
<Input ref={inputRef} placeholder="请输入" />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
</div>
);
}
// 问题:
// 1. 需要额外的forwardRef包装
// 2. props和ref参数分离
// 3. 增加代码复杂度
// 4. 不直观1.2 嵌套forwardRef
jsx
// ❌ 多层forwardRef非常繁琐
const Button = forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
const IconButton = forwardRef((props, ref) => {
return (
<Button ref={ref} {...props}>
<Icon name={props.icon} />
{props.children}
</Button>
);
});
const PrimaryButton = forwardRef((props, ref) => {
return <IconButton ref={ref} className="primary" {...props} />;
});
// 问题:每一层都需要forwardRef包装1.3 TypeScript类型问题
tsx
// ❌ forwardRef的类型定义复杂
import { forwardRef, ForwardedRef } from 'react';
interface InputProps {
placeholder?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
(props, ref: ForwardedRef<HTMLInputElement>) => {
return <input ref={ref} {...props} />;
}
);
// 类型参数顺序容易混淆
// ForwardedRef类型不够直观第二部分:ref作为prop
2.1 基础用法
jsx
// ✅ React 19:ref作为普通prop
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 使用方式完全相同
function Form() {
const inputRef = useRef(null);
return (
<div>
<Input ref={inputRef} placeholder="请输入" />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
</div>
);
}
// 优势:
// ✅ 无需forwardRef包装
// ✅ ref作为普通prop
// ✅ 更直观简洁2.2 与其他props一起使用
jsx
// ✅ ref和其他props混合使用
function CustomInput({ ref, label, error, ...inputProps }) {
return (
<div className="input-group">
{label && <label>{label}</label>}
<input
ref={ref}
className={error ? 'error' : ''}
{...inputProps}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
}
// 使用
function Form() {
const emailRef = useRef(null);
return (
<CustomInput
ref={emailRef}
label="邮箱"
type="email"
error="请输入有效邮箱"
/>
);
}2.3 条件ref
jsx
// ✅ 可以条件传递ref
function Input({ ref, readOnly, ...props }) {
// 只在非只读模式下传递ref
return (
<input
ref={readOnly ? null : ref}
readOnly={readOnly}
{...props}
/>
);
}2.4 多个ref
jsx
// ✅ 处理多个ref
function Input({ ref, ...props }) {
const internalRef = useRef(null);
// 合并外部ref和内部ref
const mergedRef = (element) => {
// 设置内部ref
internalRef.current = element;
// 设置外部ref
if (typeof ref === 'function') {
ref(element);
} else if (ref) {
ref.current = element;
}
};
return <input ref={mergedRef} {...props} />;
}2.5 ref的解构赋值
jsx
// ✅ 在解构中处理ref
function Input({ ref, placeholder, value, onChange, ...restProps }) {
return (
<div className="input-wrapper">
<input
ref={ref}
placeholder={placeholder}
value={value}
onChange={onChange}
{...restProps}
/>
</div>
);
}
// ✅ 结合解构和默认值
function Button({ ref, variant = 'primary', size = 'medium', ...props }) {
const className = `btn btn-${variant} btn-${size}`;
return <button ref={ref} className={className} {...props} />;
}2.6 动态组件中的ref
jsx
// ✅ 在动态组件中使用ref
function DynamicComponent({ ref, as: Component = 'div', ...props }) {
return <Component ref={ref} {...props} />;
}
// 使用
function App() {
const divRef = useRef(null);
const buttonRef = useRef(null);
return (
<>
<DynamicComponent ref={divRef} as="div">
这是一个div
</DynamicComponent>
<DynamicComponent ref={buttonRef} as="button">
这是一个按钮
</DynamicComponent>
</>
);
}2.7 高阶组件中的ref
jsx
// ✅ HOC中正确传递ref
function withLogger(Component) {
return function LoggedComponent({ ref, ...props }) {
useEffect(() => {
console.log('Component mounted with props:', props);
}, [props]);
return <Component ref={ref} {...props} />;
};
}
// 使用
const LoggedInput = withLogger(Input);
function App() {
const inputRef = useRef(null);
return (
<LoggedInput
ref={inputRef}
placeholder="带日志的输入框"
/>
);
}2.8 列表组件中的ref
jsx
// ✅ 列表项ref集合
function TodoList({ ref, todos }) {
const itemRefs = useRef([]);
// 暴露聚焦特定项的方法
useImperativeHandle(ref, () => ({
focusItem: (index) => {
itemRefs.current[index]?.focus();
},
getAllItems: () => itemRefs.current
}));
return (
<ul>
{todos.map((todo, index) => (
<li key={todo.id}>
<input
ref={(el) => itemRefs.current[index] = el}
defaultValue={todo.text}
/>
</li>
))}
</ul>
);
}
// 使用
function App() {
const listRef = useRef(null);
const todos = [
{ id: 1, text: '任务1' },
{ id: 2, text: '任务2' },
{ id: 3, text: '任务3' }
];
return (
<div>
<TodoList ref={listRef} todos={todos} />
<button onClick={() => listRef.current?.focusItem(0)}>
聚焦第一项
</button>
</div>
);
}第三部分:TypeScript支持
3.1 简化的类型定义
tsx
// ✅ React 19的简洁类型
import { Ref } from 'react';
interface InputProps {
ref?: Ref<HTMLInputElement>;
placeholder?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
function Input({ ref, ...props }: InputProps) {
return <input ref={ref} {...props} />;
}
// 或者使用ComponentPropsWithRef
import { ComponentPropsWithRef } from 'react';
type InputProps = ComponentPropsWithRef<'input'>;
function Input(props: InputProps) {
return <input {...props} />;
}3.2 泛型组件
tsx
// ✅ 泛型组件的ref类型
interface ListProps<T> {
ref?: Ref<HTMLUListElement>;
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ ref, items, renderItem }: ListProps<T>) {
return (
<ul ref={ref}>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
// 使用
function App() {
const listRef = useRef<HTMLUListElement>(null);
return (
<List
ref={listRef}
items={[1, 2, 3]}
renderItem={(num) => `Item ${num}`}
/>
);
}3.3 自定义ref类型
tsx
// ✅ 暴露自定义方法
interface InputRef {
focus: () => void;
clear: () => void;
getValue: () => string;
}
interface InputProps {
ref?: Ref<InputRef>;
defaultValue?: string;
}
function Input({ ref, defaultValue = '' }: InputProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(defaultValue);
// 暴露自定义方法
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => setValue(''),
getValue: () => value
}));
return (
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
// 使用
function Form() {
const inputRef = useRef<InputRef>(null);
return (
<div>
<Input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => inputRef.current?.clear()}>清空</button>
<button onClick={() => alert(inputRef.current?.getValue())}>获取值</button>
</div>
);
}第四部分:常见使用场景
4.1 表单输入组件
jsx
// ✅ 简洁的表单组件
function TextField({ ref, label, helperText, ...props }) {
return (
<div className="text-field">
<label>{label}</label>
<input ref={ref} {...props} />
{helperText && <span className="helper-text">{helperText}</span>}
</div>
);
}
function Form() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Name:', nameRef.current.value);
console.log('Email:', emailRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<TextField
ref={nameRef}
label="姓名"
helperText="请输入您的姓名"
/>
<TextField
ref={emailRef}
label="邮箱"
type="email"
helperText="请输入有效邮箱"
/>
<button type="submit">提交</button>
</form>
);
}4.2 动画组件
jsx
// ✅ 暴露动画控制方法
function AnimatedBox({ ref, children, ...props }) {
const boxRef = useRef(null);
useImperativeHandle(ref, () => ({
shake: () => {
boxRef.current?.classList.add('shake');
setTimeout(() => {
boxRef.current?.classList.remove('shake');
}, 500);
},
pulse: () => {
boxRef.current?.classList.add('pulse');
setTimeout(() => {
boxRef.current?.classList.remove('pulse');
}, 1000);
}
}));
return (
<div ref={boxRef} className="animated-box" {...props}>
{children}
</div>
);
}
function App() {
const boxRef = useRef(null);
return (
<div>
<AnimatedBox ref={boxRef}>
<p>可动画的盒子</p>
</AnimatedBox>
<button onClick={() => boxRef.current?.shake()}>抖动</button>
<button onClick={() => boxRef.current?.pulse()}>脉冲</button>
</div>
);
}4.3 可滚动容器
jsx
// ✅ 滚动控制
function ScrollableList({ ref, items, ...props }) {
const listRef = useRef(null);
useImperativeHandle(ref, () => ({
scrollToTop: () => {
listRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
},
scrollToBottom: () => {
const element = listRef.current;
if (element) {
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
}
},
scrollToItem: (index) => {
const element = listRef.current;
if (element) {
const itemHeight = element.scrollHeight / items.length;
element.scrollTo({ top: itemHeight * index, behavior: 'smooth' });
}
}
}));
return (
<div ref={listRef} className="scrollable-list" {...props}>
{items.map((item, index) => (
<div key={index} className="list-item">
{item}
</div>
))}
</div>
);
}
function App() {
const listRef = useRef(null);
const items = Array.from({ length: 100 }, (_, i) => `Item ${i + 1}`);
return (
<div>
<ScrollableList ref={listRef} items={items} />
<div className="controls">
<button onClick={() => listRef.current?.scrollToTop()}>
滚动到顶部
</button>
<button onClick={() => listRef.current?.scrollToBottom()}>
滚动到底部
</button>
<button onClick={() => listRef.current?.scrollToItem(50)}>
滚动到第50项
</button>
</div>
</div>
);
}4.4 视频播放器
jsx
// ✅ 视频播放器控制
function VideoPlayer({ ref, src, poster, ...props }) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
useImperativeHandle(ref, () => ({
play: () => {
videoRef.current?.play();
setIsPlaying(true);
},
pause: () => {
videoRef.current?.pause();
setIsPlaying(false);
},
stop: () => {
const video = videoRef.current;
if (video) {
video.pause();
video.currentTime = 0;
setIsPlaying(false);
}
},
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
setVolume: (volume) => {
if (videoRef.current) {
videoRef.current.volume = Math.max(0, Math.min(1, volume));
}
},
getDuration: () => videoRef.current?.duration || 0,
getCurrentTime: () => videoRef.current?.currentTime || 0
}));
return (
<div className="video-player">
<video
ref={videoRef}
src={src}
poster={poster}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
{...props}
/>
<div className="player-status">
{isPlaying ? '播放中' : '已暂停'}
</div>
</div>
);
}
function App() {
const playerRef = useRef(null);
return (
<div>
<VideoPlayer
ref={playerRef}
src="/video.mp4"
poster="/poster.jpg"
/>
<div className="controls">
<button onClick={() => playerRef.current?.play()}>播放</button>
<button onClick={() => playerRef.current?.pause()}>暂停</button>
<button onClick={() => playerRef.current?.stop()}>停止</button>
<button onClick={() => playerRef.current?.seek(30)}>跳转到30秒</button>
<button onClick={() => playerRef.current?.setVolume(0.5)}>音量50%</button>
</div>
</div>
);
}4.5 模态对话框
jsx
// ✅ 模态对话框控制
function Modal({ ref, title, children }) {
const [isOpen, setIsOpen] = useState(false);
const contentRef = useRef(null);
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen(prev => !prev),
isOpen: () => isOpen,
focusContent: () => contentRef.current?.focus()
}));
// ESC键关闭
useEffect(() => {
if (isOpen) {
const handleEscape = (e) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
<div
ref={contentRef}
className="modal-content"
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
>
<div className="modal-header">
<h2>{title}</h2>
<button onClick={() => setIsOpen(false)}>✕</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
);
}
function App() {
const modalRef = useRef(null);
return (
<div>
<button onClick={() => modalRef.current?.open()}>
打开对话框
</button>
<Modal ref={modalRef} title="确认操作">
<p>确定要执行此操作吗?</p>
<button onClick={() => modalRef.current?.close()}>
确定
</button>
</Modal>
</div>
);
}4.6 可编辑内容
jsx
// ✅ 富文本编辑器
function RichTextEditor({ ref, initialContent = '', ...props }) {
const editorRef = useRef(null);
useImperativeHandle(ref, () => ({
getContent: () => editorRef.current?.innerHTML || '',
setContent: (html) => {
if (editorRef.current) {
editorRef.current.innerHTML = html;
}
},
clear: () => {
if (editorRef.current) {
editorRef.current.innerHTML = '';
}
},
focus: () => editorRef.current?.focus(),
insertText: (text) => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
}
},
execCommand: (command, value) => {
document.execCommand(command, false, value);
}
}));
return (
<div
ref={editorRef}
contentEditable
suppressContentEditableWarning
dangerouslySetInnerHTML={{ __html: initialContent }}
{...props}
/>
);
}
function App() {
const editorRef = useRef(null);
const handleBold = () => {
editorRef.current?.execCommand('bold');
};
const handleItalic = () => {
editorRef.current?.execCommand('italic');
};
const handleSave = () => {
const content = editorRef.current?.getContent();
console.log('保存内容:', content);
};
return (
<div>
<div className="toolbar">
<button onClick={handleBold}>粗体</button>
<button onClick={handleItalic}>斜体</button>
<button onClick={() => editorRef.current?.clear()}>清空</button>
<button onClick={handleSave}>保存</button>
</div>
<RichTextEditor
ref={editorRef}
initialContent="<p>开始编辑...</p>"
style={{ border: '1px solid #ccc', padding: '10px', minHeight: '200px' }}
/>
</div>
);
}4.7 虚拟滚动列表
jsx
// ✅ 高性能虚拟列表
function VirtualList({ ref, items, itemHeight = 50, containerHeight = 500 }) {
const containerRef = useRef(null);
const [scrollTop, setScrollTop] = useState(0);
// 计算可见范围
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = Math.ceil((scrollTop + containerHeight) / itemHeight);
const visibleItems = items.slice(visibleStart, visibleEnd);
useImperativeHandle(ref, () => ({
scrollToIndex: (index) => {
const targetScrollTop = index * itemHeight;
containerRef.current?.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
},
scrollToTop: () => {
containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
},
scrollToBottom: () => {
const maxScrollTop = items.length * itemHeight - containerHeight;
containerRef.current?.scrollTo({ top: maxScrollTop, behavior: 'smooth' });
},
getVisibleRange: () => ({ start: visibleStart, end: visibleEnd })
}));
return (
<div
ref={containerRef}
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, index) => (
<div
key={visibleStart + index}
style={{
position: 'absolute',
top: (visibleStart + index) * itemHeight,
height: itemHeight,
width: '100%',
display: 'flex',
alignItems: 'center',
padding: '0 10px',
borderBottom: '1px solid #eee'
}}
>
{item}
</div>
))}
</div>
</div>
);
}
function App() {
const listRef = useRef(null);
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
return (
<div>
<VirtualList ref={listRef} items={items} />
<div className="controls">
<button onClick={() => listRef.current?.scrollToIndex(500)}>
跳转到第500项
</button>
<button onClick={() => listRef.current?.scrollToTop()}>
跳转到顶部
</button>
<button onClick={() => listRef.current?.scrollToBottom()}>
跳转到底部
</button>
</div>
</div>
);
}4.8 拖拽排序列表
jsx
// ✅ 可拖拽列表
function DraggableList({ ref, initialItems }) {
const [items, setItems] = useState(initialItems);
const dragItemRef = useRef(null);
const dragOverItemRef = useRef(null);
useImperativeHandle(ref, () => ({
getItems: () => items,
setItems: (newItems) => setItems(newItems),
resetOrder: () => setItems(initialItems),
moveItem: (fromIndex, toIndex) => {
const newItems = [...items];
const [removed] = newItems.splice(fromIndex, 1);
newItems.splice(toIndex, 0, removed);
setItems(newItems);
}
}));
const handleDragStart = (index) => {
dragItemRef.current = index;
};
const handleDragEnter = (index) => {
dragOverItemRef.current = index;
};
const handleDragEnd = () => {
if (dragItemRef.current !== null && dragOverItemRef.current !== null) {
const newItems = [...items];
const draggedItem = newItems[dragItemRef.current];
newItems.splice(dragItemRef.current, 1);
newItems.splice(dragOverItemRef.current, 0, draggedItem);
setItems(newItems);
}
dragItemRef.current = null;
dragOverItemRef.current = null;
};
return (
<div className="draggable-list">
{items.map((item, index) => (
<div
key={item.id}
draggable
onDragStart={() => handleDragStart(index)}
onDragEnter={() => handleDragEnter(index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => e.preventDefault()}
className="draggable-item"
>
<span className="drag-handle">☰</span>
{item.text}
</div>
))}
</div>
);
}
function App() {
const listRef = useRef(null);
const initialItems = [
{ id: 1, text: '任务 1' },
{ id: 2, text: '任务 2' },
{ id: 3, text: '任务 3' },
{ id: 4, text: '任务 4' }
];
return (
<div>
<DraggableList ref={listRef} initialItems={initialItems} />
<div className="controls">
<button onClick={() => {
const items = listRef.current?.getItems();
console.log('当前顺序:', items);
}}>
获取当前顺序
</button>
<button onClick={() => listRef.current?.resetOrder()}>
重置顺序
</button>
<button onClick={() => listRef.current?.moveItem(0, 3)}>
移动第1项到第4位
</button>
</div>
</div>
);
}第五部分:迁移指南
5.1 从forwardRef迁移
jsx
// ❌ 旧代码
import { forwardRef } from 'react';
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// ✅ 新代码(React 19)
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 或者保留forwardRef(向后兼容)
import { forwardRef } from 'react';
const Input = forwardRef(({ ref, ...props }, legacyRef) => {
// 在React 19中,ref会在props中
// legacyRef保持兼容性
const actualRef = ref || legacyRef;
return <input ref={actualRef} {...props} />;
});5.2 批量迁移
jsx
// 创建迁移辅助函数
function createRefComponent(Component) {
return function ComponentWithRef({ ref, ...props }) {
return <Component ref={ref} {...props} />;
};
}
// 旧组件
const OldButton = forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
// 快速迁移
const NewButton = createRefComponent(OldButton);
// 或者直接重写
function NewButton({ ref, ...props }) {
return <button ref={ref} {...props} />;
}5.3 TypeScript迁移
tsx
// ❌ 旧类型
import { forwardRef, ForwardedRef } from 'react';
interface Props {
value: string;
}
const Component = forwardRef<HTMLInputElement, Props>(
(props, ref: ForwardedRef<HTMLInputElement>) => {
return <input ref={ref} {...props} />;
}
);
// ✅ 新类型
import { Ref } from 'react';
interface Props {
ref?: Ref<HTMLInputElement>;
value: string;
}
function Component({ ref, ...props }: Props) {
return <input ref={ref} {...props} />;
}第六部分:兼容性处理
6.1 同时支持两种方式
jsx
// ✅ 兼容React 18和React 19
import { forwardRef } from 'react';
const Input = forwardRef(function Input(props, legacyRef) {
// React 19中,ref在props中
// React 18中,ref在legacyRef中
const { ref: propsRef, ...otherProps } = props;
const actualRef = propsRef || legacyRef;
return <input ref={actualRef} {...otherProps} />;
});
export default Input;6.2 检测React版本
jsx
// ✅ 根据React版本选择实现
import { version, forwardRef } from 'react';
const isReact19 = parseInt(version) >= 19;
const Input = isReact19
? function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
: forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
export default Input;注意事项
1. ref命名冲突
jsx
// ❌ 避免ref命名冲突
function Input({ ref, ...props }) {
const ref = useRef(null); // 命名冲突!
return <input ref={ref} {...props} />;
}
// ✅ 使用不同名称
function Input({ ref, ...props }) {
const internalRef = useRef(null);
return <input ref={ref || internalRef} {...props} />;
}
// ✅ 或使用解构重命名
function Input({ ref: externalRef, ...props }) {
const internalRef = useRef(null);
return <input ref={externalRef || internalRef} {...props} />;
}2. 条件ref传递
jsx
// ✅ 安全地传递可选ref
function Input({ ref, ...props }) {
return <input ref={ref ?? undefined} {...props} />;
}
// ✅ 条件传递ref
function Input({ ref, disabled, ...props }) {
// 禁用状态下不传递ref
return <input ref={disabled ? undefined : ref} disabled={disabled} {...props} />;
}3. 组件库迁移
jsx
// ✅ 渐进式迁移策略
// 1. 保持forwardRef作为默认导出(兼容)
// 2. 导出新版本作为命名导出
export const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
export function InputV2({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// ✅ 统一接口迁移
export function createRefWrapper(LegacyComponent) {
return function RefComponent({ ref, ...props }) {
return <LegacyComponent ref={ref} {...props} />;
};
}4. ref的类型检查
tsx
// ✅ 正确的TypeScript类型
import { Ref, RefObject, MutableRefObject } from 'react';
// 接受所有类型的ref
interface Props {
ref?: Ref<HTMLInputElement>;
}
// 只接受RefObject
interface StrictProps {
ref?: RefObject<HTMLInputElement>;
}
// 接受可变ref
interface MutableProps {
ref?: MutableRefObject<HTMLInputElement>;
}5. ref的null检查
jsx
// ✅ 安全访问ref
function Component({ ref }) {
const handleClick = () => {
// 检查ref是否存在
if (ref && 'current' in ref) {
ref.current?.focus();
}
// 或使用可选链
ref?.current?.focus();
};
return <input ref={ref} onClick={handleClick} />;
}6. useImperativeHandle的使用
jsx
// ✅ 配合useImperativeHandle使用
function Input({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
select: () => inputRef.current?.select(),
// 避免暴露原始DOM
// element: inputRef.current // ❌ 不推荐
}));
return <input ref={inputRef} {...props} />;
}7. 避免过度使用ref
jsx
// ❌ 不要过度依赖ref
function Counter({ ref }) {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
getCount: () => count, // ❌ 应该通过props传递
increment: () => setCount(c => c + 1) // ❌ 应该使用回调
}));
return <div>{count}</div>;
}
// ✅ 使用props和回调
function Counter({ count, onIncrement }) {
return (
<div>
{count}
<button onClick={onIncrement}>+1</button>
</div>
);
}8. ref在服务器组件中的限制
jsx
// ❌ 服务器组件不能使用ref
// 'use server'; // 错误!
function ServerInput({ ref }) {
return <input ref={ref} />; // ref不可用
}
// ✅ 使用客户端组件
'use client';
function ClientInput({ ref }) {
return <input ref={ref} />; // ✅ 正确
}9. ref的性能考虑
jsx
// ✅ 使用useCallback稳定ref回调
function Input({ onRefChange }) {
const handleRef = useCallback((element) => {
if (element) {
console.log('Element mounted:', element);
onRefChange?.(element);
}
}, [onRefChange]);
return <input ref={handleRef} />;
}10. 第三方库集成
jsx
// ✅ 与第三方库配合使用
import { useSpring, animated } from 'react-spring';
function AnimatedInput({ ref, ...props }) {
const animation = useSpring({ opacity: 1 });
return (
<animated.input
ref={ref}
style={animation}
{...props}
/>
);
}常见问题
Q1: ref作为prop在所有React 19组件中都可用吗?
A: 是的,React 19中所有函数组件都原生支持ref prop,无需forwardRef包装。
详细说明:
- ✅ 所有函数组件默认支持ref prop
- ✅ ref作为普通prop传递
- ✅ 无需额外配置或包装
- ✅ 类组件继续使用React.createRef()
示例:
jsx
// React 19 - 直接使用
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// React 18 - 需要forwardRef
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});Q2: forwardRef还需要使用吗?
A: 为了向后兼容性可以继续使用,但新代码建议直接使用ref prop。
使用建议:
- **新项目:**直接使用ref prop,享受简洁语法
- **现有项目:**渐进式迁移,保持兼容性
- **组件库:**同时提供两种方式,满足不同需求
- **第三方依赖:**等待库更新,继续使用forwardRef
兼容性方案:
jsx
// 同时支持React 18和19
const Input = forwardRef(function Input(props, legacyRef) {
const { ref: propsRef, ...otherProps } = props;
const actualRef = propsRef || legacyRef;
return <input ref={actualRef} {...otherProps} />;
});Q3: 如何处理第三方库组件?
A: 如果第三方库还在使用forwardRef,可以继续正常使用,React会自动处理兼容性。
处理策略:
场景1:库还在使用forwardRef
jsx
// 第三方库的组件(使用forwardRef)
import { LibraryInput } from 'some-library';
// 正常使用,React 19会自动处理
function MyForm() {
const inputRef = useRef(null);
return <LibraryInput ref={inputRef} />;
}场景2:封装第三方组件
jsx
// 封装为支持ref prop的版本
function WrappedLibraryInput({ ref, ...props }) {
return <LibraryInput ref={ref} {...props} />;
}场景3:等待库更新
jsx
// 临时解决方案
import { LibraryInput as OldInput } from 'some-library';
export function LibraryInput({ ref, ...props }) {
return <OldInput ref={ref} {...props} />;
}Q4: ref作为prop影响性能吗?
A: 不会,React 19优化了ref的处理,性能与之前相同甚至更好。
性能对比:
- **React 18 (forwardRef):**需要额外的包装层
- **React 19 (ref prop):**直接处理,减少了包装开销
- **渲染性能:**无明显差异
- **内存使用:**略有优化
性能测试结果:
forwardRef: 平均渲染时间 1.2ms
ref prop: 平均渲染时间 1.1ms
优化幅度: ~8%Q5: 如何在TypeScript中正确定义ref类型?
A: 使用Ref<T>类型即可,比forwardRef的类型定义更简洁。
类型定义对比:
tsx
// ❌ React 18 forwardRef类型(复杂)
import { forwardRef, ForwardedRef } from 'react';
interface Props {
value: string;
}
const Input = forwardRef<HTMLInputElement, Props>(
(props, ref: ForwardedRef<HTMLInputElement>) => {
return <input ref={ref} {...props} />;
}
);
// ✅ React 19 ref prop类型(简洁)
import { Ref } from 'react';
interface Props {
ref?: Ref<HTMLInputElement>;
value: string;
}
function Input({ ref, ...props }: Props) {
return <input ref={ref} {...props} />;
}
// ✅ 或使用ComponentPropsWithRef
type InputProps = ComponentPropsWithRef<'input'>;
function Input(props: InputProps) {
return <input {...props} />;
}Q6: ref可以和其他props一起解构吗?
A: 可以,ref现在是普通prop,可以像其他props一样解构使用。
解构示例:
jsx
// ✅ 直接解构
function Input({ ref, placeholder, value, onChange }) {
return (
<input
ref={ref}
placeholder={placeholder}
value={value}
onChange={onChange}
/>
);
}
// ✅ 使用剩余参数
function Input({ ref, label, ...inputProps }) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...inputProps} />
</div>
);
}
// ✅ 解构重命名
function Input({ ref: externalRef, ...props }) {
const internalRef = useRef(null);
const mergedRef = externalRef || internalRef;
return <input ref={mergedRef} {...props} />;
}Q7: 如何在高阶组件(HOC)中传递ref?
A: 直接传递即可,无需特殊处理。
HOC实现:
jsx
// ✅ React 19 - 简单直接
function withLogging(Component) {
return function LoggedComponent({ ref, ...props }) {
useEffect(() => {
console.log('Component rendered with props:', props);
});
return <Component ref={ref} {...props} />;
};
}
// ❌ React 18 - 需要特殊处理
function withLogging(Component) {
const LoggedComponent = forwardRef((props, ref) => {
useEffect(() => {
console.log('Component rendered with props:', props);
});
return <Component ref={ref} {...props} />;
});
return LoggedComponent;
}Q8: 组件库应该如何迁移到ref prop?
A: 采用渐进式迁移策略,保持向后兼容性。
迁移步骤:
步骤1:评估影响
bash
# 查找所有使用forwardRef的组件
grep -r "forwardRef" src/components步骤2:创建兼容版本
jsx
// 保留forwardRef版本(默认导出)
export const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// 添加ref prop版本(命名导出)
export function InputV2({ ref, ...props }) {
return <input ref={ref} {...props} />;
}步骤3:更新文档
markdown
## 迁移指南
### React 19用户
使用新的ref prop版本:
\`\`\`jsx
import { InputV2 as Input } from 'your-library';
\`\`\`
### React 18用户
继续使用forwardRef版本:
\`\`\`jsx
import { Input } from 'your-library';
\`\`\`步骤4:逐步过渡
- 在下一个主版本完全移除forwardRef
- 提供代码修改工具(codemod)
- 发布迁移公告
Q9: ref prop和useImperativeHandle如何配合?
A: 完全兼容,使用方式与forwardRef时相同。
配合示例:
jsx
// ✅ React 19 - 直接使用
function Input({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
select: () => inputRef.current?.select(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}));
return <input ref={inputRef} {...props} />;
}
// 使用
function Form() {
const inputRef = useRef(null);
return (
<div>
<Input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
聚焦
</button>
<button onClick={() => inputRef.current?.clear()}>
清空
</button>
</div>
);
}Q10: 如何测试使用ref prop的组件?
A: 测试方式与之前相同,ref现在更容易测试。
测试示例:
jsx
import { render } from '@testing-library/react';
import { useRef } from 'react';
// 组件
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 测试
describe('Input Component', () => {
it('should expose ref correctly', () => {
const TestWrapper = () => {
const inputRef = useRef(null);
useEffect(() => {
// 测试ref是否正确设置
expect(inputRef.current).toBeInstanceOf(HTMLInputElement);
expect(inputRef.current?.tagName).toBe('INPUT');
}, []);
return <Input ref={inputRef} placeholder="test" />;
};
render(<TestWrapper />);
});
it('should support focus method', () => {
const TestWrapper = () => {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
expect(document.activeElement).toBe(inputRef.current);
}, []);
return <Input ref={inputRef} />;
};
render(<TestWrapper />);
});
});总结
ref作为prop的核心优势
1. 语法简化
React 18 (forwardRef):
jsx
import { forwardRef } from 'react';
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});React 19 (ref prop):
jsx
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}对比优势:
- ✅ 减少代码行数(约30%)
- ✅ 无需导入forwardRef
- ✅ 更直观的API
- ✅ 降低学习曲线
2. TypeScript类型简化
类型定义对比:
tsx
// ❌ forwardRef类型(复杂)
const Input = forwardRef<HTMLInputElement, Props>(
(props, ref: ForwardedRef<HTMLInputElement>) => { ... }
);
// ✅ ref prop类型(简洁)
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
...
}类型优势:
- ✅ 类型定义减少40%代码
- ✅ 类型参数顺序更清晰
- ✅ 更好的IDE提示
- ✅ 减少类型错误
3. 组件组合优化
嵌套组件对比:
jsx
// ❌ forwardRef嵌套(繁琐)
const Button = forwardRef((props, ref) => <button ref={ref} {...props} />);
const IconButton = forwardRef((props, ref) => <Button ref={ref} {...props}><Icon /></Button>);
const PrimaryButton = forwardRef((props, ref) => <IconButton ref={ref} {...props} />);
// ✅ ref prop(简洁)
function Button({ ref, ...props }) { return <button ref={ref} {...props} />; }
function IconButton({ ref, ...props }) { return <Button ref={ref} {...props}><Icon /></Button>; }
function PrimaryButton({ ref, ...props }) { return <IconButton ref={ref} {...props} />; }组合优势:
- ✅ 减少包装层级
- ✅ 提升可读性
- ✅ 降低复杂度
- ✅ 易于维护
完整迁移步骤指南
步骤1:评估现有代码
bash
# 统计forwardRef使用情况
grep -r "forwardRef" src/ --include="*.jsx" --include="*.tsx" | wc -l
# 查找具体位置
grep -rn "forwardRef" src/components/步骤2:创建迁移计划
优先级排序:
- **高优先级:**新开发的组件
- **中优先级:**经常修改的组件
- **低优先级:**稳定的组件
- **暂缓:**第三方库依赖的组件
步骤3:逐个组件迁移
迁移模板:
jsx
// 1. 移除forwardRef导入
- import { forwardRef } from 'react';
+ // 不需要导入
// 2. 修改组件定义
- const Input = forwardRef((props, ref) => {
+ function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
- });
+ }
// 3. 更新TypeScript类型
- const Input = forwardRef<HTMLInputElement, Props>(
- (props, ref: ForwardedRef<HTMLInputElement>) => {
+ function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
- }
- );
+ }
// 4. 更新导出
- export default Input;
+ export default Input; // 保持不变步骤4:测试验证
测试清单:
□ ref正确传递给DOM元素
□ useImperativeHandle正常工作
□ TypeScript类型检查通过
□ 现有测试用例通过
□ 集成测试正常
□ 性能无明显退化步骤5:更新文档
文档更新内容:
- API文档更新
- 示例代码更新
- 迁移指南编写
- Breaking Changes说明
步骤6:发布通知
发布内容:
markdown
## v2.0.0 更新日志
### 💥 Breaking Changes
- 移除forwardRef,使用原生ref prop
- 需要React 19+
### ✨ 新特性
- 简化的ref传递
- 更好的TypeScript支持
### 📖 迁移指南
详见 [MIGRATION.md](./MIGRATION.md)最佳实践汇总
开发实践
DO(推荐):
jsx
// ✅ 直接使用ref prop
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// ✅ 使用TypeScript类型
import { Ref } from 'react';
interface Props {
ref?: Ref<HTMLInputElement>;
}
// ✅ 配合useImperativeHandle
function Input({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
}));
return <input ref={inputRef} />;
}
// ✅ 安全访问ref
if (ref && 'current' in ref) {
ref.current?.method();
}DON'T(避免):
jsx
// ❌ 命名冲突
function Input({ ref }) {
const ref = useRef(null); // 错误!
}
// ❌ 过度使用ref
function Component({ ref }) {
useImperativeHandle(ref, () => ({
getData: () => data // 应该用props
}));
}
// ❌ 忘记TypeScript类型
function Input({ ref, ...props }) { // 缺少类型定义
return <input ref={ref} {...props} />;
}组件库实践
向后兼容策略:
jsx
// 同时导出两个版本
export const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
export function InputV2({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 或使用条件导出
export const Input = IS_REACT_19
? function Input({ ref, ...props }) { ... }
: forwardRef((props, ref) => { ... });性能优化
ref回调优化:
jsx
// ✅ 使用useCallback稳定ref回调
const handleRef = useCallback((element) => {
if (element) {
// 初始化逻辑
}
}, []);
return <input ref={handleRef} />;合并多个ref:
jsx
// ✅ 高效合并ref
function useMergedRef(...refs) {
return useCallback(
(element) => {
refs.forEach(ref => {
if (typeof ref === 'function') {
ref(element);
} else if (ref) {
ref.current = element;
}
});
},
refs
);
}技术对比总结
| 特性 | forwardRef (React 18) | ref prop (React 19) |
|---|---|---|
| 代码量 | 更多 | 更少 (减少30%) |
| 可读性 | 一般 | 优秀 |
| TypeScript支持 | 复杂 | 简洁 |
| 学习曲线 | 较陡 | 平缓 |
| 嵌套处理 | 繁琐 | 简单 |
| 性能 | 正常 | 略优(~8%) |
| 向后兼容 | N/A | 完全兼容 |
| IDE支持 | 良好 | 更好 |
生态系统影响
主流库的支持:
- **React Router:**已支持ref prop
- **Material-UI:**计划在v6支持
- **Ant Design:**正在迁移
- **Chakra UI:**已完成迁移
- **React Hook Form:**已完全支持
未来展望
React团队的ref prop改进为组件开发带来了显著提升:
- **降低学习门槛:**新手更容易理解和使用
- **提升开发效率:**减少样板代码
- **改善开发体验:**更好的类型支持和IDE提示
- **促进生态发展:**推动库和框架的现代化
行动建议
立即行动:
- ✅ 在新项目中使用ref prop
- ✅ 学习新的ref传递方式
- ✅ 更新团队编码规范
短期计划(1-3个月):
- ✅ 迁移活跃开发的组件
- ✅ 更新组件库文档
- ✅ 培训团队成员
长期规划(3-6个月):
- ✅ 完成所有组件迁移
- ✅ 移除forwardRef依赖
- ✅ 发布新主版本
ref作为prop让React组件更加简洁优雅,是React 19最实用的改进之一!