Reactの基礎 (PropsとState)

Reactの基礎 (PropsとState)

前回 Create React App を使ってReactアプリの雛形を作成し、中身をすこし編集してみるところまで進めました。今回は PropsState などのReactの基礎的なことについて紹介をしていきます。

目次

Props

一番最初のチャプターで紹介したとおり、Reactではマークアップ(HTML)、スタイル (CSS)、ロジック (JavaScript) を1セットにして、 コンポーネント いう単位でUIを定義・作成していきます。コンポーネントという形でUIを定義していくことで、メンテナンスしやすくするとともに、アプリケーション内部で安全にUI部品として使い回しやすくなります。

例えば、下記のような MyBtn というシンプルなボタンコンポーネントがあります。HTMLのbutton要素をレンダリングして、クリックされた時に自分の色をアラートダイアログで知らせるコンポーネントです。

この MyBtnコンポーネントの色だけを変えて、他の場所で使いまわしたい場合にはどうすれば良いでしょうか? MyRedColorBtnやMyBlueColorBtnなど、それぞれの色のボタンを定義することもできますが、特別な理由なく、そういったコンポーネントをコピペしつつ作成していくのは不合理に思えます。

import React from "react";
import ReactDOM from "react-dom";

/* クリックすると自分の色を表示するシンプルなボタンをReactコンポーネントとして定義 */
const MyBtn = () => (
  <button
    style={{ backgroundColor: "#C5E1A5" }}
    onClick={() => {
      alert("#C5E1A5");
    }}
  >
    #C5E1A5
  </button>
);

const App = () => (
  <div>
    <MyBtn />
  </div>
);

/* idがrootであるHTML要素にAppコンポーネントをマウント */
ReactDOM.render(<App />, document.getElementById("root"));

Reactでは親コンポーネント (ここではAppコンポーネント) から Props というパラメーターを子コンポーネント (ここではMyBtnコンポーネント) に渡すことで手軽に子コンポーネントの見た目や振る舞いに変化を加えることができます。

先程定義したコンポーネントに対して Props を親コンポーネントから渡す ことにより、色々な色のボタンに対応できるようにしてみましょう。

import React from "react";
import ReactDOM from "react-dom";

/* 親コンポーネントから渡されたPropsを元に自分の色を変える */
const MyBtn = ({ color }) => (
  <button
    style={{ backgroundColor: color }}
    onClick={() => {
      alert(color);
    }}
  >
    {color}
  </button>
);

const App = () => (
  <div>
    <MyBtn color={"#F57C00"} />
    <MyBtn color={"#CDDC39"} />
    <MyBtn color={"#039BE5"} />
    <MyBtn color={"#F06292"} />
    <MyBtn color={"#90A4AE"} />
  </div>
);

/* idがrootであるHTML要素にAppコンポーネントをマウント */
ReactDOM.render(<App />, document.getElementById("root"));

上の例では colorと名付けたPropsMyBtn コンポーネントにわたしてあげることで、 MyBtn コンポーネントの見た目や振る舞いを変更しています。このようにそれぞれに異なるPropsを渡してあげることで、一つのコンポーネントに様々な振る舞いと異なる見た目をもたせつつ、アプリ中のあらゆる箇所でコンポーネントを安全かつ効率的に使い回すことができるようになります。

参照: Propsについての公式ドキュメント

State

コンポーネントの振る舞いを変更しつつアプリ内で使い回すには上記のように Props を親から渡してやる方法で可能となります。では例えば、チェックボックスのON・OFFの状態を基準にして、ボタンの色を変えたい場合はどうでしょうか?

Reactチェックボックス

こういった 状態 を管理するためにReactでは State という仕組みが提供されています。State を利用することで、コンポーネントに状態を持たせることができ、例えば「データをロードしている時には要素を表示しない」、「チェックボックスにチェックが入った時にダイアログを表示」などといった 状態 を持つことで表現できる振る舞いを実装できるようになります。

チェックボックスのON・OFFの状態をStateとして持たせ、そのStateを元にチェックボックスのON ・OFFの状態とボタンの色の振る舞いを制御してみましょう。


import React from "react";
import ReactDOM from "react-dom";

/* 
isCheckedがtrueのとき、チェックされる。
チェックが切り替わる時に親から渡されてきた onChange (中身は関数) を実行する。
=> isCheckedのtrueとfalseを反転させる。
*/
const MyCheckbox = ({ isChecked, onChange }) => (
  <label>
    MyCheckbox
    <input type="checkbox" checked={isChecked} onChange={onChange} />
  </label>
);

/* 親コンポーネントから渡されたisCHeckedを基準にして自身の色とテキストを更新する。 */
const MyBtn = ({ isChecked }) => (
  <button
    style={{ backgroundColor: isChecked ? "#CE93D8" : "#fff" }}
    onClick={() => {
      alert(isChecked);
    }}
  >
    {isChecked ? "チェックされています" : "チェックされていません"}
  </button>
);

/* isCheckedというStateを持つAppコンポーネント */
class App extends React.Component {
  constructor(props) {
    super(props);
    /* 自身のstateを初期化する。*/
    this.state = {
      isChecked: false
    };
  }

  render() {
    return (
      <div>
        <div style={{ margin: "0 0 30px" }}>
          <MyCheckbox
            isChecked={this.state.isChecked}
            /* 自身のStateを変更させる関数を渡す。(実行された時this.state.isCheckedの値がtrue => false, false => trueに切り替わる。)  */
            onChange={() => {
              this.setState({ isChecked: !this.state.isChecked });
            }}
          />
        </div>
        <div>
          <MyBtn isChecked={this.state.isChecked} />
        </div>
      </div>
    );
  }
}

/* idがrootであるHTML要素にAppコンポーネントをマウント */
ReactDOM.render(<App />, document.getElementById("root"));

上記のように執筆時点でのReactのバージョン (React v. 16.7) では State を管理するためには 基本的にはClass構文 を使う必要があります。将来的なバージョンのReactではClassを使わずに State を定義できるようになる機能 (Hooks) が提案されています (React v. 17)。

※ Hooks APIのuseState()が使える場面ではクラス構文よりも、useState()を積極的に使用していきましょう。

useState (Hooks API)

Propsのバケツリレーを避ける

大規模なアプリケーションを作っていると親コンポーネント => 子コンポーネント => 孫コンポーネント => 曾孫コンポーネント というようにPropsがかなり深い階層まで渡される構造ができあがってしまうことがあります。俗に「バケツリレー」と呼ばれます。

React Propsバケツリレー図

若干極端な例ですが、下記の例を考えてみましょう。

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

/* isLoggedInをPropsとして要求する */
const LoginBtn = ({ isLoggedIn }) => (
  <div>
    {isLoggedIn ? (
      <span>ログアウトしますか?</span>
    ) : (
      <span>ログインしますか?</span>
    )}
    {isLoggedIn ? <button>ログアウト</button> : <button>ログイン</button>}
  </div>
);

const DialogNotice = props => (
  <div
    style={{
      backgroundColor: "#BDBDBD",
      width: "90%",
      margin: "0 auto"
    }}
  >
    <LoginBtn {...props} />
  </div>
);

const MyDialog = props => (
  <div
    style={{
      backgroundColor: "#C5CAE9",
      width: "300px",
      height: "150px",
      padding: "20px"
    }}
  >
    <DialogNotice {...props} />
  </div>
);

const Background = props => (
  <div
    style={{
      backgroundColor: "#90A4AE",
      width: "100%",
      height: "100%"
    }}
  >
    <MyDialog {...props} />
  </div>
);

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLoggedIn: true
    };
  }

  render() {
    return (
      <div>
        <p>
          {this.state.isLoggedIn
            ? "ログインしています"
            : "ログインしていません"}
        </p>
        <Background isLoggedIn={this.state.isLoggedIn} />;
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

最下層のLoginBtnコンポーネントがログインしているかどうかのデータを要求するため、Appコンポーネントから、isLoggedInがPropsとして何回層もバケツリレーされており、非常に煩雑です。また、子コンポーネントが何をPropsとして受け取るか、親から何が渡されてくるかなが規定されてしまうことによって、中間にあるコンポーネントの使い回しが難しくなってしてしまう場合があるなど、柔軟な設計を阻害してしまうケースもあります。

このようなバケツリレーを避け、大規模アプリでもStateを管理しやすくする目的のためにReduxのようなライブラリが人気です。本チュートリアルでは Redux に深く立ち入ることはしませんが、 Redux を解説している書籍やWebサイトが幾つかあるので、興味があれば参照してみてください。

React + Redux入門 - ReactはできるけどReduxがわからないやってみたい人のためのreact-redux入門

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)

Context API

React自体にはこうしたバケツリレーを回避する仕組みとしてContext APIというものが提供されています。この Context APIを用いると、親から子へとという Props 伝播の流れを断ち切り、コンポーネントの親子関係を無視して、直接、値を渡すことができるようになります。Context APIはReact v16.3 で刷新・導入されたAPIです。

Context API には大きく Provider, Consumer という2つの登場人物が存在します。Context APIを使う時の流れは下記のようになります。

  • 1: まずはContextを作成して
  • 2: Providerのvalueにコンポーネント間で共有したい値をセットします。
  • 3: Consumerでvalueにセットされた値を購読する

先程のコードを Context API を使って、少し修正してみましょう。

import React from "react";
import ReactDOM from "react-dom";

/* LoginContextというContextを作成する */
const LoginContext = React.createContext();

const LoginBtn = ({ isLoggedIn }) => (
  <div>
    {/* LoginContext.Consumerで囲った要素の中にはProvider (Appコンポーネント参照) で渡した値がそのまま流れてくる。 */}
    <LoginContext.Consumer>
      {val => (
        <div>
          {val.isLoggedIn ? (
            <span>ログアウトしますか?</span>
          ) : (
            <span>ログインしますか?</span>
          )}
          {val.isLoggedIn ? (
            <button onClick={val.toggleIsLoggedIn}>ログアウト</button>
          ) : (
            <button onClick={val.toggleIsLoggedIn}>ログイン</button>
          )}
        </div>
      )}
    </LoginContext.Consumer>
  </div>
);

/* 中間要素 */
const DialogNotice = props => (
  <div
    style={{
      backgroundColor: "#BDBDBD",
      width: "90%",
      margin: "0 auto"
    }}
  >
    <LoginBtn {...props} />
  </div>
);

/* 中間要素 */
const MyDialog = props => (
  <div
    style={{
      backgroundColor: "#C5CAE9",
      width: "300px",
      height: "150px",
      padding: "20px"
    }}
  >
    <DialogNotice {...props} />
  </div>
);

/* 中間要素 */
const Background = props => (
  <div
    style={{
      backgroundColor: "#90A4AE",
      width: "100%",
      height: "100%"
    }}
  >
    <MyDialog {...props} />
  </div>
);

class App extends React.Component {
  /* ログイン中か非ログイン状態かを切り替える関数。 */
  toggleIsLoggedIn = () => {
    this.setState({
      isLoggedIn: !this.state.isLoggedIn
    });
  };

  constructor(props) {
    super(props);

    /* 自身のStateを定義する。 */
    /*
     * isLoggedIn: ログイン状態かどうか
     * toggleIsLoggedIn: ログイン中か非ログイン状態かを切り替える関数
     */
    this.state = {
      isLoggedIn: false,
      toggleIsLoggedIn: this.toggleIsLoggedIn
    };
  }

  render() {
    return (
      <div>
        {/* 
          LoginContext.Providerのvalueに自身のStateをセットする。
          valueの値が変更されるたびに、下位のコンポーネントで定義してあるLoginContext.Consumerに新しいデータが流れてくる。
         */}
        <LoginContext.Provider value={this.state}>
          {/* 子コンポーネントにはPropsとしては何も渡さない */}
          <MyDialog />
        </LoginContext.Provider>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

上記のように Provider, Consumer 経由で値をセットしてあげることで、バケツリレーをする必要がなくなりました。すべて Context APIRedux などのライブラリでデータを管理すればよいかというと、そうではなく、直接Propsを渡してあげたほうが、読みやすく、またパフォーマンスや再利用性が向上するケースがあり、Reactでは、「このデータはPropsで渡すべきか、それとも ReduxContext API などでグローバルにデータを管理するべきか…🤔」と、エンジニアが適宜データの設計を考えながら、開発を進めていきます。

アプリ内部のあらゆる箇所で参照されるグローバルな「テーマカラー (色の設定) 」、「ログイン中かどうか」、「言語設定 (日本語・English・中文)」などのデータには Context API などを適用すると良いかと思われます。

useState(Hooks API)

Hooks はReact v17で導入される予定のAPIです。今までStateを定義するのに、クラス構文を使う必要がありましたが、Hooks を使うとクラス構文を使わずに State を定義できるようになる予定です。クラスを使う必要がない場合、積極的に使用していきましょう。

参照: 公式ドキュメント

下記の例では React v16.8.0-alpha.1 を使用しています。

  "dependencies": {
    "react": "16.8.0-alpha.1",
    "react-dom": "16.8.0-alpha.1"   
  }

Hooks APIの useState() を使ってカウンターを実装するサンプルです。クラス構文を使わずとも、関数の形でStateを簡潔に表現できるようになります。

State Hooks (公式ドキュメント)

import React, { useState } from "react";
import ReactDOM from "react-dom";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        increment
      </button>

      <button
        onClick={() => {
          setCount(count - 1);
        }}
      >
        decrement
      </button>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
const [count, setCount] = useState(0);

useState() には初期状態のStateを渡します。上記では「0」を指定して、Stateを生成しており、初期値は「0」となります。 count がStateであり、 setCount がStateを更新するための関数となります。

setCount(123);