[譯]React高級話題之Forwarding Refs

前言

本文爲意譯,翻譯過程當中摻雜本人的理解,若有誤導,請放棄繼續閱讀。javascript

原文地址:Forwarding Refshtml

Ref forwarding是一種將ref鉤子自動傳遞給組件的子孫組件的技術。對於應用的大部分組件,這種技術並非那麼的必要。然而,它對於個別的組件仍是特別地有用的,尤爲是那些可複用組件的類庫。下面的文檔講述的是這種技術的最多見的應用場景。java

正文

傳遞refs到DOM components

假設咱們有一個叫FancyButton的組件,它負責在界面上渲染出一個原生的DOM元素-button:react

function FancyButton(props) {
  return (
    <button className="FancyButton"> {props.children} </button>
  );
}
複製代碼

通常意義來講,React組件就是要隱藏它們的實現細節,包括本身的UI輸出。而其餘引用了<FancyButton>的組件也不太可能想要獲取ref,而後去訪問<FancyButton>內部的原生DOM元素button。在組件間相互引用的過程當中,儘可能地不要去依賴對方的DOM結構,這屬於一種理想的使用場景。app

對於一些應用層級下的組件,好比<FeedStory><Comment>組件(原文檔中,沒有給出這兩個組件的實現代碼,咱們只能顧名思義了),這種封裝性是咱們樂見其成的。可是,這種封裝性對於達成某些「葉子」(級別的)組件(好比,<FancyButton><MyTextInput>)的高可複用性是十分的不方便的。由於在項目的大部分場景下,咱們每每是打算把這些「葉子」組件都看成真正的DOM節點button和input來使用的。這些場景多是管理元素的聚焦,文本選擇或者動畫相關的操做。對於這些場景,訪問組件的真正DOM元素是在所不免的了。dom

Ref forwarding是組件一個可選的特徵。一個組件一旦有了這個特徵,它就能接受上層組件傳遞下來的ref,而後順勢將它傳遞給本身的子組件。ide

在下面的例子當中,<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元素同樣。動畫

下面咱們逐步逐步地來解釋一下上面所說的是如何發生的:ui

  1. 咱們經過調用React.createRef來生成了一個React ref,而且把它賦值給了ref變量。
  2. 咱們經過手動賦值給<FancyButton>的ref屬性進一步將這個React ref傳遞下去。
  3. 接着,React又將ref傳遞給React.forwardRef()調用時傳遞進來的函數(props, ref) => ...。屆時,ref將做爲這個函數的第二個參數。
  4. (props, ref) => ...組件的內部,咱們又將這個ref 傳遞給了做爲UI輸出一部分的<button ref={ref}>組件。
  5. <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。

相關文章
相關標籤/搜索