React公式ドキュメントを読む26 コンポーネントを構成するコンポジション

前回からの続きですが、このページだけ読んでも分かる内容になっています。 今回読むReact公式ドキュメントは「コンポジション vs 継承」のページです。

Reactでアプリを作成する際には、コンポーネントをいかに分割して構成するかが重要となります。Reactコンポーネントのコンポジションについて見ていきましょう。

Reactにおけるコンポジションとは?

Reactでアプリを作成する場合、ヘッダ・フッタ・サイドバー・メニューなどの部品をコンポーネントによって作成します。Reactでアプリを作成する際には、コンポーネントをどのように分割して構成するかが重要となります。これを「Reactコンポーネントのコンポジション」と呼びます。

例えば、何らかの機能を実現するひとつのコンポーネントがある場合、これを2つのコンポーネントに分割して、分割したコンポーネント間でデータを受け渡すことで同じ機能を実現できるかもしれません。適切なコンポジションにより、プログラムの見晴らしを良くしてコンポーネントの再利用性を高められます。

複数のエンジニアが開発に関わる場合、それぞれが書いたコンポーネントが組み合わさって一つのアプリとして正しく動作する必要があります。ひとつひとつの Reactコンポーネントは関数のようなものですが、それらを組み合わせてコンポジションによってカスタマイズできるので、Reactコンポーネントは関数以上のものと言えるかもしれません。

以下に、コンポジションの実例をいくつか見ていきましょう。

どんな子要素が入るのか決まっていないコンポーネント

Reactでは、ウェブページ内のサイドバーやメッセージボックスなどの部品を、コンポーネントとして作っていきます。この際、どんな子要素が入るのか決まっていないコンポーネントを作りたくなることがあるかもしれません。

例えば、以下のような青色の枠のなかに何らかのメッセージを表示するメッセージボックスを作成する場合を考えてみましょう。

このコンポーネントは、何らかのメッセージの周りを青い枠で囲うものです。この枠を作りだすコンポーネントは、枠のなかにどんな子要素が入るのかを知りません。

このような場合には、children という特別な機能を持つ props を使うことができます。childrenは 子要素を渡すための専用の props です。以下のコードで、実例を見てみましょう。

// 青色の枠で囲うコンポーネント
// 枠のなかに配置する子要素は {props.children} で受けとる
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

// メッセージを表示するコンポーネント
function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        ようこそ!
      </h1>
      <p className="Dialog-message">
        ご訪問ありがとうございます。
      </p>
    </FancyBorder>
  );
}

// レンダリング
ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('root')
);

上記のサンプルソースをHTMLファイルにしたものが以下です。

https://programming-world.net/sample/react_doc/composition1-1.html

上記コードのFancyBorderコンポーネントは<div>~</div>で枠を作り出しますが、そのなかに何が入るのかは別のコンポーネントが決めることなので、 FancyBorderコンポーネント 自身は関与しません。

この場合、 FancyBorderコンポーネントでは、children という特別な機能を持つ props を子要素として配置します。 <div> {props.children} </div> という具合です。

子要素となるメッセージの内容は、WelcomeDialogコンポーネントが作成しています。 WelcomeDialogコンポーネントでは、<h1>~</h1> や <p>~</p> によって枠のなかにはいるメッセージ内容を用意します。

WelcomeDialogコンポーネントが呼び出されると、<h1>~</h1> や <p>~</p> によって作成された要素が、FancyBorderコンポーネントに children という props として渡されます。

上記の例について、別の可能性を検討

上記の例について、別のコンポジションの可能性を検討してみましょう。以下のコードでは、WelcomeDialogコンポーネントをさらに分割しています。

// 青色の枠で囲う
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

//ダイアログの基本形をつくる(汎用的なコンポーネント)
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="ようこそ!"
      message="ご訪問ありがとうございます。" />
  );
}

//レンダリング
ReactDOM.render(
  <WelcomeDialog />,
  document.getElementById('root')
);

上記のサンプルソースをHTMLファイルにしたものが以下です。

https://programming-world.net/sample/react_doc/composition1-2.html

上記コードは、上の例と同じ機能を実現していますが、 Dialogコンポーネントでダイアログの基本形を作成して、ウェルカム形式に特化したダイアログをWelcomeDialogコンポーネントで指定する構成になっています。

どちらが正解ということではなく、アプリの目的や運用面を考慮して、各コンポーネントの再利用性がより高くなるコンポジションにするのが良いということです。

複数個所に子要素を渡す場合

ここまでに登場したコード例では、枠のなかに配置する一箇所分の子要素を渡していますが、複数個所に子要素を渡したい場合があるかもしれません。その場合、childrenを使用するのではなく、独自のpropsを作成することで実現できます。

以下のコードは、画面を左右に2分割するレイアウトを作成する実例です。leftとrightという独自のpropsを作成して、2か所に子要素を渡しています。

// 左側の水色の領域
function Contacts() {
  return <div className="Contacts" />;
}

// 右側のピンク色の領域
function Chat() {
  return <div className="Chat" />;
}

// 左右に縦割りレイアウトを作るコンポーネント
function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

// leftとrightという独自のpropsを使用して
// leftにはContacts、rightにはChatを指定しながら
// SplitPaneコンポーネントを呼び出す
function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

// レンダリング
ReactDOM.render(
  <App />,
  document.getElementById('root')
);

上記のサンプルソースをHTMLファイルにしたものが以下です。

https://programming-world.net/sample/react_doc/composition1-3.html

上記のサンプルでは、左側の水色の領域と右側のピンク色の領域に、画面を左右2分割してレイアウトを作成しています。

この際、leftとrightという独自のpropsを作成して、2か所に子要素を渡しています。leftというpropsによって左側に入る子要素を渡し、rightというpropsによって右側に入る子要素を渡しています。

継承するのではなく、コンポーネントを分割してpropsで渡す

Reactでは、継承よりもコンポジションが推奨されています。コンポーネントを継承して階層構造にするのではなく、コンポーネントを分割してデータを受け渡して機能実現する方がおすすめということです。

データの受け渡しには、props を使います。Reactでは値・要素・関数など、どのようなpropsでも柔軟に受け渡しできます。props とコンポジションにより、コンポーネントを柔軟にカスタマイズできるので、わざわざコンポーネントを継承して複雑な階層構造にする必要はありません。

コンポーネントを適切に分割して構成するコンポジションによって、コンポーネントの再利用性が高まります。複数のエンジニアが参加する開発ではコンポーネントの再利用性は特に重要となるでしょう。

次回へ続きます。