在React開發中,組件間的通信是一個核心話題。雖然props和state能夠處理大部分場景,但有時我們需要更直接的方式來操作子組件。今天我們來深入探討兩個強大的React Hook:forwardRef
和useImperativeHandle
。
forwardRef:傳遞引用的橋梁
什么是forwardRef?
forwardRef
是React提供的一個高階組件,它允許組件將ref傳遞給其子組件。在正常情況下,ref只能用于DOM元素或類組件,但通過forwardRef,我們可以讓函數組件也能接收和轉發ref。
基本語法
const MyComponent = React.forwardRef((props, ref) => {return <div ref={ref}>Hello World</div>;
});
實際應用場景
場景1:封裝輸入組件
import React, { forwardRef, useRef } from 'react';const CustomInput = forwardRef((props, ref) => {return (<div className="input-wrapper"><label>{props.label}</label><inputref={ref}type={props.type || 'text'}placeholder={props.placeholder}{...props}/></div>);
});// 使用示例
function App() {const inputRef = useRef(null);const focusInput = () => {inputRef.current?.focus();};return (<div><CustomInputref={inputRef}label="用戶名"placeholder="請輸入用戶名"/><button onClick={focusInput}>聚焦輸入框</button></div>);
}
場景2:組件庫開發
在開發組件庫時,forwardRef特別有用,因為用戶可能需要直接訪問底層DOM元素:
const Button = forwardRef(({ children, variant = 'primary', ...props }, ref) => {return (<buttonref={ref}className={`btn btn-${variant}`}{...props}>{children}</button>);
});
useImperativeHandle:精確控制暴露的接口
什么是useImperativeHandle?
useImperativeHandle
允許我們自定義通過ref暴露給父組件的實例值。它通常與forwardRef一起使用,讓我們能夠精確控制哪些方法和屬性對外可見。
基本語法
useImperativeHandle(ref, createHandle, [deps])
ref
:從forwardRef傳入的refcreateHandle
:返回暴露值的函數deps
:依賴數組(可選)
高級應用場景
場景1:可控制的媒體播放器
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';const VideoPlayer = forwardRef((props, ref) => {const videoRef = useRef(null);const [isPlaying, setIsPlaying] = useState(false);const [currentTime, setCurrentTime] = useState(0);useImperativeHandle(ref, () => ({play: () => {videoRef.current?.play();setIsPlaying(true);},pause: () => {videoRef.current?.pause();setIsPlaying(false);},seek: (time) => {if (videoRef.current) {videoRef.current.currentTime = time;setCurrentTime(time);}},getCurrentTime: () => currentTime,isPlaying: () => isPlaying,getDuration: () => videoRef.current?.duration || 0}), [isPlaying, currentTime]);return (<videoref={videoRef}src={props.src}onTimeUpdate={(e) => setCurrentTime(e.target.currentTime)}style={{ width: '100%', height: 'auto' }}/>);
});// 使用示例
function MediaController() {const playerRef = useRef(null);const handlePlay = () => playerRef.current?.play();const handlePause = () => playerRef.current?.pause();const handleSeek = () => playerRef.current?.seek(30);return (<div><VideoPlayer ref={playerRef} src="/video.mp4" /><div><button onClick={handlePlay}>播放</button><button onClick={handlePause}>暫停</button><button onClick={handleSeek}>跳轉到30秒</button></div></div>);
}
場景2:表單驗證組件
const ValidatedInput = forwardRef(({ validation, ...props }, ref) => {const [value, setValue] = useState('');const [error, setError] = useState('');const inputRef = useRef(null);const validate = () => {if (validation) {const result = validation(value);setError(result.error || '');return result.isValid;}return true;};useImperativeHandle(ref, () => ({validate,focus: () => inputRef.current?.focus(),getValue: () => value,setValue: (newValue) => setValue(newValue),clearError: () => setError(''),hasError: () => !!error}));return (<div><inputref={inputRef}value={value}onChange={(e) => setValue(e.target.value)}onBlur={validate}{...props}/>{error && <span className="error">{error}</span>}</div>);
});// 使用示例
function RegistrationForm() {const emailRef = useRef(null);const passwordRef = useRef(null);const handleSubmit = (e) => {e.preventDefault();const emailValid = emailRef.current?.validate();const passwordValid = passwordRef.current?.validate();if (emailValid && passwordValid) {console.log('表單提交成功');} else {console.log('表單驗證失敗');}};return (<form onSubmit={handleSubmit}><ValidatedInputref={emailRef}type="email"placeholder="郵箱"validation={(value) => ({isValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),error: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? '' : '請輸入有效郵箱'})}/><ValidatedInputref={passwordRef}type="password"placeholder="密碼"validation={(value) => ({isValid: value.length >= 6,error: value.length >= 6 ? '' : '密碼至少6位'})}/><button type="submit">注冊</button></form>);
}
最佳實踐和注意事項
1. 避免過度使用
雖然這兩個Hook很強大,但不應該成為組件通信的首選方案。優先考慮props和callback的方式:
// ? 過度使用imperative方式
const BadExample = forwardRef((props, ref) => {useImperativeHandle(ref, () => ({updateData: (data) => setData(data),showModal: () => setModalVisible(true),hideModal: () => setModalVisible(false)}));// ...
});// ? 更好的聲明式方式
const GoodExample = ({ data, modalVisible, onDataChange, onModalToggle }) => {// ...
};
2. 合理命名和文檔化
const DataTable = forwardRef((props, ref) => {useImperativeHandle(ref, () => ({// 清晰的方法命名refreshData: () => fetchData(),exportToCSV: () => exportData('csv'),exportToExcel: () => exportData('excel'),selectAllRows: () => setSelectedRows(allRows),clearSelection: () => setSelectedRows([])}));
});
3. 性能優化
使用依賴數組來避免不必要的重新創建:
useImperativeHandle(ref, () => ({someMethod: () => {// 方法實現}
}), [dependency1, dependency2]); // 添加依賴數組
4. TypeScript支持
interface VideoPlayerRef {play: () => void;pause: () => void;seek: (time: number) => void;getCurrentTime: () => number;
}const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>((props, ref) => {// 實現
});