React講座 コンポーネントとProps

コンポーネントにより UI を独立した再利用できるパーツに分割し、パーツそれぞれを分離して考えることができるようになります。今回はコンポーネントという概念を理解できることを目標にしましょう。

関数コンポーネントとクラスコンポーネント

コンポーネントを定義する最もシンプルな方法は JavaScript の関数を書くことです:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

この関数は、データの入った”props”プロパティというオブジェクトを引数として受け取り、React要素を返します。これがコンポーネントになります。

プロパティは親からもらうプレゼントをイメージしてください。そのプレゼントには何かのデータが入っていることをイメージしましょう。※propsは常に親から子です。子のコンポーネントから親コンポーネントにpropsが渡ることはありませんので覚えておきましょう。

classを使ってコンポーネントを定義することもできます。

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

細かいコンポーネントの構成については次回以降に説明をするので、再度コンポーネントの概念について理解を深めていきましょう。


コンポーネントのレンダー

ここまでは、DOMのタグを使うReact要素のみを使いました。

const element = <div />;

しかし、要素はユーザー定義のコンポーネントを使用することもできます。

const element = <Welcome name="Sara" />;

React がユーザ定義のコンポーネントを見つけた場合、JSX に書かれている属性と子要素を単一のオブジェクトとしてこのコンポーネントに渡します。このオブジェクトのことを “props” と呼びます。

例えば以下のコードではページ上に “Hello, Sara” を表示します:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <Welcome name="Sara" />;
root.render(element);

この例で何が起こるのかおさらいしてみましょう。

  1. <Welcome name="Sara" /> という要素を引数として root.render() を呼び出します。
  2. React は Welcome コンポーネントを呼び出し、そのときに props として {name: 'Sara'} を渡します。
  3. Welcome コンポーネントは出力として <h1>Hello, Sara</h1> 要素を返します。
  4. React DOM は <h1>Hello, Sara</h1> に一致するよう、DOM を効率的に更新します。
補足: コンポーネント名は常に大文字で始めてください。

React は小文字で始まるコンポーネントを DOM タグとして扱います。例えば、<div /> は HTML の div タグを表しますが、<Welcome /> はコンポーネントを表しており、スコープ内に Welcome が存在する必要があります。

コンポーネントを組み合わせる

コンポーネントは自身の出力の中で他のコンポーネントを参照できます。これにより、どの詳細度のレベルにおいても、コンポーネントという単一の抽象化を利用できます。ボタン、フォーム、ダイアログ、画面:React アプリでは、これらは共通してコンポーネントとして表現されます。

例えば、Welcome を何回もレンダーする App コンポーネントを作成できます:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

典型的には、新規の React アプリは階層の一番上に単一の App コンポーネントを持っています。しかし、既存のアプリに React を統合する場合は、Button のような小さなコンポーネントからボトムアップで始め、徐々にビューの階層構造の頂上に向かって進んでいってもよいでしょう。


コンポーネントの抽出

では、コンポーネントがどのように構成されるか分かったところで、どの段階でコンポーネントにするか悩みますよね。しかし、コンポーネントをより小さなコンポーネントに分割することを恐れる必要はありません。

例えば、この Comment コンポーネントについて考えましょう:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

これは props として author(オブジェクト)、text(文字列)、および date(日付)を受け取り、ソーシャルメディアサイトにおける 1 つのコメントを表します。

これだけのネスト化されたデータがあるため、このコンポーネントの変更が簡単ではありません。また内部の個々のパーツを再利用することも困難です。ここからいくつかのコンポーネントを抽出しましょう。

まず、Avatar を抽出します:

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

Avatar は、自身が Comment の内側でレンダーされているということを知っている必要はありません。なので props の名前として、author ではなく user というもっと一般的な名前を付けました。

コンポーネントが使用される文脈ではなく、コンポーネント自身からの観点で props の名前を付けることをお勧めします。

これで Comment をほんの少しシンプルにできます:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar user={props.author} />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

次に、ユーザ名の隣の Avatar をレンダーするために使われる、UserInfo コンポーネントを抽出しましょう。

function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}

これにより Comment をさらにシンプルにできます:

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

コンポーネントの抽出は最初は面倒なタスクになりますが、再利用できるコンポーネントを別々に分けておくことは、アプリケーションが大きくなればコードを繰り返す必要が減り効率が良くなります。役に立つ経験則として、UI の一部(ButtonPanelAvatar など)が複数回使われている場合、またはその UI 自体が複雑(AppFeedStoryComment など)である場合、それらは別のコンポーネントに抽出する有力な候補であるといえます。

Props は読み取り専用

この記事の最初でも説明しましたが、コンポーネントを関数で宣言するかクラスで宣言するかに関わらず、自分自身の props は決して変更してはいけません。この sum 関数を見てください。

function sum(a, b) {
  return a + b;
}

このような関数は入力されたものを変更しようとせず、同じ入力に対し同じ結果を返すので “純粋 (pure)” であると言われます。

対照的に、以下の関数は自身への入力を変更するため純関数ではありません:

function withdraw(account, amount) {
  account.total -= amount;
}

React は柔軟ですが、1 つだけ厳格なルールがあります:

全ての React コンポーネントは、自己の props に対して純関数のように扱わなければいけません。

もちろんアプリケーションの UI は動的で、時間に応じて変化するものです。次の章では、“state” という新しい概念を紹介します。state により React コンポーネントは上述のルールを壊すことなく、時間と共にユーザのアクション、ネットワークのレスポンスや他の様々な事に反応して、出力を変更することができます。