前言
React Ref(引用)是React中一個強大而重要的概念,它為我們提供了直接訪問DOM元素或組件實例的能力。雖然React推崇聲明式編程和數據驅動的理念,但在某些場景下,我們仍需要直接操作DOM或訪問組件實例。本文將深入探討React Ref的工作原理、使用方法和最佳實踐。
什么是React Ref?
React Ref是一個可以讓我們訪問DOM節點或在render方法中創建的React元素的方式。它本質上是一個對象,包含一個current
屬性,用于存儲對真實DOM節點或組件實例的引用。
為什么需要Ref?
在React的聲明式編程模型中,數據流是單向的:props向下傳遞,事件向上冒泡。但在以下場景中,我們需要直接訪問DOM或組件:
- 管理焦點、文本選擇或媒體播放
- 觸發強制動畫
- 集成第三方DOM庫
- 測量DOM元素的尺寸
- 訪問子組件的方法
Ref的演進歷史
1. String Refs(已廢棄)
// 不推薦使用
class MyComponent extends React.Component {componentDidMount() {this.refs.myInput.focus();}render() {return <input ref="myInput" />;}
}
String Refs存在性能問題和潛在的內存泄漏風險,已在React 16.3中被廢棄。
2. Callback Refs
class MyComponent extends React.Component {setInputRef = (element) => {this.inputElement = element;}componentDidMount() {if (this.inputElement) {this.inputElement.focus();}}render() {return <input ref={this.setInputRef} />;}
}
3. createRef(React 16.3+)
class MyComponent extends React.Component {constructor(props) {super(props);this.inputRef = React.createRef();}componentDidMount() {this.inputRef.current.focus();}render() {return <input ref={this.inputRef} />;}
}
4. useRef Hook(React 16.8+)
function MyComponent() {const inputRef = useRef(null);useEffect(() => {inputRef.current.focus();}, []);return <input ref={inputRef} />;
}
深入理解useRef
useRef的基本用法
useRef
返回一個可變的ref對象,其.current
屬性被初始化為傳入的參數。
const refContainer = useRef(initialValue);
useRef的特點
- 持久化存儲:useRef在組件的整個生命周期中保持同一個引用
- 不觸發重新渲染:修改
.current
屬性不會觸發組件重新渲染 - 同步更新:
.current
的值會同步更新,不像state那樣異步
useRef vs useState
function RefVsState() {const [stateValue, setStateValue] = useState(0);const refValue = useRef(0);const updateState = () => {setStateValue(prev => prev + 1);console.log('State value:', stateValue); // 異步更新,可能顯示舊值};const updateRef = () => {refValue.current += 1;console.log('Ref value:', refValue.current); // 同步更新,顯示新值};return (<div><p>State: {stateValue}</p><p>Ref: {refValue.current}</p><button onClick={updateState}>Update State</button><button onClick={updateRef}>Update Ref</button></div>);
}
Ref的實際應用場景
1. 訪問DOM元素
function FocusInput() {const inputRef = useRef(null);const handleFocus = () => {inputRef.current.focus();};const handleClear = () => {inputRef.current.value = '';};return (<div><input ref={inputRef} type="text" /><button onClick={handleFocus}>Focus Input</button><button onClick={handleClear}>Clear Input</button></div>);
}
2. 存儲可變值
function Timer() {const [time, setTime] = useState(0);const intervalRef = useRef(null);const start = () => {if (intervalRef.current) return;intervalRef.current = setInterval(() => {setTime(prev => prev + 1);}, 1000);};const stop = () => {if (intervalRef.current) {clearInterval(intervalRef.current);intervalRef.current = null;}};useEffect(() => {return () => {if (intervalRef.current) {clearInterval(intervalRef.current);}};}, []);return (<div><p>Time: {time}</p><button onClick={start}>Start</button><button onClick={stop}>Stop</button></div>);
}
3. 保存上一次的值
function usePrevious(value) {const ref = useRef();useEffect(() => {ref.current = value;});return ref.current;
}function MyComponent({ count }) {const prevCount = usePrevious(count);return (<div><p>Current: {count}</p><p>Previous: {prevCount}</p></div>);
}
高級Ref技巧
1. forwardRef
forwardRef
允許組件將ref轉發到其子組件:
const FancyInput = React.forwardRef((props, ref) => (<input ref={ref} className="fancy-input" {...props} />
));function Parent() {const inputRef = useRef(null);const handleFocus = () => {inputRef.current.focus();};return (<div><FancyInput ref={inputRef} /><button onClick={handleFocus}>Focus Input</button></div>);
}
2. useImperativeHandle
useImperativeHandle
可以自定義暴露給父組件的實例值:
const CustomInput = React.forwardRef((props, ref) => {const inputRef = useRef(null);useImperativeHandle(ref, () => ({focus: () => {inputRef.current.focus();},scrollIntoView: () => {inputRef.current.scrollIntoView();},getValue: () => {return inputRef.current.value;}}));return <input ref={inputRef} {...props} />;
});function Parent() {const customInputRef = useRef(null);const handleAction = () => {customInputRef.current.focus();console.log(customInputRef.current.getValue());};return (<div><CustomInput ref={customInputRef} /><button onClick={handleAction}>Focus and Get Value</button></div>);
}
3. Ref回調函數
function MeasureElement() {const [dimensions, setDimensions] = useState({ width: 0, height: 0 });const measureRef = useCallback((node) => {if (node !== null) {setDimensions({width: node.getBoundingClientRect().width,height: node.getBoundingClientRect().height});}}, []);return (<div><div ref={measureRef} style={{ padding: '20px', border: '1px solid #ccc' }}>Measure me!</div><p>Width: {dimensions.width}px</p><p>Height: {dimensions.height}px</p></div>);
}
最佳實踐與注意事項
1. 避免過度使用Ref
// ? 不推薦:過度使用ref
function BadExample() {const inputRef = useRef(null);const [value, setValue] = useState('');const handleChange = () => {setValue(inputRef.current.value); // 不必要的ref使用};return <input ref={inputRef} onChange={handleChange} />;
}// ? 推薦:使用受控組件
function GoodExample() {const [value, setValue] = useState('');const handleChange = (e) => {setValue(e.target.value);};return <input value={value} onChange={handleChange} />;
}
2. 檢查ref的有效性
function SafeRefUsage() {const elementRef = useRef(null);const handleClick = () => {// 總是檢查ref是否有效if (elementRef.current) {elementRef.current.focus();}};return (<div><input ref={elementRef} /><button onClick={handleClick}>Focus</button></div>);
}
3. 清理副作用
function ComponentWithCleanup() {const intervalRef = useRef(null);useEffect(() => {intervalRef.current = setInterval(() => {console.log('Interval running');}, 1000);// 清理函數return () => {if (intervalRef.current) {clearInterval(intervalRef.current);}};}, []);return <div>Component with cleanup</div>;
}
4. 避免在渲染期間訪問ref
// ? 不推薦:在渲染期間訪問ref
function BadRefUsage() {const inputRef = useRef(null);// 渲染期間訪問ref可能為nullconst inputValue = inputRef.current?.value || '';return <input ref={inputRef} placeholder={inputValue} />;
}// ? 推薦:在effect或事件處理器中訪問ref
function GoodRefUsage() {const inputRef = useRef(null);const [placeholder, setPlaceholder] = useState('');useEffect(() => {if (inputRef.current) {setPlaceholder(inputRef.current.value || 'Enter text');}});return <input ref={inputRef} placeholder={placeholder} />;
}
性能考慮
1. 使用useCallback優化ref回調
function OptimizedRefCallback() {const [dimensions, setDimensions] = useState({ width: 0, height: 0 });// 使用useCallback避免不必要的重新渲染const measureRef = useCallback((node) => {if (node !== null) {const rect = node.getBoundingClientRect();setDimensions({ width: rect.width, height: rect.height });}}, []);return <div ref={measureRef}>Measured content</div>;
}
2. 避免內聯ref回調
// ? 不推薦:內聯ref回調
function InlineRefCallback() {const [element, setElement] = useState(null);return (<div ref={(node) => setElement(node)}>Content</div>);
}// ? 推薦:使用useCallback
function OptimizedRefCallback() {const [element, setElement] = useState(null);const refCallback = useCallback((node) => {setElement(node);}, []);return <div ref={refCallback}>Content</div>;
}
實際項目示例
自定義Hook:useClickOutside
function useClickOutside(callback) {const ref = useRef(null);useEffect(() => {const handleClick = (event) => {if (ref.current && !ref.current.contains(event.target)) {callback();}};document.addEventListener('mousedown', handleClick);return () => {document.removeEventListener('mousedown', handleClick);};}, [callback]);return ref;
}// 使用示例
function Dropdown() {const [isOpen, setIsOpen] = useState(false);const dropdownRef = useClickOutside(() => setIsOpen(false));return (<div ref={dropdownRef}><button onClick={() => setIsOpen(!isOpen)}>Toggle Dropdown</button>{isOpen && (<div className="dropdown-menu"><p>Dropdown content</p></div>)}</div>);
}