前言#
在 React
中,ref
和 forwardRef
並不是在所有情況下都必須用到。 React
鼓勵使用聲明式的方式構建 UI
,通過 props
和 state
來驅動視圖更新。 ref
和 forwardRef
屬於更偏向命令式的操作,通常在需要進行 DOM
操作或者訪問組件實例時才會被使用。
使用場景#
什麼情況下 必須 (或者說非常需要 / 強烈建議) 使用 ref
和 forwardRef
:
1. 直接操作 DOM
元素:
這是 ref
最主要也是最經典的應用場景。 在 React
的聲明式世界裡,我們通常避免直接操作 DOM
。 但是,有些場景下,必須 或者 直接操作 DOM
會更方便高效:
- 焦點管理 (Focus Management):
- 聚焦到特定元素: 比如,表單加載後自動聚焦到第一個輸入框,或者模態框打開後聚焦到第一個可交互元素。
- 失去焦點時執行操作: 例如,在輸入框失去焦點時觸發驗證。
- 文本選擇 (Text Selection) 和 操作:
- 程序化地選中輸入框中的文本: 例如,點擊一個按鈕後,自動選中輸入框中的所有文本。
- 獲取或設置文本框的光標位置。
- 媒體元素控制 (Media Element Control):
- 控制
<video>
或<audio>
元素的播放、暫停、音量等: 例如,實現自定義的視頻播放器控制。 - 監聽媒體元素的事件: 例如,監聽視頻播放結束事件。
- 控制
- Canvas 元素操作:
- 獲取 Canvas 元素的 Context (2D 或 WebGL) 進行繪圖操作。
- 集成第三方 DOM 操作庫:
- 當你需要集成一些直接操作 DOM 的第三方庫 (雖然在現代 React 應用中這種情況越來越少見),例如某些舊的 jQuery 插件,或者一些需要直接訪問 DOM 元素的圖表庫。
- 觸發 DOM 元素的原生方法:
- 例如,觸發 input 元素的
focus()
,blur()
,click()
,select()
等方法。
- 例如,觸發 input 元素的
示例:聚焦輸入框
import React, { useRef, useEffect } from 'react';
function MyForm() {
const inputRef = useRef(null);
useEffect(() => {
// 組件掛載後,聚焦到 input 元素
inputRef.current.focus();
}, []);
return (
<div>
<input type="text" ref={inputRef} placeholder="請輸入..." />
{/* ... 其他表單元素 ... */}
</div>
);
}
2. 訪問子組件的 DOM 節點 (需要 forwardRef
):
默認情況下,父組件無法直接通過 ref
訪問子組件的 DOM 節點。 如果你想要在父組件中獲取子組件的 DOM 元素 (例如,子組件是一個自定義的 Input 組件,父組件需要獲取其內部的 <input>
元素), 就需要使用 forwardRef
將 ref 傳遞到子組件內部的 DOM 元素上。
為什麼需要 forwardRef
?
這是因為 ref
默認只能綁定到組件實例 (對於 class 組件) 或 DOM 元素。 對於函數式組件,默認情況下 ref
會綁定到組件本身,但函數式組件本身沒有實例,所以 ref
通常為 null
。 forwardRef
允許你將父組件傳遞的 ref
轉發到子組件的某個 DOM 元素或 class 組件實例上。
示例:父組件獲取子組件 Input 的 DOM 元素
import React, { useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
// 子組件 - 自定義 Input 組件
const CustomInput = forwardRef((props, ref) => {
const inputElementRef = useRef(null);
// 使用 useImperativeHandle 暴露子組件的方法 (可選,非 forwardRef 必須)
useImperativeHandle(ref, () => ({
focus: () => {
inputElementRef.current.focus();
},
getValue: () => {
return inputElementRef.current.value;
}
}));
return (
<div>
<label htmlFor={props.id}>{props.label}: </label>
<input type="text" id={props.id} ref={inputElementRef} {...props} />
</div>
);
});
CustomInput.displayName = 'CustomInput'; // 方便 React DevTools 顯示
// 父組件
function ParentComponent() {
const customInputRef = useRef(null);
useEffect(() => {
// 組件掛載後,聚焦到 CustomInput 組件內部的 input 元素
customInputRef.current.focus();
// 獲取 CustomInput 組件內部 input 的值 (通過 useImperativeHandle 暴露的方法)
// const inputValue = customInputRef.current.getValue();
// console.log("Input Value:", inputValue);
}, []);
return (
<div>
<CustomInput label="姓名" id="nameInput" ref={customInputRef} />
{/* ... 其他內容 ... */}
</div>
);
}
3. 在某些特定的組件庫或場景中需要訪問組件實例 (通過 useImperativeHandle
結合 forwardRef
):
雖然 React 提倡數據驅動,但有些情況下,你可能需要在父組件中命令式地調用子組件的方法,例如:
- 控制子組件的內部狀態或行為: 例如,觸發一個彈窗組件的
open()
或close()
方法,或者調用一個表格組件的refreshData()
方法。 - 集成某些第三方 UI 組件庫: 有些組件庫可能會提供命令式 API,需要通過 ref 獲取組件實例來調用其方法。
使用 useImperativeHandle
控制暴露給父組件的 API:
當你需要讓父組件通過 ref 訪問子組件的方法時,通常會結合 forwardRef
和 useImperativeHandle
Hook。 useImperativeHandle
允許你 精確地控制哪些方法和屬性可以通過 ref 暴露給父組件,避免暴露所有內部細節。
什麼時候應該避免使用 ref
和 forwardRef
:
- 一切都應該優先考慮聲明式方式 (props 和 state)。 儘量通過 props 傳遞數據和回調函數來控制子組件的行為和狀態。
- 避免過度使用
ref
進行組件間的通信。 過多的ref
使用會使組件關係變得複雜和難以維護,破壞 React 的數據流。 - 不要為了訪問子組件的 state 而使用
ref
。 應該通過 props 傳遞數據,讓子組件將數據通過回調函數傳遞給父組件 (狀態提升)。 - 在沒有明確必要性的情況下,不要濫用
forwardRef
。 如果父組件不需要直接訪問子組件的 DOM 或實例,就不需要使用forwardRef
。
總結:
ref
和 forwardRef
是 React 中強大的工具,但應該謹慎使用。 它們在以下情況是必要的或非常有用的:
- 需要直接操作 DOM 元素時 (例如,焦點管理、媒體控制等)。
- 需要在父組件中訪問子組件的 DOM 節點時 (使用
forwardRef
)。 - 在某些特定場景下,需要命令式地調用子組件的方法時 (使用
forwardRef
和useImperativeHandle
)。
記住,優先考慮聲明式的方式解決問題。 只有在聲明式方式難以實現或者命令式操作更加高效簡潔的情況下,才考慮使用 ref
和 forwardRef
。 避免過度使用它們,保持代碼的清晰和可維護性。