React Context API について復習

公開日:

目次

React Context API について結構知識があやふやだなぁと思ったのでここらで一度まとめておこうと思います。React Context API について公式ドキュメントを見てみると以下のように説明されています。

コンテクストは各階層で手動でプロパティを下に渡すことなく、コンポーネントツリー内でデータを渡す方法を提供します。

つまり、React Context API とは props を通じてステートを伝達しなくても、コンポーネント間でステートを共有する方法です。React Context API を使用することで、ステート管理を1箇所で行うことができるようになります。

React Context API を使ってみる

まずは React Context API を使ってみましょう。ここでは、簡単なカウントアップ・ダウンアプリケーションを例にしてみましょう。

// index.js

import { StrictMode, createContext } from "react";
import ReactDOM from "react-dom";

import App from "./App";

export const CounterContext = createContext();

const count = 0;

ReactDOM.render(
  <StrictMode>
    <CounterContext.Provider value={count}>
      <App />
    </CounterContext.Provider>
  </StrictMode>,
  document.getElementById("root")
);

上記のソースコードでは、createContext を用いて CounterContext というコンテキストオブジェクトを作成しています。コンテキストオブジェクトである CounterContextCounterContext.ProviderCounterContext.Consumer という2つのコンポーネントを提供します。

propsとして共有したいデータ(今回の場合は count)を渡した、 CounterContext.Provider コンポーネントでルートの App コンポーネントを囲んでいます。それにより、App コンポーネント配下のすべてのコンポーネントで count のデータを参照することができます。

count のデータを参照したいコンポーネントでは、エクスポートした CounterContext をインポートし、CounterContext.Consumer コンポーネント内で レンダープロップ という方法を用いて count のデータにアクセスすることができます。以下に CounterContext.Consumer コンポーネントを使用して、 count のデータにアクセスする例を示します。

// App.js

import { CounterContext } from "./index";

export default function App() {
  return (
    <CounterContext.Consumer>
      {count => {
        return (
          <div className="App">
            <h1>Counter</h1>
            <p>現在のカウント: {count}</p>
          </div>
        );
      }}
    </CounterContext.Consumer>
  );
}

useContext フックからステートを参照する

先程の ``CounterContext.Consumer` コンポーネントを利用してステートを参照する方法は、React Hooks が登場する前の方法です。React Hooks の登場により、useContext フックが使用できるようになり、より簡潔に コンテキストオブジェクト内のステートを参照することができるようになったので、useContext フックを使った方法も解説します。

useContext フック を使用したソースコードの例は下記になります。レンダープロップ のような複雑な記述をすることなく count のデータが参照できています。

// App.js

import { useContext } from "react";
import { CounterContext } from "./index";

export default function App() {
  const count = useContext(CounterContext);

  return (
    <div className="App">
      <h1>Counter</h1>
      <p>現在のカウント: {count}</p>
    </div>
  );
}

useContext × useState

React Context API を活用することで props のバケツリレーをすることなくコンポーネント間でステートを共有することができますが、データを変更することはできません。コンテキストオブジェクト内で保持するステートの値を変更するためには、Provider コンポーネントを独自のコンポーネントでラップする必要があります。ラップしたコンポーネントでステートの値と、ステートを変更するための関数を定義することで、ラップしたコンポーネントのステートが変更された場合、そのコンポーネントが再描画され、結果的に Provider 配下のコンポーネントが更新されたデータで再描画されます。

このようにステートを保持するコンポーネントが Provider コンポーネントを描画する場合、ステートを保持するコンポーネントのことを カスタムプロバイダー と呼ぶようです。今回は カスタムプロバイダー として CountProvider というコンポーネントを定義していきたいと思います。

// CounterProvider.js

import React, { useState, createContext } from "react";

const CounterContext = createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  return (
    <CounterContext.Provider value={{ count, setCount }}>
      {children}
    </CounterContext.Provider>
  );
};

export { CounterProvider, CounterContext };

上記のソースコードのように、 useState フックを使用することで、コンポーネント内でステートとステートを更新する関数を保持することができるようになります。CounterProvider.js で定義した count と setCount のステートをどのように子コンポーネントで使用するかというと、今回も useContext フックを使用します。

// App.js

import { useContext } from "react";
import { CounterContext } from "./CounterProvider";

export default function App() {
  const { count, setCount } = useContext(CounterContext);

  return (
    <div className="App">
      <h1>Counter</h1>
      <p>現在のカウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>カウントアップ</button>
    </div>
  );
}

これでカウントアップができるようになりました。しかし、ここで1つ問題があります。 setCount という直接ステートの値を変更できる関数を公開することです。カウントアップ・ダウンのような単純なアプリケーションであれば問題ないのですが、これが大規模なアプリケーションになってくると、何でもできるというのはバグを生み出す可能性があります。対策として、必要な操作のみができる関数を公開することでバグの混入可能性を最低限に抑えることができます。ここでは、カウントアップする addCount 関数とカウントダウンする `minusCount` 関数を公開するように変更します。

// CounterProvider.js

import React, { useState, createContext } from "react";

const CounterContext = createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const addCount = (num) => setCount(count + num);
  const minusCount = (num) => setCount(count - num);

  return (
    <CounterContext.Provider value={{ count, addCount, minusCount }}>
      {children}
    </CounterContext.Provider>
  );
};

export { CounterProvider, CounterContext };

作成した addCount 関数と minusCount` 関数を App.js で useContext` フックを使用して読み込み、ボタンのクリックイベントに登録します。

// App.js

import { useContext } from "react";
import { CounterContext } from "./CounterProvider";

export default function App() {
  const { count, addCount, minusCount } = useContext(CounterContext);

  return (
    <div className="App">
      <h1>Counter</h1>
      <p>現在のカウント: {count}</p>
      <button onClick={() => addCount(1)}>カウントアップ</button>
      <button onClick={() => minusCount(1)}>カウントダウン</button>
    </div>
  );
}

これで、setCount 関数を直接公開せず、コンテキストオブジェクト内のステートの値を変更することができるようになりました?

React Context API とカスタムフックを組み合わせる

今までの説明で基本的な React Context API の使用方法の説明は終わりです。ただ最後に、カスタムフックと React Context API を組み合わせることで、更に簡潔にステートの値を参照できるようになります。CounterProvider.js に以下のように編集します。

// CounterProvider.js
// ...省略
const useCount = () => useContext(CounterContext);

export { CounterProvider, useCount };

上記のソースコードでは、コンテキストオブジェクトの操作を useCount というカスタムフックで隠蔽することができます。カスタムフックにすることでコンテキストオブジェクトの値を別のコンポーネントで使用したい場合でも、CounterContext をインポートする必要がなくなり、ソースコードが簡潔になります。例えば、App.js を以下のように書き換えることができます。

// App.js

export default function App() {
  const { count, addCount, minusCount } = useCount(); // 簡潔に記述できる

  return (
    <div className="App">
      <h1>Counter</h1>
      <p>現在のカウント: {count}</p>
      <button onClick={() => addCount(1)}>カウントアップ</button>
      <button onClick={() => minusCount(1)}>カウントダウン</button>
    </div>
  );
}

まとめ

今回は、React Context API の使い方についてまとめてみました。Reactの状態管理では Redux を採用することも多いかと思いますが、個人で開発するときなど小規模なアプリでは React Context API が向いているのではないかなぁと感じました。