DEV Community

はむさん
はむさん

Posted on

React Context API

背景

  • Reactでは、配下のコンポーネントにデータを渡すための手段としてpropsという機能があります。ところが、propsを使用すると、孫コンポーネント、ひ孫コンポーネント、...、というように渡したいコンポーネントまで渡したいデータをバケツリレーのように延々と渡していかなければならない弱点があります。
  • その弱点を解消するべく、どのコンポーネントからでも特定のデータにアクセスできる仕組みがreact-reduxから提供されていて、Reduxを使用したことのある人なら大抵の人が恩恵を受けているであろうProviderコンポーネントです。
  • Providerコンポーネントとは文字通りProviderコンポーネントでwrapした全コンポーネントに特定のデータを届けることが目的とします。
  • その後、Reactは、バージョンv16.3で、Reduxに喧嘩を売っているのか?とまさに耳を疑うような機能を追加しました。react-reduxProviderとほぼ同様の機能で同名のProviderというコンポーネントをリリースしました。
  • この記事ではReact側でリリースされたProvierを含むContext APIについての紹介と、Context APIを使用したアプリケーションの実装例について紹介したいと思います。
  • 尚、「先にソースコードを見たい!」という方は末尾のソースコードよりgit cloneしてください。

Reactアプリケーションの作成

Reactアプリケーションが必要になりますので以下のコマンドでサクッと作成してしまいます。ちなみに、npxnpmのv5.2.0で導入されたもので、create-react-appのようなグローバルスコープにインストールしてから使用するようなコマンドが、インストールしなくても実行できるようにするためのコマンドです。グローバルスコープにあまり使わないコマンドを導入したくないという方は今回のようにnpxで実行すると良いでしょう。

$ npx create-react-app counter
$ cd counter

念のため、package.jsonの中身を確認しておきましょう。Reactのバージョンがv16.3以上でないと以降の動作確認ができませんのでね。

今回確認した時のpackage.jsonは以下の内容でした。reactのバージョンが^16.3.2 となっているので問題ないです。

{
  "name": "counter",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.3.2",
    "react-dom": "^16.3.2",
    "react-scripts": "1.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

続いてReactアプリケーションを起動します。

$ npm start

npm startを実行するとブラウザが自動起動し、Welcome to Reactのおなじみの画面が表示されるはずです。これが確認できたらエディターに戻って、ファイルを編集します。

React Context APIの導入したカウンタアプリの例

「React Context APIの導入したカウンタアプリ」の例を以下に記載します。

まずはReactアプリケーションのトップレベルのファイルであるsrc/index.jsを編集していきます。

編集前は、以下のようになっていると思いますが、一旦全部消去します。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

そして、src/index.jsを以下のように書き換えます。

import React from 'react';
import ReactDOM from 'react-dom';
import { CounterContext } from './contexts/counter'
import Counter from './components/counter'

class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      count: 0,
      increment: () => this.setState(state => ({count: state.count + 1})),
      decrement: () => this.setState(state => ({count: state.count - 1}))
    }
  }

  render() {
    return (
      <CounterContext.Provider value={this.state} >
        <Counter />
      </CounterContext.Provider>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

以下の点がポイントになります。

  • contextの作成はsrc/index.jsではやらず別ファイルに分離しています。大規模なアプリケーションを想定するとおそらく別のファイルに分離しないとカオスになると思われるためです。というのも、contextは実質無限個作成できるからです。
  • contextと同様の理由でcomponentも別ファイルに分離しました。これはReactアプリケーションを書く人なら既に身についているであろう慣習です。
  • 状態はReactの基本機能であるstateで管理します。状態遷移についてもReactのsetStateで行います。なのでReduxで書くときとは違い、stateの変更とそれを誘発するイベントがコンポーネントにべったりな点は、Reduxアプリを書き慣れている人で何でもかんでも分離したがる潔癖症な人にとってはかなりキモいコードと感じてしまうかもしれません。
  • importしたCounterContextに紐づくCounterContext.Providerというコンポーネントで渡したい状態を受け取るコンポーネントをwrapします。
  • ProviderではなくCounterContext.Providerと表記しているのは、今回のケースでは、Providerでも良かったんですがContextは無数存在し得る物なのでどのProviderなのかを識別できるようCounterContext.というプレフィックスにより名前空間を識別する習慣を付けないと後々アプリケーションが大規模になってきて複数のContextが入り乱れるようになってときにリファクタリングを強いられると思うのでそうしています。
  • ProvierでConsumerに渡したい状態をvalue=で渡しています。

Counterコンポーネント

続いてCounterコンポーネントを作成します。 

$ mkdir src/components
$ touch src/components/counter.js

エディターで以下のように編集します。

import React from 'react';

import { CounterContext } from '../contexts/counter'

const Counter = () => (
  <CounterContext.Consumer>
    {
      ({ count, increment, decrement }) =>  {
        return (
          <React.Fragment>
            <div>count: {count}</div>
            <button onClick={increment}>+1</button>
            <button onClick={decrement}>-1</button>
          </React.Fragment>
        )
      }
    }
  </CounterContext.Consumer>
)

export default Counter

以下の点がポイントになります。

  • カウンタコンポーネントはConsumerコンポーネントを有するコンポーネントです。なので冒頭で該当のContextをimportしています。
  • CounterContext.Consumerと表記しているのは、CounterContext.Providerと表記しているのと同じ理由からです。(上述)
  • Consumerの内部のchildは関数であり必須です。関数を書いてください。
  • Consumer内部の関数の引数で、Providerが渡してくれたvalueを受け取ることができます。今回のアプリケーションでは、カウンタの値、インクリメントの関数、デクリメントの関数を受け取ることができます。
  • Consumer側では受け取った値を適当な場所に表示させたり、受け取った処理を適当なイベントハンドラに渡したりすれば良いです。

CounterContextの作成

最後にカウンタ専用のCounterContextを作成します。

$ mkdir src/contexts
$ touch src/contexts/counter.js

src/contexts/counter.jsを以下のように編集します。

import { createContext } from 'react';

export const CounterContext = createContext({
  count: null,
  increment: () => {},
  decrement: () => {}
})

コンテキストは即ちProviderとConsumer間で共有したい状態や処理です。createContextの引数には上記のようにオブジェクトをデフォルト値として渡すことができます。countを0にしても良いのですが初期化は今回の実装ではコンポーネントで実行させるポリシーにしています。

デモ画面

という感じで、以下の画面ようにカウンタアプリがブラウザに表示され動作すると思います。

デモ画面

ソースコード

本記事で扱った動作確認のとれているソースコードはGitHubに公開しています。書くのが面倒という方は下記コマンドでgit cloneしてください。

$ git clone git@github.com:ProgrammingSamurai/react-recipes.git

告知

先日、Udemy講師デビューを果たしました。「フロントエンドエンジニアのためのReact・Reduxアプリケーション開発入門」というコースを公開中です。これからReactをやってみようとお考えの方は是非始めてみてください!こちらのリンクから95%オフで購入できます。ご質問も大歓迎です! :)

Latest comments (0)