React Context入門

React Contextはv16.3で追加された機能です。React Contextを使用すると、複数のコンポーネントで値を共有することができます。

今回は、このReact Contextの使い方を簡単に書いてみます。

どういうときに使用するか

公式に書かれているように、グローバルライクに使用したいpropsがある場合に利用されます。ただし、どこからでもアクセスが可能になるわけではなく、明示的に指定した範囲内でのみ参照が可能です。

Reduxと比較されることもあるようですが、状態を保管するストアが別にあるわけではなく、アプリケーション全体の状態管理をするには向いていません。

凡例

A、B、Cという3つのコンポーネントがあるとします。Aコンポーネントの子にBコンポーネントがあり、Bコンポーネントの子にCコンポーネントがあります。

Aコンポーネントはcountという状態を持ち、この値はCコンポーネントでも利用します。一般的にReactでは親から子に値を渡すにはpropsを使うため、Aのcount値をCでも使うにはB経由でpropsを伝播させる必要があります。

// Aコンポーネントのrender内
<B count={this.state.count} />

// Bコンポーネント内
const B = (props) => <C count={props.count} />;

// Cコンポーネント内(ステートフルの場合)
const count = this.props.count;

Bは単にCコンポーネントをレンダリングするだけで、count値は必要ありません。これは1階層下のため問題はありませんが、100階層下のコンポーネントに値を引き渡す場合は非常に面倒です。これを解決するのがコンテキストです。

コンテキストは指定したpropsを、コンポーネント間で共有させることができます。

利用方法

コンテキストの利用に必要なステップは、3つだけです。

  1. Contextを作成する
  2. Providerに受け渡したい値をセットして定義する
  3. Consumerを定義して値を受け取り処理をする

サンプルプログラム

先の凡例を実際のコードで作成してみます。まずAコンポーネントです。

import React from "react";
import B from "./B";

export const CountContext = React.createContext();

export default class A extends React.Component {
  state = {
    count: 0
  };

  render() {
    return (
      <div>
        <CountContext.Provider value={this.state}>
          <B />
        </CountContext.Provider>
      </div>
    );
  }
}

最初に、React.createContext()でCountContextというコンテキストを作成しています。そしてrender()メソッド内では、コンテキスト名.Providerという形式でProviderを定義しています。Providerは、他所からアクセスさせたい値をvalue propにセットします。

次はBコンポーネントです。ただのステートレスな中間コンポーネントです。

import React from "react";
import C from "./C";

const B = () => {
  return (
    <div>
      <C />
    </div>
  );
};

export default B;

最後にCコンポーネントです。

import React from "react";
import { CountContext } from "./A";

const C = props => (
  <CountContext.Consumer>
    {context => {
      return (
        <div>
          <h2>{context.count}</h2>
        </div>
      );
    }}
  </CountContext.Consumer>
);

export default C;

<コンテキスト名.Consumer>でConsumerを配置します。ラップされている内部の関数のcontext引数には、コンテキストの値が格納されています。ここではAコンポーネントのstate値です。あとはcontext.countでcount値を出力しているだけです。propsを経由しなくても親コンポーネントの値にアクセスできました!

Sample Code

メソッド呼び出す

Consumer内から参照している値を更新することも可能です。Reduxのようにアクションをディスパッチする必要はなく、単にメソッドを呼び出すだけです。

ここでは先ほどの例を使い、シンプルなカウンターを作成してみましょう。

まず、Aコンポーネントにcount値を増減する2つのメソッドを定義します。

onIncrement = (e) => {
  this.setState((prevState) => ({
      count: prevState.count + 1
  }));
}

onDecrement = (e) => {
  this.setState((prevState) => ({
      count: prevState.count - 1
}));

そしてstate内に、上記2つのメソッドをコールする関数を定義します。メソッドは2つともactionsというプロパティ名を持つオブジェクト内に定義します。

state = {
  count: 0,
  actions: {
    onIncrement: () => this.onIncrement(),
    onDecrement: () => this.onDecrement()
  }
};

最後にConsumer内に、このメソッドを呼び出すボタンを作成します。

<button onClick={context.actions.onIncrement}>+</button>
<button onClick={context.actions.onDecrement}>-</button>

仕組みは値の場合とまったく同じです。Providerのvalue propにAコンポーネントの状態をセットしているため、context.actions経由でメソッドを呼び出すことができるのです。

勿論、Aコンポーネント内から自身のメソッドを呼び出しても問題ありません。

// Aコンポーネントのrender()
render() {
  return (
    <div>
      <CountContext.Provider value={this.state}>
        <B />
      </CountContext.Provider>
      <button onClick={this.onIncrement}>add</button>
      <button onClick={this.onDecrement}>dec</button>
    </div>
  );
}

state.counter値の整合性を気にする必要はありません。参照先は同じです。