[譯]React高階話題之Forwarding Refs
前言
本文為意譯,翻譯過程中摻雜本人的理解,如有誤導,請放棄繼續閱讀。
原文地址:Forwarding Refs
Ref forwarding是一種將ref鉤子自動傳遞給元件的子孫元件的技術。對於應用的大部分元件,這種技術並不是那麼的必要。然而,它對於個別的元件還是特別地有用的,尤其是那些可複用元件的類庫。下面的文件講述的是這種技術的最常見的應用場景。
正文
傳遞refs到DOM components
假設我們有一個叫FancyButton的元件,它負責在介面上渲染出一個原生的DOM元素-button:
function FancyButton(props) {
return (
<button className="FancyButton">
{props.children}
</button>
);
}
複製程式碼
一般意義來說,React元件就是要隱藏它們的實現細節,包括自己的UI輸出。而其他引用了<FancyButton>
的元件也不太可能想要獲取ref,然後去訪問<FancyButton>
內部的原生DOM元素button。在元件間相互引用的過程中,儘量地不要去依賴對方的DOM結構,這屬於一種理想的使用場景。
對於一些應用層級下的元件,比如<FeedStory>
和<Comment>
<FancyButton>
和<MyTextInput>
)的高可複用性是十分的不方便的。因為在專案的大部分場景下,我們往往是打算把這些“葉子”元件都當作真正的DOM節點button和input來使用的。這些場景可能是管理元素的聚焦,文字選擇或者動畫相關的操作。對於這些場景,訪問元件的真正DOM元素是在所難免的了。
Ref forwarding是元件一個可選的特徵。一個元件一旦有了這個特徵,它就能接受上層元件傳遞下來的ref,然後順勢將它傳遞給自己的子元件。
在下面的例子當中,<FancyButton>
通過React.forwardRef的賦能,它可以接收上層元件傳遞下來的ref,並將它傳遞給自己的子元件-一個原生的DOM元素button:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 假如你沒有通過 React.createRef的賦能,在function component上你是不可以直接掛載ref屬性的。
// 而現在你可以這麼做了,並能訪問到原生的DOM元素:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
複製程式碼
通過這種方式,使用了<FancyButton>
的元件就能通過掛載ref到<FancyButton>
元件的身上來訪問到對應的底層的原生DOM元素了-就像直接訪問這個DOM元素一樣。
下面我們逐步逐步地來解釋一下上面所說的是如何發生的:
- 我們通過呼叫React.createRef來生成了一個React ref,並且把它賦值給了ref變數。
- 我們通過手動賦值給
<FancyButton>
的ref屬性進一步將這個React ref傳遞下去。 - 接著,React又將ref傳遞給React.forwardRef()呼叫時傳遞進來的函式
(props, ref) => ...
。屆時,ref將作為這個函式的第二個引數。 - 在
(props, ref) => ...
元件的內部,我們又將這個ref 傳遞給了作為UI輸出一部分的<button ref={ref}>
元件。 - 當
<button ref={ref}>
元件被真正地掛載到頁面的時候,,我們就可以在使用ref.current
來訪問真正的DOM元素button了。
注意,上面提到的第二個引數ref只有在你通過呼叫React.forwardRef()來定義元件的情況下才會存在。普通的function component和 class component是不會收到這個ref引數的。同時,ref也不是props的一個屬性。
Ref forwarding技術不單單用於將ref傳遞到DOM component。它也適用於將ref傳遞到class component,以此你可以獲取這個class component的例項引用。
元件類庫維護者的注意事項
當你在你的元件類庫中引入了forwardRef,那麼你就應該把這個引入看作一個breaking change,並給你的類庫釋出個major版本。這麼說,是因為一旦你引入了這個特性,那你的類庫將會表現得跟以往是不同( 例如:what refs get assigned to, and what types are exported),這將會打破其他依賴於老版ref功能的類庫和整個應用的正常功能。
我們得有條件地使用React.forwardRef
,即使有這樣的條件,我們也推薦你能不用就不要用。理由是:React.forwardRef
會改變你類庫的行為,並且會在使用者升級React版本的時候打破使用者應用的正常功能。
高階元件裡的Forwarding refs
這種技術對於高階元件來說也是特別有用的。假設,我們要實現一個列印props的高階元件,以往我們是這麼寫的:
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;
}
複製程式碼
高階元件logProps
將所有的props都照樣傳遞給了WrappedComponent
,所以高階元件的UI輸出和WrappedComponent
的UI輸出將會一樣的。舉個例子,我們將會使用這個高階元件來把我們傳遞給<FancyButton>
的props答應出來。
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);
複製程式碼
上面的例子有一個要注意的地方是:refs實際上並沒有被傳遞下去(到WrappedComponent
元件中)。這是因為ref並不是真正的prop。正如key一樣,它們都不是真正的prop,而是被用於React的內部實現。像上面的例子那樣給一個高階元件直接傳遞ref,那麼這個ref指向的將會是(高階元件所返回)的containercomponent例項而不是wrapper component例項:
import FancyButton from './FancyButton';
const ref = React.createRef();
// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
複製程式碼
幸運的是,我們可以通過呼叫React.forwardRef這個API來顯式地傳遞ref到FancyButton
元件的內部。React.forwardRef接收一個render function,這個render function將會得到兩個實參:props和ref。舉例如下:
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;
// Assign the custom prop "forwardedRef" as a ref
+ return <Component ref={forwardedRef} {...rest} />;
- return <Component {...this.props} />;
}
}
// Note the second param "ref" provided by React.forwardRef.
// We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
// And it can then be attached to the Component.
+ return React.forwardRef((props, ref) => {
+ return <LogProps {...props} forwardedRef={ref} />;
+ });
}
複製程式碼
在DevTools裡面顯示一個自定義的名字
React.forwardRef接收一個render function。React DevTools將會使用這個function來決定將ref forwarding component名顯示成什麼樣子。
舉個例子,下面的WrappedComponent就是ref forwarding component。它在React DevTools將會顯示成“ForwardRef”:
const WrappedComponent = React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
複製程式碼
假如你給render function命名了,那麼React DevTools將會把這個名字包含在ref forwarding component名中(如下,顯示為“ForwardRef(myFunction)”):
const WrappedComponent = React.forwardRef(
function myFunction(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
);
複製程式碼
你甚至可以把wrappedComponent的名字也囊括進來,讓它成為render function的displayName的一部分(如下,顯示為“ForwardRef(logProps(${wrappedComponent.name}))”):
function logProps(Component) {
class LogProps extends React.Component {
// ...
}
function forwardRef(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
// Give this component a more helpful display name in DevTools.
// e.g. "ForwardRef(logProps(MyComponent))"
const name = Component.displayName || Component.name;
forwardRef.displayName = `logProps(${name})`;
return React.forwardRef(forwardRef);
}
複製程式碼
這樣一來,你就可以看到一條清晰的refs傳遞路徑:React.forwardRef -> logProps -> wrappedComponent。如果這個wrappeedComponent是我們上面用React.forwardRef包裹的FancyButton
,這條路徑可以更長:React.forwardRef -> logProps -> React.forwardRef -> FancyButton -> button。