[譯]React高階話題之Forwarding Refs

鯊叔發表於2018-12-10

前言

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

原文地址: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元素一樣。

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

  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。

相關文章