Appearance
forwardRef与useImperativeHandle
学习目标
通过本章学习,你将全面掌握:
- forwardRef的核心概念和应用
- ref转发的工作原理
- useImperativeHandle的作用机制
- 自定义ref暴露方法的最佳实践
- forwardRef在组件库中的应用
- ref回调函数的高级用法
- TypeScript中的ref类型定义
- React 19中ref处理的简化
- forwardRef与useImperativeHandle的组合模式
第一部分:forwardRef核心概念
1.1 为什么需要forwardRef
jsx
import { useRef, forwardRef } from 'react';
// 问题演示:函数组件不能直接接收ref
function MyInput(props) {
return <input {...props} />;
}
function ProblemDemo() {
const inputRef = useRef(null);
return (
<div>
{/* 警告: Function components cannot be given refs */}
<MyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
</div>
);
}
// 问题原因:
// 1. 函数组件没有实例,不像类组件有this
// 2. ref是React的特殊prop,不会传递给组件
// 3. 直接传递ref会被React忽略并警告
// 解决方案:使用forwardRef
const MyInputForwarded = forwardRef((props, ref) => {
// ref作为第二个参数传入
return <input ref={ref} {...props} />;
});
function SolutionDemo() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
const selectAll = () => {
inputRef.current?.select();
};
const clearInput = () => {
if (inputRef.current) {
inputRef.current.value = '';
}
};
return (
<div>
<MyInputForwarded
ref={inputRef}
placeholder="这是一个转发了ref的输入框"
/>
<div>
<button onClick={focusInput}>聚焦</button>
<button onClick={selectAll}>全选</button>
<button onClick={clearInput}>清空</button>
</div>
</div>
);
}1.2 forwardRef的基本语法
jsx
// 基本语法
const Component = forwardRef((props, ref) => {
// props: 组件的所有props
// ref: 从父组件传递的ref
return <div ref={ref}>内容</div>;
});
// 带显示名称
const NamedComponent = forwardRef(function MyComponent(props, ref) {
return <div ref={ref}>内容</div>;
});
// 设置displayName(调试用)
const DisplayNameComponent = forwardRef((props, ref) => {
return <div ref={ref}>内容</div>;
});
DisplayNameComponent.displayName = 'DisplayNameComponent';
// 完整示例:自定义输入框
const CustomInput = forwardRef(function CustomInput(
{ label, error, ...props },
ref
) {
return (
<div className="custom-input">
{label && <label>{label}</label>}
<input
ref={ref}
className={error ? 'error' : ''}
{...props}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
});
// 使用自定义输入框
function FormWithCustomInput() {
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);
};
const focusFirstEmpty = () => {
if (!nameRef.current.value) {
nameRef.current.focus();
} else if (!emailRef.current.value) {
emailRef.current.focus();
}
};
return (
<form onSubmit={handleSubmit}>
<CustomInput
ref={nameRef}
label="姓名"
placeholder="请输入姓名"
/>
<CustomInput
ref={emailRef}
label="邮箱"
type="email"
placeholder="请输入邮箱"
/>
<button type="submit">提交</button>
<button type="button" onClick={focusFirstEmpty}>
聚焦第一个空字段
</button>
</form>
);
}1.3 ref转发的工作原理
jsx
// ref转发链条
function RefForwardingChain() {
const finalInputRef = useRef(null);
return (
<div>
{/*
ref转发链:
Parent (finalInputRef)
↓
Container (转发ref)
↓
Wrapper (转发ref)
↓
Input (最终接收ref)
*/}
<Container ref={finalInputRef} />
<button onClick={() => finalInputRef.current?.focus()}>
聚焦最内层输入框
</button>
</div>
);
}
const Container = forwardRef((props, ref) => {
console.log('Container收到ref');
return <Wrapper ref={ref} {...props} />;
});
const Wrapper = forwardRef((props, ref) => {
console.log('Wrapper收到ref');
return (
<div className="wrapper">
<Input ref={ref} {...props} />
</div>
);
});
const Input = forwardRef((props, ref) => {
console.log('Input收到ref');
return <input ref={ref} {...props} />;
});
// 多ref管理
const MultiRefComponent = forwardRef((props, externalRef) => {
// 内部也需要使用ref
const internalRef = useRef(null);
useEffect(() => {
// 内部逻辑使用internalRef
console.log('内部ref:', internalRef.current);
}, []);
// 同时支持外部ref
useEffect(() => {
if (typeof externalRef === 'function') {
externalRef(internalRef.current);
} else if (externalRef) {
externalRef.current = internalRef.current;
}
}, [externalRef]);
return <input ref={internalRef} {...props} />;
});第二部分:useImperativeHandle详解
2.1 useImperativeHandle的作用
jsx
import { forwardRef, useRef, useImperativeHandle } from 'react';
// 不使用useImperativeHandle:暴露整个DOM元素
const InputWithoutImperative = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
function ParentWithoutImperative() {
const inputRef = useRef(null);
const handleClick = () => {
// 可以访问input的所有方法和属性
inputRef.current.focus();
inputRef.current.select();
inputRef.current.value = 'Modified directly'; // 直接修改DOM
inputRef.current.style.color = 'red'; // 直接修改样式
// 问题:父组件可以完全控制DOM,破坏了封装性
};
return (
<div>
<InputWithoutImperative ref={inputRef} />
<button onClick={handleClick}>操作</button>
</div>
);
}
// 使用useImperativeHandle:只暴露特定方法
const InputWithImperative = forwardRef((props, ref) => {
const internalRef = useRef(null);
// 只暴露focus和clear方法
useImperativeHandle(ref, () => ({
focus: () => {
internalRef.current?.focus();
},
clear: () => {
if (internalRef.current) {
internalRef.current.value = '';
}
}
}), []);
return <input ref={internalRef} {...props} />;
});
function ParentWithImperative() {
const inputRef = useRef(null);
const handleClick = () => {
// 只能使用暴露的方法
inputRef.current.focus(); // ✅ 可以
inputRef.current.clear(); // ✅ 可以
// inputRef.current.select(); // ❌ 不可以,未暴露
// inputRef.current.value = ''; // ❌ 不可以,没有直接访问DOM
};
return (
<div>
<InputWithImperative ref={inputRef} />
<button onClick={handleClick}>操作</button>
</div>
);
}2.2 useImperativeHandle的语法
jsx
// 基本语法
useImperativeHandle(ref, createHandle, dependencies);
// 参数说明:
// ref: forwardRef传入的ref
// createHandle: 返回要暴露的方法对象的函数
// dependencies: 依赖数组(可选,默认每次渲染都重新创建)
// 完整示例
const AdvancedInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
useImperativeHandle(
ref,
() => ({
// 暴露的方法
focus: () => {
inputRef.current?.focus();
},
blur: () => {
inputRef.current?.blur();
},
getValue: () => {
return value;
},
setValue: (newValue) => {
setValue(newValue);
},
clear: () => {
setValue('');
},
selectAll: () => {
inputRef.current?.select();
},
insertText: (text) => {
setValue(prev => prev + text);
}
}),
[value] // 依赖value,当value变化时重新创建handle
);
return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
{...props}
/>
);
});
// 使用
function AdvancedInputUsage() {
const inputRef = useRef(null);
return (
<div>
<AdvancedInput ref={inputRef} placeholder="高级输入框" />
<div>
<button onClick={() => inputRef.current?.focus()}>
聚焦
</button>
<button onClick={() => inputRef.current?.blur()}>
失焦
</button>
<button onClick={() => inputRef.current?.selectAll()}>
全选
</button>
<button onClick={() => inputRef.current?.clear()}>
清空
</button>
<button onClick={() => inputRef.current?.insertText('Hello')}>
插入文本
</button>
<button onClick={() => {
const value = inputRef.current?.getValue();
alert(`当前值: ${value}`);
}}>
获取值
</button>
</div>
</div>
);
}2.3 暴露的方法设计
jsx
// 设计1:只暴露操作方法,不暴露状态
const ReadOnlyInput = forwardRef((props, ref) => {
const [value, setValue] = useState('');
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => setValue(''),
// 不暴露getValue,父组件无法直接读取值
// 父组件需要通过onChange回调获取值
}), []);
return (
<input
ref={inputRef}
value={value}
onChange={e => {
setValue(e.target.value);
props.onChange?.(e);
}}
/>
);
});
// 设计2:暴露读写方法
const ReadWriteInput = forwardRef((props, ref) => {
const [value, setValue] = useState('');
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 读方法
getValue: () => value,
isEmpty: () => value.trim() === '',
getLength: () => value.length,
// 写方法
setValue: (newValue) => setValue(newValue),
clear: () => setValue(''),
append: (text) => setValue(prev => prev + text),
// DOM操作
focus: () => inputRef.current?.focus(),
select: () => inputRef.current?.select()
}), [value]);
return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
{...props}
/>
);
});
// 设计3:暴露验证方法
const ValidatedInput = forwardRef((props, ref) => {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 验证方法
validate: () => {
if (!value) {
setError('不能为空');
return false;
}
if (value.length < 3) {
setError('至少3个字符');
return false;
}
setError('');
return true;
},
// 获取状态
getValue: () => value,
hasError: () => !!error,
getError: () => error,
// 操作方法
focus: () => inputRef.current?.focus(),
clear: () => {
setValue('');
setError('');
}
}), [value, error]);
return (
<div className="validated-input">
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
className={error ? 'error' : ''}
{...props}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
});
// 使用验证输入框
function FormWithValidation() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// 验证所有字段
const nameValid = nameRef.current?.validate();
const emailValid = emailRef.current?.validate();
const passwordValid = passwordRef.current?.validate();
if (nameValid && emailValid && passwordValid) {
// 获取所有值
const formData = {
name: nameRef.current.getValue(),
email: emailRef.current.getValue(),
password: passwordRef.current.getValue()
};
console.log('提交表单:', formData);
} else {
// 聚焦第一个错误字段
if (!nameValid) {
nameRef.current?.focus();
} else if (!emailValid) {
emailRef.current?.focus();
} else if (!passwordValid) {
passwordRef.current?.focus();
}
}
};
return (
<form onSubmit={handleSubmit}>
<ValidatedInput
ref={nameRef}
placeholder="姓名"
/>
<ValidatedInput
ref={emailRef}
type="email"
placeholder="邮箱"
/>
<ValidatedInput
ref={passwordRef}
type="password"
placeholder="密码"
/>
<button type="submit">提交</button>
</form>
);
}第三部分:高级应用模式
3.1 多ref组合
jsx
// 组件同时需要内部ref和外部ref
const DualRefComponent = forwardRef((props, externalRef) => {
const internalRef = useRef(null);
// 内部使用internalRef
useEffect(() => {
console.log('内部ref:', internalRef.current);
// 内部逻辑...
}, []);
// 暴露给外部
useImperativeHandle(externalRef, () => ({
focus: () => internalRef.current?.focus(),
getElement: () => internalRef.current
}), []);
return <input ref={internalRef} {...props} />;
});
// 合并多个ref
function useCombinedRefs(...refs) {
return useCallback((element) => {
refs.forEach(ref => {
if (!ref) return;
if (typeof ref === 'function') {
ref(element);
} else {
ref.current = element;
}
});
}, refs);
}
// 使用合并的ref
const CombinedRefComponent = forwardRef((props, externalRef) => {
const internalRef = useRef(null);
const combinedRef = useCombinedRefs(internalRef, externalRef);
useEffect(() => {
// 两个ref都可以使用
console.log('内部ref:', internalRef.current);
}, []);
return <input ref={combinedRef} {...props} />;
});3.2 条件ref转发
jsx
// 根据条件决定ref转发目标
const ConditionalRefForward = forwardRef(({ type, ...props }, ref) => {
const inputRef = useRef(null);
const textareaRef = useRef(null);
useImperativeHandle(ref, () => {
const targetRef = type === 'textarea' ? textareaRef : inputRef;
return {
focus: () => targetRef.current?.focus(),
getValue: () => targetRef.current?.value || '',
setValue: (value) => {
if (targetRef.current) {
targetRef.current.value = value;
}
}
};
}, [type]);
if (type === 'textarea') {
return <textarea ref={textareaRef} {...props} />;
}
return <input ref={inputRef} {...props} />;
});
// 使用
function ConditionalRefUsage() {
const [inputType, setInputType] = useState('input');
const ref = useRef(null);
return (
<div>
<select value={inputType} onChange={e => setInputType(e.target.value)}>
<option value="input">Input</option>
<option value="textarea">Textarea</option>
</select>
<ConditionalRefForward ref={ref} type={inputType} />
<button onClick={() => ref.current?.focus()}>
聚焦
</button>
<button onClick={() => ref.current?.setValue('Hello')}>
设置值
</button>
</div>
);
}3.3 ref回调函数
jsx
// 使用回调函数形式的ref
function CallbackRefExample() {
const [height, setHeight] = useState(0);
// ref回调函数
const measureRef = useCallback((element) => {
if (element) {
console.log('元素挂载:', element);
setHeight(element.getBoundingClientRect().height);
} else {
console.log('元素卸载');
setHeight(0);
}
}, []);
return (
<div>
<div ref={measureRef} style={{ padding: '20px', background: '#f0f0f0' }}>
<p>这个div的高度会被测量</p>
<p>可以添加更多内容改变高度</p>
</div>
<p>测量的高度: {height}px</p>
</div>
);
}
// forwardRef with callback ref
const MeasurableComponent = forwardRef((props, ref) => {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const internalRef = useRef(null);
useImperativeHandle(ref, () => ({
measure: () => {
if (internalRef.current) {
const rect = internalRef.current.getBoundingClientRect();
const dims = {
width: rect.width,
height: rect.height
};
setDimensions(dims);
return dims;
}
return null;
},
getDimensions: () => dimensions,
getElement: () => internalRef.current
}), [dimensions]);
return (
<div ref={internalRef} {...props}>
<p>宽度: {dimensions.width}px</p>
<p>高度: {dimensions.height}px</p>
{props.children}
</div>
);
});第四部分:实战案例
4.1 Modal组件
jsx
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
const Modal = forwardRef(({ title, children, onClose }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const contentRef = useRef(null);
useImperativeHandle(ref, () => ({
open: () => {
setIsOpen(true);
},
close: () => {
setIsOpen(false);
onClose?.();
},
toggle: () => {
setIsOpen(prev => !prev);
},
isOpen: () => isOpen,
focusContent: () => {
contentRef.current?.focus();
}
}), [isOpen, onClose]);
useEffect(() => {
if (isOpen) {
// 打开时聚焦内容
contentRef.current?.focus();
// 禁止body滚动
document.body.style.overflow = 'hidden';
// ESC键关闭
const handleEscape = (e) => {
if (e.key === 'Escape') {
setIsOpen(false);
onClose?.();
}
};
window.addEventListener('keydown', handleEscape);
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', handleEscape);
};
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={() => {
setIsOpen(false);
onClose?.();
}}>
<div
ref={contentRef}
className="modal-content"
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<div className="modal-header">
<h2>{title}</h2>
<button
className="close-button"
onClick={() => {
setIsOpen(false);
onClose?.();
}}
>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
);
});
Modal.displayName = 'Modal';
// 使用Modal
function ModalUsage() {
const modalRef = useRef(null);
const confirmModalRef = useRef(null);
const handleAction = () => {
// 使用暴露的方法
modalRef.current?.close();
confirmModalRef.current?.open();
};
return (
<div>
<button onClick={() => modalRef.current?.open()}>
打开模态框
</button>
<Modal ref={modalRef} title="信息">
<p>这是模态框内容</p>
<button onClick={handleAction}>
执行操作
</button>
</Modal>
<Modal ref={confirmModalRef} title="确认" onClose={() => console.log('已关闭')}>
<p>确认要执行此操作吗?</p>
<div>
<button onClick={() => {
console.log('已确认');
confirmModalRef.current?.close();
}}>
确认
</button>
<button onClick={() => confirmModalRef.current?.close()}>
取消
</button>
</div>
</Modal>
</div>
);
}4.2 Video播放器组件
jsx
const VideoPlayer = forwardRef(({ src, poster, onTimeUpdate, onEnded }, ref) => {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [muted, setMuted] = useState(false);
useImperativeHandle(ref, () => ({
// 播放控制
play: async () => {
try {
await videoRef.current?.play();
setIsPlaying(true);
} catch (err) {
console.error('播放失败:', err);
}
},
pause: () => {
videoRef.current?.pause();
setIsPlaying(false);
},
stop: () => {
videoRef.current?.pause();
if (videoRef.current) {
videoRef.current.currentTime = 0;
}
setIsPlaying(false);
setCurrentTime(0);
},
togglePlay: async () => {
if (isPlaying) {
videoRef.current?.pause();
setIsPlaying(false);
} else {
try {
await videoRef.current?.play();
setIsPlaying(true);
} catch (err) {
console.error('播放失败:', err);
}
}
},
// 时间控制
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = Math.max(0, Math.min(time, duration));
setCurrentTime(videoRef.current.currentTime);
}
},
skipForward: (seconds = 10) => {
if (videoRef.current) {
videoRef.current.currentTime = Math.min(
videoRef.current.currentTime + seconds,
duration
);
}
},
skipBackward: (seconds = 10) => {
if (videoRef.current) {
videoRef.current.currentTime = Math.max(
videoRef.current.currentTime - seconds,
0
);
}
},
// 音量控制
setVolume: (vol) => {
if (videoRef.current) {
videoRef.current.volume = Math.max(0, Math.min(vol, 1));
setVolume(videoRef.current.volume);
}
},
mute: () => {
if (videoRef.current) {
videoRef.current.muted = true;
setMuted(true);
}
},
unmute: () => {
if (videoRef.current) {
videoRef.current.muted = false;
setMuted(false);
}
},
toggleMute: () => {
if (videoRef.current) {
videoRef.current.muted = !videoRef.current.muted;
setMuted(videoRef.current.muted);
}
},
// 获取状态
isPlaying: () => isPlaying,
getCurrentTime: () => currentTime,
getDuration: () => duration,
getVolume: () => volume,
isMuted: () => muted,
// 获取原始元素
getVideoElement: () => videoRef.current
}), [isPlaying, currentTime, duration, volume, muted]);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const handleTimeUpdate = () => {
setCurrentTime(video.currentTime);
onTimeUpdate?.(video.currentTime);
};
const handleLoadedMetadata = () => {
setDuration(video.duration);
};
const handleEnded = () => {
setIsPlaying(false);
onEnded?.();
};
video.addEventListener('timeupdate', handleTimeUpdate);
video.addEventListener('loadedmetadata', handleLoadedMetadata);
video.addEventListener('ended', handleEnded);
return () => {
video.removeEventListener('timeupdate', handleTimeUpdate);
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
video.removeEventListener('ended', handleEnded);
};
}, [onTimeUpdate, onEnded]);
return (
<div className="video-player">
<video
ref={videoRef}
src={src}
poster={poster}
/>
</div>
);
});
VideoPlayer.displayName = 'VideoPlayer';
// 使用VideoPlayer
function VideoPlayerController() {
const playerRef = useRef(null);
const [currentTime, setCurrentTime] = useState(0);
return (
<div>
<VideoPlayer
ref={playerRef}
src="/video.mp4"
onTimeUpdate={setCurrentTime}
/>
<div className="controls">
<button onClick={() => playerRef.current?.togglePlay()}>
播放/暂停
</button>
<button onClick={() => playerRef.current?.stop()}>
停止
</button>
<button onClick={() => playerRef.current?.skipBackward(10)}>
后退10秒
</button>
<button onClick={() => playerRef.current?.skipForward(10)}>
前进10秒
</button>
<button onClick={() => playerRef.current?.toggleMute()}>
静音
</button>
</div>
<div className="info">
<p>当前时间: {currentTime.toFixed(2)}秒</p>
<p>总时长: {playerRef.current?.getDuration().toFixed(2)}秒</p>
</div>
</div>
);
}4.3 Form组件
jsx
const Form = forwardRef(({ onSubmit, children }, ref) => {
const formRef = useRef(null);
const fieldsRef = useRef(new Map());
useImperativeHandle(ref, () => ({
// 注册字段
registerField: (name, fieldRef) => {
fieldsRef.current.set(name, fieldRef);
},
// 注销字段
unregisterField: (name) => {
fieldsRef.current.delete(name);
},
// 验证所有字段
validateAll: () => {
let isValid = true;
const errors = {};
fieldsRef.current.forEach((fieldRef, name) => {
if (fieldRef.current?.validate) {
const valid = fieldRef.current.validate();
if (!valid) {
isValid = false;
errors[name] = fieldRef.current.getError?.() || '验证失败';
}
}
});
return { isValid, errors };
},
// 获取所有值
getValues: () => {
const values = {};
fieldsRef.current.forEach((fieldRef, name) => {
if (fieldRef.current?.getValue) {
values[name] = fieldRef.current.getValue();
}
});
return values;
},
// 设置所有值
setValues: (values) => {
Object.entries(values).forEach(([name, value]) => {
const fieldRef = fieldsRef.current.get(name);
if (fieldRef?.current?.setValue) {
fieldRef.current.setValue(value);
}
});
},
// 重置所有字段
reset: () => {
fieldsRef.current.forEach((fieldRef) => {
fieldRef.current?.clear?.();
});
},
// 聚焦第一个错误字段
focusFirstError: () => {
for (const [name, fieldRef] of fieldsRef.current) {
if (fieldRef.current?.hasError?.()) {
fieldRef.current?.focus?.();
break;
}
}
},
// 提交表单
submit: () => {
formRef.current?.requestSubmit();
}
}), []);
const handleSubmit = (e) => {
e.preventDefault();
const { isValid, errors } = ref.current?.validateAll() || { isValid: false, errors: {} };
if (isValid) {
const values = ref.current?.getValues() || {};
onSubmit?.(values);
} else {
console.error('表单验证失败:', errors);
ref.current?.focusFirstError();
}
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
{children}
</form>
);
});
// Field组件
const Field = forwardRef(({ name, label, required, minLength }, ref) => {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const inputRef = useRef(null);
const formRef = useRef(null);
useImperativeHandle(ref, () => ({
validate: () => {
if (required && !value) {
setError('此字段必填');
return false;
}
if (minLength && value.length < minLength) {
setError(`至少${minLength}个字符`);
return false;
}
setError('');
return true;
},
getValue: () => value,
setValue: (newValue) => setValue(newValue),
clear: () => {
setValue('');
setError('');
},
hasError: () => !!error,
getError: () => error,
focus: () => inputRef.current?.focus()
}), [value, error, required, minLength]);
// 注册到表单
useEffect(() => {
// 向上查找Form组件并注册
// 这里简化处理,实际应该使用Context
return () => {
// 组件卸载时注销
};
}, [name]);
return (
<div className="field">
<label>
{label}
{required && <span className="required">*</span>}
</label>
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
className={error ? 'error' : ''}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
});
// 使用Form和Field
function FormUsage() {
const formRef = useRef(null);
const handleSubmit = (values) => {
console.log('表单提交:', values);
alert(`提交成功!\n${JSON.stringify(values, null, 2)}`);
};
const handleReset = () => {
formRef.current?.reset();
};
const handleFill = () => {
formRef.current?.setValues({
name: 'Alice',
email: 'alice@example.com',
password: '123456'
});
};
return (
<div>
<Form ref={formRef} onSubmit={handleSubmit}>
<Field
name="name"
label="姓名"
required
minLength={3}
/>
<Field
name="email"
label="邮箱"
type="email"
required
/>
<Field
name="password"
label="密码"
type="password"
required
minLength={6}
/>
<div className="form-actions">
<button type="submit">提交</button>
<button type="button" onClick={handleReset}>
重置
</button>
<button type="button" onClick={handleFill}>
填充示例数据
</button>
</div>
</Form>
</div>
);
}4.4 Carousel组件
jsx
const Carousel = forwardRef(({ items, autoPlay = false, interval = 3000 }, ref) => {
const [currentIndex, setCurrentIndex] = useState(0);
const containerRef = useRef(null);
const intervalRef = useRef(null);
useImperativeHandle(ref, () => ({
next: () => {
setCurrentIndex(prev => (prev + 1) % items.length);
},
prev: () => {
setCurrentIndex(prev => (prev - 1 + items.length) % items.length);
},
goTo: (index) => {
if (index >= 0 && index < items.length) {
setCurrentIndex(index);
}
},
getCurrentIndex: () => currentIndex,
getItemCount: () => items.length,
startAutoPlay: () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(() => {
setCurrentIndex(prev => (prev + 1) % items.length);
}, interval);
},
stopAutoPlay: () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}), [currentIndex, items.length, interval]);
useEffect(() => {
if (autoPlay) {
intervalRef.current = setInterval(() => {
setCurrentIndex(prev => (prev + 1) % items.length);
}, interval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}
}, [autoPlay, interval, items.length]);
return (
<div className="carousel" ref={containerRef}>
<div className="carousel-items">
{items.map((item, index) => (
<div
key={index}
className={`carousel-item ${index === currentIndex ? 'active' : ''}`}
style={{
transform: `translateX(-${currentIndex * 100}%)`
}}
>
{item}
</div>
))}
</div>
<div className="carousel-indicators">
{items.map((_, index) => (
<button
key={index}
className={index === currentIndex ? 'active' : ''}
onClick={() => setCurrentIndex(index)}
/>
))}
</div>
</div>
);
});
Carousel.displayName = 'Carousel';
// 使用Carousel
function CarouselUsage() {
const carouselRef = useRef(null);
const items = [
<div className="slide">幻灯片 1</div>,
<div className="slide">幻灯片 2</div>,
<div className="slide">幻灯片 3</div>
];
return (
<div>
<Carousel ref={carouselRef} items={items} autoPlay interval={2000} />
<div className="controls">
<button onClick={() => carouselRef.current?.prev()}>
上一张
</button>
<button onClick={() => carouselRef.current?.next()}>
下一张
</button>
<button onClick={() => carouselRef.current?.goTo(0)}>
回到第一张
</button>
<button onClick={() => carouselRef.current?.stopAutoPlay()}>
停止自动播放
</button>
<button onClick={() => carouselRef.current?.startAutoPlay()}>
开始自动播放
</button>
</div>
<p>
当前: {(carouselRef.current?.getCurrentIndex() ?? 0) + 1} / {items.length}
</p>
</div>
);
}第五部分:TypeScript支持
5.1 forwardRef的类型定义
typescript
import { forwardRef, useRef, useImperativeHandle, Ref, ForwardedRef } from 'react';
// 基本类型
interface InputProps {
label?: string;
placeholder?: string;
}
const TypedInput = forwardRef<HTMLInputElement, InputProps>(
({ label, placeholder, ...props }, ref) => {
return (
<div>
{label && <label>{label}</label>}
<input ref={ref} placeholder={placeholder} {...props} />
</div>
);
}
);
// 使用
function TypedInputUsage() {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
inputRef.current?.focus(); // 完全类型安全
};
return (
<div>
<TypedInput ref={inputRef} label="用户名" />
<button onClick={handleClick}>聚焦</button>
</div>
);
}5.2 useImperativeHandle的类型
typescript
// 定义暴露的方法接口
interface InputHandle {
focus: () => void;
blur: () => void;
getValue: () => string;
setValue: (value: string) => void;
clear: () => void;
}
interface InputProps {
label?: string;
defaultValue?: string;
}
const TypedImperativeInput = forwardRef<InputHandle, InputProps>(
({ label, defaultValue = '' }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(defaultValue);
useImperativeHandle(
ref,
(): InputHandle => ({
focus: () => {
inputRef.current?.focus();
},
blur: () => {
inputRef.current?.blur();
},
getValue: () => {
return value;
},
setValue: (newValue: string) => {
setValue(newValue);
},
clear: () => {
setValue('');
}
}),
[value]
);
return (
<div>
{label && <label>{label}</label>}
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
/>
</div>
);
}
);
// 使用
function TypedImperativeUsage() {
const inputRef = useRef<InputHandle>(null);
const handleClick = () => {
inputRef.current?.focus(); // ✅ 类型安全
const value = inputRef.current?.getValue(); // ✅ 类型推断
console.log('Value:', value);
};
return (
<div>
<TypedImperativeInput ref={inputRef} label="姓名" />
<button onClick={handleClick}>操作</button>
</div>
);
}5.3 复杂类型定义
typescript
// 泛型forwardRef组件
interface ListHandle<T> {
getItems: () => T[];
addItem: (item: T) => void;
removeItem: (id: string | number) => void;
clear: () => void;
getItemCount: () => number;
}
interface ListProps<T> {
initialItems?: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
const List = forwardRef(function List<T>(
{ initialItems = [], renderItem, keyExtractor }: ListProps<T>,
ref: ForwardedRef<ListHandle<T>>
) {
const [items, setItems] = useState<T[]>(initialItems);
useImperativeHandle(
ref,
(): ListHandle<T> => ({
getItems: () => items,
addItem: (item: T) => {
setItems(prev => [...prev, item]);
},
removeItem: (id: string | number) => {
setItems(prev => prev.filter(item => keyExtractor(item) !== id));
},
clear: () => {
setItems([]);
},
getItemCount: () => items.length
}),
[items, keyExtractor]
);
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}) as <T>(
props: ListProps<T> & { ref?: ForwardedRef<ListHandle<T>> }
) => JSX.Element;
// 使用泛型List
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoListUsage() {
const listRef = useRef<ListHandle<Todo>>(null);
const addTodo = () => {
listRef.current?.addItem({
id: Date.now(),
text: '新任务',
completed: false
});
};
return (
<div>
<List<Todo>
ref={listRef}
renderItem={(todo) => (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => {}}
/>
<span>{todo.text}</span>
</div>
)}
keyExtractor={todo => todo.id}
/>
<button onClick={addTodo}>添加任务</button>
<button onClick={() => listRef.current?.clear()}>清空</button>
</div>
);
}第六部分:性能优化
6.1 避免不必要的重新创建
jsx
// ❌ 问题:每次渲染都创建新的handle对象
const BadImperative = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
})); // 没有依赖数组,每次渲染都重新创建
return <input ref={inputRef} />;
});
// ✅ 解决:使用空依赖数组
const GoodImperative = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
}), []); // 空依赖,只创建一次
return <input ref={inputRef} />;
});
// 有依赖的情况
const DependentImperative = forwardRef((props, ref) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
getValue: () => value, // 依赖value
setValue: (newValue) => setValue(newValue) // 修改value
}), [value]); // 只在value变化时重新创建
return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
/>
);
});6.2 memo与forwardRef结合
jsx
import { memo, forwardRef } from 'react';
// 组合使用memo和forwardRef
const MemoizedInput = memo(
forwardRef((props, ref) => {
console.log('MemoizedInput渲染');
return <input ref={ref} {...props} />;
})
);
MemoizedInput.displayName = 'MemoizedInput';
// 使用
function MemoizedInputUsage() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const inputRef = useRef(null);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加Count</button>
{/* MemoizedInput不会因为count变化而重新渲染 */}
<MemoizedInput
ref={inputRef}
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
</div>
);
}
// 自定义比较函数
const MemoizedWithCompare = memo(
forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
}),
(prevProps, nextProps) => {
// 返回true表示不重新渲染
return prevProps.value === nextProps.value;
}
);第七部分:React 19增强
7.1 ref作为prop(React 19)
jsx
// React 19简化:ref可以直接作为prop传递
// 旧方式(React 18及之前)
const OldWayInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// 新方式(React 19)
function NewWayInput({ ref, ...props }) {
// ref直接作为普通prop
return <input ref={ref} {...props} />;
}
// 使用
function React19RefUsage() {
const inputRef = useRef(null);
return (
<div>
{/* 两种方式都可以 */}
<OldWayInput ref={inputRef} />
<NewWayInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
聚焦
</button>
</div>
);
}
// React 19中不再需要forwardRef
function SimpleComponent({ ref, children }) {
return <div ref={ref}>{children}</div>;
}
// 但对于需要useImperativeHandle的情况,仍然有用
const CustomHandleComponent = forwardRef((props, ref) => {
const internalRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => internalRef.current?.focus(),
getElement: () => internalRef.current
}), []);
return <div ref={internalRef}>{props.children}</div>;
});7.2 ref cleanup function
jsx
// React 19: ref cleanup支持
function CleanupRefExample() {
const [show, setShow] = useState(true);
const refCallback = (element) => {
if (element) {
console.log('元素挂载:', element);
element.style.background = 'lightblue';
// React 19: 返回清理函数
return () => {
console.log('元素卸载,执行清理');
element.style.background = '';
};
}
};
return (
<div>
<button onClick={() => setShow(!show)}>切换</button>
{show && <div ref={refCallback}>带清理函数的ref</div>}
</div>
);
}
// forwardRef with cleanup
const CleanupComponent = forwardRef((props, ref) => {
const internalRef = useRef(null);
useImperativeHandle(ref, () => {
const handle = {
focus: () => internalRef.current?.focus(),
highlight: () => {
if (internalRef.current) {
internalRef.current.style.background = 'yellow';
}
}
};
// React 19: 返回清理函数
return () => {
console.log('清理imperative handle');
if (internalRef.current) {
internalRef.current.style.background = '';
}
};
}, []);
return <input ref={internalRef} {...props} />;
});第八部分:最佳实践
8.1 命名规范
jsx
// ✅ 好的命名
const Button = forwardRef(function Button(props, ref) {
return <button ref={ref} {...props} />;
});
Button.displayName = 'Button';
const Input = forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
Input.displayName = 'Input';
const CustomSelect = forwardRef(function CustomSelect(props, ref) {
return <select ref={ref} {...props} />;
});
CustomSelect.displayName = 'CustomSelect';
// ❌ 不好的命名
const Component1 = forwardRef((props, ref) => {
return <div ref={ref} />;
});
// 没有displayName,调试困难
const FC = forwardRef(function FC(props, ref) {
return <div ref={ref} />;
});
// 名称不清晰8.2 ref暴露的API设计
jsx
// 原则1:最小暴露原则
const MinimalExposureInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
useImperativeHandle(ref, () => ({
// 只暴露必要的方法
focus: () => inputRef.current?.focus(),
clear: () => setValue('')
// 不暴露:
// - setValue (通过onChange控制)
// - blur (一般不需要)
// - select (不是核心功能)
// - 直接访问DOM元素
}), []);
return (
<input
ref={inputRef}
value={value}
onChange={e => {
setValue(e.target.value);
props.onChange?.(e);
}}
/>
);
});
// 原则2:语义化方法名
const SemanticMethodsInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
useImperativeHandle(ref, () => ({
// ✅ 好的方法名
focus: () => inputRef.current?.focus(),
clear: () => setValue(''),
isEmpty: () => value.trim() === '',
validate: () => value.length >= 3,
// ❌ 不好的方法名
// doFocus: () => {}, // 多余的'do'
// clearValue: () => {}, // 'Value'是多余的
// checkIfEmpty: () => {}, // 太啰嗦
// val: () => {}, // 太简短
}), [value]);
return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} />;
});
// 原则3:一致的接口设计
// 所有输入组件都应该有相似的接口
const ConsistentInput = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
focus: () => {},
blur: () => {},
getValue: () => {},
setValue: (value) => {},
validate: () => {},
clear: () => {}
}), []);
return <input {...props} />;
});
const ConsistentTextarea = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
focus: () => {},
blur: () => {},
getValue: () => {},
setValue: (value) => {},
validate: () => {},
clear: () => {}
// 相同的接口,方便替换使用
}), []);
return <textarea {...props} />;
});8.3 错误处理
jsx
const ErrorHandlingComponent = forwardRef((props, ref) => {
const elementRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => {
try {
if (!elementRef.current) {
throw new Error('元素未挂载');
}
elementRef.current.focus();
} catch (error) {
console.error('聚焦失败:', error);
props.onError?.(error);
}
},
getValue: () => {
if (!elementRef.current) {
console.warn('元素未挂载,返回空字符串');
return '';
}
return elementRef.current.value;
},
safeOperation: (callback) => {
try {
if (elementRef.current) {
callback(elementRef.current);
} else {
throw new Error('元素未挂载');
}
} catch (error) {
console.error('操作失败:', error);
return { success: false, error };
}
return { success: true };
}
}), [props]);
return <input ref={elementRef} {...props} />;
});第九部分:组件库开发
9.1 Button组件
jsx
interface ButtonHandle {
focus: () => void;
blur: () => void;
click: () => void;
disable: () => void;
enable: () => void;
}
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
onClick?: () => void;
}
const Button = forwardRef<ButtonHandle, ButtonProps>(
({ children, variant = 'primary', size = 'medium', loading = false, onClick, ...props }, ref) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const [disabled, setDisabled] = useState(false);
useImperativeHandle(
ref,
(): ButtonHandle => ({
focus: () => {
buttonRef.current?.focus();
},
blur: () => {
buttonRef.current?.blur();
},
click: () => {
buttonRef.current?.click();
},
disable: () => {
setDisabled(true);
},
enable: () => {
setDisabled(false);
}
}),
[]
);
return (
<button
ref={buttonRef}
className={`btn btn-${variant} btn-${size}`}
disabled={disabled || loading}
onClick={onClick}
{...props}
>
{loading ? <span className="spinner" /> : children}
</button>
);
}
);
Button.displayName = 'Button';9.2 Select组件
jsx
interface SelectHandle {
focus: () => void;
getValue: () => string;
setValue: (value: string) => void;
getSelectedOption: () => { value: string; label: string } | null;
clear: () => void;
}
interface SelectOption {
value: string;
label: string;
}
interface SelectProps {
options: SelectOption[];
defaultValue?: string;
placeholder?: string;
onChange?: (value: string) => void;
}
const Select = forwardRef<SelectHandle, SelectProps>(
({ options, defaultValue = '', placeholder, onChange }, ref) => {
const selectRef = useRef<HTMLSelectElement>(null);
const [value, setValue] = useState(defaultValue);
useImperativeHandle(
ref,
(): SelectHandle => ({
focus: () => {
selectRef.current?.focus();
},
getValue: () => {
return value;
},
setValue: (newValue: string) => {
if (options.some(opt => opt.value === newValue)) {
setValue(newValue);
onChange?.(newValue);
}
},
getSelectedOption: () => {
const option = options.find(opt => opt.value === value);
return option || null;
},
clear: () => {
setValue('');
onChange?.('');
}
}),
[value, options, onChange]
);
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newValue = e.target.value;
setValue(newValue);
onChange?.(newValue);
};
return (
<select ref={selectRef} value={value} onChange={handleChange}>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
);
Select.displayName = 'Select';9.3 Slider组件
jsx
interface SliderHandle {
getValue: () => number;
setValue: (value: number) => void;
increment: (step?: number) => void;
decrement: (step?: number) => void;
reset: () => void;
}
interface SliderProps {
min?: number;
max?: number;
step?: number;
defaultValue?: number;
onChange?: (value: number) => void;
}
const Slider = forwardRef<SliderHandle, SliderProps>(
({ min = 0, max = 100, step = 1, defaultValue = 50, onChange }, ref) => {
const [value, setValue] = useState(defaultValue);
const sliderRef = useRef<HTMLInputElement>(null);
useImperativeHandle(
ref,
(): SliderHandle => ({
getValue: () => value,
setValue: (newValue: number) => {
const clampedValue = Math.max(min, Math.min(max, newValue));
setValue(clampedValue);
onChange?.(clampedValue);
},
increment: (customStep = step) => {
const newValue = Math.min(value + customStep, max);
setValue(newValue);
onChange?.(newValue);
},
decrement: (customStep = step) => {
const newValue = Math.max(value - customStep, min);
setValue(newValue);
onChange?.(newValue);
},
reset: () => {
setValue(defaultValue);
onChange?.(defaultValue);
}
}),
[value, min, max, step, defaultValue, onChange]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(e.target.value);
setValue(newValue);
onChange?.(newValue);
};
return (
<div className="slider-container">
<input
ref={sliderRef}
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={handleChange}
/>
<span className="slider-value">{value}</span>
</div>
);
}
);
Slider.displayName = 'Slider';
// 使用Slider
function SliderUsage() {
const sliderRef = useRef<SliderHandle>(null);
return (
<div>
<Slider
ref={sliderRef}
min={0}
max={100}
step={5}
defaultValue={50}
onChange={(value) => console.log('值变化:', value)}
/>
<div className="controls">
<button onClick={() => sliderRef.current?.increment()}>
+5
</button>
<button onClick={() => sliderRef.current?.decrement()}>
-5
</button>
<button onClick={() => sliderRef.current?.increment(20)}>
+20
</button>
<button onClick={() => sliderRef.current?.reset()}>
重置
</button>
<button onClick={() => {
const value = sliderRef.current?.getValue();
alert(`当前值: ${value}`);
}}>
获取值
</button>
</div>
</div>
);
}第十部分:复杂实战案例
10.1 富文本编辑器
jsx
interface EditorHandle {
focus: () => void;
getContent: () => string;
setContent: (content: string) => void;
insertText: (text: string) => void;
clear: () => void;
undo: () => void;
redo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
}
interface EditorProps {
defaultContent?: string;
onChange?: (content: string) => void;
placeholder?: string;
}
const RichTextEditor = forwardRef<EditorHandle, EditorProps>(
({ defaultContent = '', onChange, placeholder }, ref) => {
const editorRef = useRef<HTMLDivElement>(null);
const [content, setContent] = useState(defaultContent);
const [history, setHistory] = useState<string[]>([defaultContent]);
const [historyIndex, setHistoryIndex] = useState(0);
useImperativeHandle(
ref,
(): EditorHandle => ({
focus: () => {
editorRef.current?.focus();
},
getContent: () => {
return content;
},
setContent: (newContent: string) => {
setContent(newContent);
if (editorRef.current) {
editorRef.current.innerHTML = newContent;
}
addToHistory(newContent);
},
insertText: (text: string) => {
if (editorRef.current) {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
const newContent = editorRef.current.innerHTML;
setContent(newContent);
addToHistory(newContent);
}
}
},
clear: () => {
const newContent = '';
setContent(newContent);
if (editorRef.current) {
editorRef.current.innerHTML = newContent;
}
addToHistory(newContent);
},
undo: () => {
if (historyIndex > 0) {
const newIndex = historyIndex - 1;
const newContent = history[newIndex];
setHistoryIndex(newIndex);
setContent(newContent);
if (editorRef.current) {
editorRef.current.innerHTML = newContent;
}
}
},
redo: () => {
if (historyIndex < history.length - 1) {
const newIndex = historyIndex + 1;
const newContent = history[newIndex];
setHistoryIndex(newIndex);
setContent(newContent);
if (editorRef.current) {
editorRef.current.innerHTML = newContent;
}
}
},
canUndo: () => historyIndex > 0,
canRedo: () => historyIndex < history.length - 1
}),
[content, history, historyIndex]
);
const addToHistory = (newContent: string) => {
setHistory(prev => {
const newHistory = prev.slice(0, historyIndex + 1);
newHistory.push(newContent);
// 限制历史记录长度
if (newHistory.length > 50) {
newHistory.shift();
setHistoryIndex(prev => prev);
} else {
setHistoryIndex(newHistory.length - 1);
}
return newHistory;
});
};
const handleInput = () => {
if (editorRef.current) {
const newContent = editorRef.current.innerHTML;
setContent(newContent);
onChange?.(newContent);
addToHistory(newContent);
}
};
return (
<div
ref={editorRef}
contentEditable
className="rich-text-editor"
onInput={handleInput}
data-placeholder={placeholder}
suppressContentEditableWarning
/>
);
}
);
RichTextEditor.displayName = 'RichTextEditor';
// 使用富文本编辑器
function EditorUsage() {
const editorRef = useRef<EditorHandle>(null);
const handleBold = () => {
document.execCommand('bold');
};
const handleItalic = () => {
document.execCommand('italic');
};
const handleSave = () => {
const content = editorRef.current?.getContent();
console.log('保存内容:', content);
alert('已保存!');
};
return (
<div>
<div className="toolbar">
<button onClick={handleBold}>加粗</button>
<button onClick={handleItalic}>斜体</button>
<button onClick={() => editorRef.current?.insertText('Hello')}>
插入文本
</button>
<button onClick={() => editorRef.current?.undo()}
disabled={!editorRef.current?.canUndo()}>
撤销
</button>
<button onClick={() => editorRef.current?.redo()}
disabled={!editorRef.current?.canRedo()}>
重做
</button>
<button onClick={() => editorRef.current?.clear()}>
清空
</button>
<button onClick={handleSave}>
保存
</button>
</div>
<RichTextEditor
ref={editorRef}
placeholder="开始输入..."
onChange={(content) => console.log('内容变化:', content)}
/>
</div>
);
}10.2 Tabs组件
jsx
interface TabsHandle {
activateTab: (index: number) => void;
getActiveIndex: () => number;
nextTab: () => void;
prevTab: () => void;
}
interface TabsProps {
children: React.ReactNode;
defaultActiveIndex?: number;
onChange?: (index: number) => void;
}
const Tabs = forwardRef<TabsHandle, TabsProps>(
({ children, defaultActiveIndex = 0, onChange }, ref) => {
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
const tabs = React.Children.toArray(children);
useImperativeHandle(
ref,
(): TabsHandle => ({
activateTab: (index: number) => {
if (index >= 0 && index < tabs.length) {
setActiveIndex(index);
onChange?.(index);
tabRefs.current[index]?.focus();
}
},
getActiveIndex: () => activeIndex,
nextTab: () => {
const nextIndex = (activeIndex + 1) % tabs.length;
setActiveIndex(nextIndex);
onChange?.(nextIndex);
tabRefs.current[nextIndex]?.focus();
},
prevTab: () => {
const prevIndex = (activeIndex - 1 + tabs.length) % tabs.length;
setActiveIndex(prevIndex);
onChange?.(prevIndex);
tabRefs.current[prevIndex]?.focus();
}
}),
[activeIndex, tabs.length, onChange]
);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowRight') {
ref.current?.nextTab();
} else if (e.key === 'ArrowLeft') {
ref.current?.prevTab();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [ref]);
return (
<div className="tabs">
<div className="tab-headers">
{tabs.map((tab: any, index) => (
<button
key={index}
ref={el => tabRefs.current[index] = el}
className={`tab-header ${index === activeIndex ? 'active' : ''}`}
onClick={() => {
setActiveIndex(index);
onChange?.(index);
}}
>
{tab.props.title}
</button>
))}
</div>
<div className="tab-content">
{tabs[activeIndex]}
</div>
</div>
);
}
);
Tabs.displayName = 'Tabs';
// Tab子组件
function Tab({ title, children }: { title: string; children: React.ReactNode }) {
return <div>{children}</div>;
}
// 使用Tabs
function TabsUsage() {
const tabsRef = useRef<TabsHandle>(null);
return (
<div>
<div className="tab-controls">
<button onClick={() => tabsRef.current?.prevTab()}>
上一个标签
</button>
<button onClick={() => tabsRef.current?.nextTab()}>
下一个标签
</button>
<button onClick={() => tabsRef.current?.activateTab(0)}>
回到第一个标签
</button>
<button onClick={() => {
const index = tabsRef.current?.getActiveIndex();
alert(`当前标签索引: ${index}`);
}}>
获取当前索引
</button>
</div>
<Tabs ref={tabsRef} onChange={(index) => console.log('切换到标签:', index)}>
<Tab title="首页">
<h2>首页内容</h2>
<p>这是首页的内容</p>
</Tab>
<Tab title="个人资料">
<h2>个人资料</h2>
<p>这是个人资料页面</p>
</Tab>
<Tab title="设置">
<h2>设置</h2>
<p>这是设置页面</p>
</Tab>
</Tabs>
<p className="hint">
提示: 使用左右箭头键也可以切换标签
</p>
</div>
);
}练习题
基础练习
- 创建一个使用forwardRef的自定义输入框组件
- 使用useImperativeHandle暴露focus和clear方法
- 实现一个可以通过ref控制的Modal组件
- 创建一个带验证的表单字段组件
进阶练习
- 实现一个完整的Form组件,支持多个Field注册
- 创建一个Video播放器组件,暴露播放控制方法
- 实现一个Carousel组件,支持通过ref控制切换
- 使用TypeScript定义完整的ref类型系统
高级练习
- 实现一个富文本编辑器组件,支持撤销/重做
- 创建一个可拖拽的组件,暴露位置控制方法
- 实现一个Tab组件,支持键盘导航
- 创建一个组件库,统一ref接口设计
实战项目
- 开发一个完整的UI组件库,所有组件支持ref
- 实现一个表单构建器,动态创建和管理表单字段
- 创建一个可视化编辑器,通过ref控制元素
- 实现一个游戏,使用ref管理游戏对象
通过本章学习,你已经全面掌握了forwardRef和useImperativeHandle的使用,从基础概念到高级模式,从简单ref转发到复杂组件库开发。这两个工具是构建可复用组件的关键,掌握它们将使你能够创建专业级的React组件库。