Ref 轉(zhuǎn)發(fā)是一項(xiàng)將 ref 自動(dòng)地通過組件傳遞到其一子組件的技巧。對于大多數(shù)應(yīng)用中的組件來說,這通常不是必需的。但其對某些組件,尤其是可重用的組件庫是很有用的。最常見的案例如下所述。
轉(zhuǎn)發(fā) refs 到 DOM 組件
考慮這個(gè)渲染原生 DOM 元素 button 的 FancyButton 組件:
function FancyButton(props) {
return (
<button className="FancyButton">
{props.children}
</button>
);
}
React 組件隱藏其實(shí)現(xiàn)細(xì)節(jié),包括其渲染結(jié)果。其他使用 FancyButton 的組件通常不需要獲取內(nèi)部的 DOM 元素 button 的 ref。這很好,因?yàn)檫@防止組件過度依賴其他組件的 DOM 結(jié)構(gòu)。
雖然這種封裝對類似 FeedStory 或 Comment 這樣的應(yīng)用級組件是理想的,但其對 FancyButton 或 MyTextInput 這樣的高可復(fù)用“葉”組件來說可能是不方便的。這些組件傾向于在整個(gè)應(yīng)用中以一種類似常規(guī) DOM button 和 input 的方式被使用,并且訪問其 DOM 節(jié)點(diǎn)對管理焦點(diǎn),選中或動(dòng)畫來說是不可避免的。
Ref 轉(zhuǎn)發(fā)是一個(gè)可選特性,其允許某些組件接收 ref,并將其向下傳遞(換句話說,“轉(zhuǎn)發(fā)”它)給子組件。
在下面的示例中,F(xiàn)ancyButton 使用 React.forwardRef 來獲取傳遞給它的 ref,然后轉(zhuǎn)發(fā)到它渲染的 DOM button:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接獲取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
這樣,使用 FancyButton 的組件可以獲取底層 DOM 節(jié)點(diǎn) button 的 ref ,并在必要時(shí)訪問,就像其直接使用 DOM button 一樣。
以下是對上述示例發(fā)生情況的逐步解釋:
- 我們通過調(diào)用
React.createRef創(chuàng)建了一個(gè) React ref 并將其賦值給ref變量。 - 我們通過指定
ref為 JSX 屬性,將其向下傳遞給<FancyButton ref={ref}>。 - React 傳遞
ref給forwardRef內(nèi)函數(shù)(props, ref) => ...,作為其第二個(gè)參數(shù)。 - 我們向下轉(zhuǎn)發(fā)該
ref參數(shù)到<button ref={ref}>,將其指定為 JSX 屬性。 - 當(dāng) ref 掛載完成,
ref.current將指向<button>DOM 節(jié)點(diǎn)。
注意
第二個(gè)參數(shù) ref 只在使用 React.forwardRef 定義組件時(shí)存在。常規(guī)函數(shù)和 class 組件不接收 ref 參數(shù),且 props 中也不存在 ref。
Ref 轉(zhuǎn)發(fā)不僅限于 DOM 組件,你也可以轉(zhuǎn)發(fā) refs 到 class 組件實(shí)例中。
組件庫維護(hù)者的注意事項(xiàng)
當(dāng)你開始在組件庫中使用 forwardRef 時(shí),你應(yīng)當(dāng)將其視為一個(gè)破壞性更改,并發(fā)布庫的一個(gè)新的主版本。 這是因?yàn)槟愕膸炜赡軙?huì)有明顯不同的行為(例如 refs 被分配給了誰,以及導(dǎo)出了什么類型),并且這樣可能會(huì)導(dǎo)致依賴舊行為的應(yīng)用和其他庫崩潰。
出于同樣的原因,當(dāng) React.forwardRef 存在時(shí)有條件地使用它也是不推薦的:它改變了你的庫的行為,并在升級 React 自身時(shí)破壞用戶的應(yīng)用。
在高階組件中轉(zhuǎn)發(fā) refs
這個(gè)技巧對高階組件(也被稱為 HOC)特別有用。讓我們從一個(gè)輸出組件 props 到控制臺的 HOC 示例開始:
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return LogProps;
}
“l(fā)ogProps” HOC 透傳(pass through)所有 props 到其包裹的組件,所以渲染結(jié)果將是相同的。例如:我們可以使用該 HOC 記錄所有傳遞到 “fancy button” 組件的 props:
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// 我們導(dǎo)出 LogProps,而不是 FancyButton。
// 雖然它也會(huì)渲染一個(gè) FancyButton。
export default logProps(FancyButton);
上面的示例有一點(diǎn)需要注意:refs 將不會(huì)透傳下去。這是因?yàn)?ref 不是 prop 屬性。就像 key 一樣,其被 React 進(jìn)行了特殊處理。如果你對 HOC 添加 ref,該 ref 將引用最外層的容器組件,而不是被包裹的組件。
這意味著用于我們 FancyButton 組件的 refs 實(shí)際上將被掛載到 LogProps 組件:
import FancyButton from './FancyButton';
const ref = React.createRef();
// 我們導(dǎo)入的 FancyButton 組件是高階組件(HOC)LogProps。
// 盡管渲染結(jié)果將是一樣的,
// 但我們的 ref 將指向 LogProps 而不是內(nèi)部的 FancyButton 組件!
// 這意味著我們不能調(diào)用例如 ref.current.focus() 這樣的方法
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
幸運(yùn)的是,我們可以使用 React.forwardRef API 明確地將 refs 轉(zhuǎn)發(fā)到內(nèi)部的 FancyButton 組件。React.forwardRef 接受一個(gè)渲染函數(shù),其接收 props 和 ref 參數(shù)并返回一個(gè) React 節(jié)點(diǎn)。例如:
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 將自定義的 prop 屬性 “forwardedRef” 定義為 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回調(diào)的第二個(gè)參數(shù) “ref”。
// 我們可以將其作為常規(guī) prop 屬性傳遞給 LogProps,例如 “forwardedRef”
// 然后它就可以被掛載到被 LogProps 包裹的子組件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
在 DevTools 中顯示自定義名稱
React.forwardRef 接受一個(gè)渲染函數(shù)。React DevTools 使用該函數(shù)來決定為 ref 轉(zhuǎn)發(fā)組件顯示的內(nèi)容。
例如,以下組件將在 DevTools 中顯示為 “ForwardRef”:
const WrappedComponent = React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
如果你命名了渲染函數(shù),DevTools 也將包含其名稱(例如 “ForwardRef(myFunction)”):
const WrappedComponent = React.forwardRef(
function myFunction(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
);
你甚至可以設(shè)置函數(shù)的 displayName 屬性來包含被包裹組件的名稱:
function logProps(Component) {
class LogProps extends React.Component {
// ...
}
function forwardRef(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
// 在 DevTools 中為該組件提供一個(gè)更有用的顯示名。
// 例如 “ForwardRef(logProps(MyComponent))”
const name = Component.displayName || Component.name;
forwardRef.displayName = `logProps(${name})`;
return React.forwardRef(forwardRef);
}