React公式ドキュメントを読む11 コンポーネントにstateを導入する

このページでは、stateという概念が登場します。propsとstateは似ていますが、異なる概念です。propsはコンポーネント間でデータの受け渡しをする際に使用されますが、stateはそのコンポーネントのなかだけで保有される状態です。

Reactコンポーネントでstateを利用するとは具体的にどういうことなのか、stateを利用することで何ができるようになるのでしょうか。1秒ごとに現在時刻が更新される「時刻表示プログラム」の実例を通して、stateへの理解を深めます。

時刻表示プログラムを再利用しやすいコンポーネントにするには?(前編)

まずは、 1秒ごとに現在時刻が更新される「時刻表示プログラム」について考えるところから始めましょう。以下のサンプルコードは、1秒ごとに現在時刻が更新される「時刻表示プログラム」です。

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

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

https://programming-world.net/sample/react_state1-1.html

上記サンプルでは、タイマーで1秒ごとにtick()関数が呼び出されて毎秒ReactDOM.render()が実行されることで、現在時刻を毎秒更新しています。

このプログラムでは、呼び出す際にタイマーが必要になるなど、時計コンポーネントとして自分自身だけで完結していません。そのため、毎回タイマーをセットしながら呼び出さねばならず、再利用しづらいものになってしまっています。

上記のサンプルコードを、タイマーも含めて時刻表示に関する機能をすべて内包させて、自分自身だけで完結する時計コンポーネントに段階的に変更していきます。

1. 時刻表示部分をClockコンポーネントとして切り出す

まず、1秒ごとに呼び出されるtick()関数から、時刻を画面表示する要素を生成する部分をClockコンポーネントとして切り出してみましょう。

//時刻表示部分を切り出す
function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

//Clockコンポーネントを描画する関数となった
function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

//まだ、呼び出し側にタイマーがある
setInterval(tick, 1000);

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

https://programming-world.net/sample/react_state1-2.html

上記のサンプルでは、時刻表示部分を切り出しました。tick()はClockコンポーネントを描画する関数となり、Clockコンポーネントはpropsとして受け取ったdateの値を表示する要素を返すものとなりました。

ただし、Clockコンポーネントはこの時点ではまだタイマーを内包しておらず、完全にカプセル化されたコンポーネントにはなっていません。目指すのは、ただClockコンポーネントを呼び出すだけで、現在時刻が1秒ごとに更新表示されるものです。

タイマーを内包するClockコンポーネントが完成したら、呼び出し側は以下のようなコードになるはずです。

//Clockコンポーネントを呼び出す側にタイマーはありません
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

2. 関数コンポーネントをクラスコンポーネントに変更する

次に、Clockコンポーネントを、これまでの関数コンポーネントからクラスコンポーネントに変換します。

//React.Componentを継承してClockクラスを作成
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

//Clockコンポーネントを描画する関数
function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

//まだ、呼び出し側にタイマーがある
setInterval(tick, 1000);

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

https://programming-world.net/sample/react_state1-3.html

上記のサンプルでは、これまでの関数コンポーネントからクラスコンポーネントに変換しました。なぜ、関数からクラスに変換したのかと言えば、そうすることで 「ローカル state」の機能が使えるようになるからです。Clockコンポーネントをカプセル化するには、stateの機能が必要なのです。

上記のサンプルでは、React.Component を継承してClockクラスを作成しています。React.Component を継承するクラスにしたことで、親クラスとなる React.Component が用意してくれる「ライフサイクルメソッド」機能も利用できるようになります。

3. Clockコンポーネントにローカルstateを追加する

クラスに変換したClockコンポーネントに、ローカルstateを追加します。ローカルstateは、そのコンポーネントのなかだけで管理されるプライベートなものです。propsと state は似ていますが、propsが複数のコンポーネントをまたいで受け渡すことができるのに対して、stateはそのコンポーネントのなかだけで利用します。

Clockコンポーネントにstateを追加するには、いくつかの点を変更する必要があります。その手順は以下の通りです。

  1. render() メソッド内の this.props.date を this.state.date に書き換える
  2. this.state の初期状態を設定するクラスコンストラクタを追加する
  3. <Clock />要素から date プロパティを削除する

上記サンプルソースにstateを追加して、変更した結果は以下の通りです。

//React.Componentを継承してClockクラスを作成
class Clock extends React.Component {
  //this.state の初期状態を設定するクラスコンストラクタを追加する
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  //propsではなくstateを利用するように書き換える
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

//<Clock />要素からdateプロパティを削除する
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

//タイマーのコードは、後からClockコンポーネント内に追加する予定

クラスコンストラクタとは、クラスからオブジェクトを作成した際に、自動的に実行されるメソッドのことす。上記サンプルで追加したクラスコンストラクタは、constructor(props) { ~ }の部分です。

Clockクラスのインスタンスを作った際には、constructor(props) { ~ } の部分が自動的に実行されて this.state の初期状態を設定します。初期状態が設定された this.state は、その下にあるrender()メソッドのなかで利用されます。

クラスのコンポーネントは、 props を引数として親クラスのコンストラクタを呼び出してやる必要があります。上記サンプルで言うと super(props); の部分でprops を引数として親クラスのコンストラクタが呼び出されています。

<Clock />要素から date プロパティが削除されていますが、現在日時を取得する機能は constructor() が担うようになっています。ReactDOM.render()は、 Clockコンポーネントから受け取った要素をレンダリングするだけになりました。

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

https://programming-world.net/sample/react_state1-4.html

上記サンプルには、これまであったタイマー部分がありません。そのため、上記サンプルは現在時刻は表示しますが、1秒ごとに更新されません。タイマーのコードは、後からClockコンポーネント内に追加する予定です。

次回へ続きます。