Composition vs 繼承

React 具有強大的 composition 模型,我們建議你在 component 之間使用 composition 來複用你的程式碼,而不是使用繼承。

在這個章節中,我們將考慮一些新 React 開發者常常遇到的繼承問題,並示範如何透過 composition 來解決它們。

包含

有些 component 不會提早知道它們的 children 有些什麼。對於像是 SidebarDialog 這類通用的「box」component 特別常見。

我們建議這些 component 使用特殊的 children prop 將 children element 直接傳入到它們的輸出:

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

這讓其他的 component 透過巢狀的 JSX 將任意的 children 傳遞給它們:

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

在 CodePen 試試看吧!

任何在 <FancyBorder> JSX tag 內的內容都被作為 children prop 被傳遞給 FancyBorder component。由於 FancyBorder<div> 內 render {props.children},被傳遞的 element 會在最終的輸出出現。

雖然這個情況不常見,但有時候你可能需要在 component 中使用多個「hole」。在這種情況下,你可以使用你慣用的方法,而不是使用 children

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

在 CodePen 上試試吧!

React 的 element 像是 <Contacts /><Chat /> 都只是 object,所以你可以像其它任何的資料一樣,將它們作為 props 傳遞。這種方法可能會提醒你其他函式庫中的「slot」,但對於你可以在 React 中作為 prop 傳遞的內容沒有限制。

特別化

有時候,我們需要考慮 component 會不會是其他 component 的「特別情況」。例如,我們可能會說 WelcomeDialogDialog 的一個特定情況。

在 React 中,這也可以透過 composition 被實現,其中更「特別」的 component render 更多「通用」的 component,並使用 prop 對其進行設定:

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </FancyBorder>
  );
}

function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thank you for visiting our spacecraft!" />
  );
}

在 CodePen 上試試看吧!

對於使用 class 定義的 component,composition 一樣有效:

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </FancyBorder>
  );
}

class SignUpDialog extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = {login: ''};
  }

  render() {
    return (
      <Dialog title="Mars Exploration Program"
              message="How should we refer to you?">
        <input value={this.state.login}
               onChange={this.handleChange} />
        <button onClick={this.handleSignUp}>
          Sign Me Up!
        </button>
      </Dialog>
    );
  }

  handleChange(e) {
    this.setState({login: e.target.value});
  }

  handleSignUp() {
    alert(`Welcome aboard, ${this.state.login}!`);
  }
}

在 CodePen 上試試看吧!

那麼關於繼承呢?

在 Facebook 中,我們使用 React 在成千上萬個 component,我們找不到任何使用案例來推薦你建立繼承結構的 component。

Prop 和 composition 提供你明確和安全的方式來自訂 component 的外觀和行為所需的靈活性。請記得,component 可以接受任意的 prop,包含 primitive value、React element,或者是 function。

如果你想要在 component 之間複用非 UI 的功能,我們建議抽離它到一個獨立的 JavaScript 模組。Component 可以 import 並使用它的 function、object,或者是 class,而不需要繼承它。