ReactのuseContextはコンポーネント間でデータ(状態)の共有とデータ(状態)の受け渡しに関するHookです。useStateやuseReducerと組み合わせて利用することができます。まず本書では最も基本的なuseContextの使用方法について説明を行いその後にuseStateとuseReducerを使った利用方法を別々に説明します。その後コンポーネントの再描写についても動作確認を行い、本文書を読み終えるとuseContextがどのようなものかを理解することができます。

React 18で動作確認を行なっています。

useContextとは

通常親コンポーネントから子コンポーネントにデータを渡す際はpropsを介して行います。しかし親から子、そのまた子といったように複数のコンポーネントを介してデータを渡す場合にprops(prop-drilling)では設定が複雑になってきます。

ReactのContext APIを利用することでpropsを利用することなく異なる階層のコンポーネントとデータの共有を行うことができます。propsに比べて覚えなければいけないことがいくつかあるので慣れるまではすべて暗記しようとせず実際に利用しないといけない時に本文書を読み直して復習してください。

最も簡単な使用例

App.jsファイルを一番親のコンポーネントとして、App.jsファイルに子コンポーネントAをimportし、コンポーネントAにコンポーネントB、コンポーネントBにコンポーネントCをimportした4階層のコンポーネントを作成します。

4階層のコンポーネント
4階層のコンポーネント

一番上の親コンポーネントであるApp.jsのファイルの中身は下記のようになります。ComponentA、B、Cについてはcomponentsディレクトリを作成してその下に作成していきます。


import ComponentA from './components/ComponentA'

function App() {
  return (
    <div style={{ textAlign: 'center' }}>
      <h1>Learn useContext</h1>
      <ComponentA/>
    </div>
  );
}

export default App;

ComponentA


import ComponentB from './ComponentB'

const ComponentA = () => {
    return (
        <div>
            <p>Componet A</p>
            <ComponentB />
        </div>
    )
}

export default ComponentA

ComponentB


import ComponentC from './ComponentC'

const ComponentB = () => {
    return (
        <div>
            <p>Componet B</p>
            <ComponentC />
        </div>
    )
}

export default ComponentB

ComponentC


const ComponentC = () => {
    return (
        <div>
            <p>Componet C</p>
        </div>
    )
}

export default ComponentC

ブラウザで見ると下記のように表示されます。

4階層のコンポーネントを表示
4階層のコンポーネントを表示

準備ができたのでApp.jsから数字100をContext APIを利用してComponetCに渡します。

親コンポーネントでの設定

まずApp.jsでContextの作成を行います。Contextオブジェクトの作成はcreateContextで行います。


export const UserCount = createContext()

作成したUserCountをexportしているのはContextを利用するComponentCを含めた他のコンポーネントでimportを行なって利用するためです。

次はUserCount.Providerコンポーネントで数字を渡したいコンポーネントが入っているComponentAを囲みます。UserCount.Providerのタグの中のvalueに100を設定します。UserCount.Providerというタグでわざわざ囲まないといけないという処理が必要ですがこれだけで親コンポーネントでの設定は完了です。ここまでの設定ではブラウザ上には何の変化もありません。


import { createContext } from 'react';
import ComponentA from './components/ComponentA'

export const UserCount = createContext()

function App() {
  return (
    <div style={{ textAlign: 'center' }}>
      <h1>Learn useContext</h1>
      <UserCount.Provider value={100}>
        <ComponentA/>
      </UserCount.Provider>
    </div>
  );
}

export default App;

値を受け取るコンポーネントでの設定

今回はComponentCで値を受け取るのでComponenCのみ設定を行います。ComponentBなど他のコンポーネントでも値を受け取りたい場合の設定方法は同じです。

ComponentCでuseContextを利用するためimportします。また、App.jsでexportしたUserCountはここでimportする必要があります。


import {useContext} from 'react'
import { UserCount } from '../App'

useContextとUserCountを使ってvalueで設定した値を取り出し変数countに入れます。


const count = useContext(UserCount)

これでcountをコンポーネントCで利用することができます。


import { useContext} from 'react'
import { UserCount } from '../App'

const ComponentC = () => {
    const count = useContext(UserCount)
    return (
        <div>
            <p>Componet C</p>
            <p>{count}</p>
        </div>
    )
}

export default ComponentC

ブウウザで確認し、Componet Cの文字列の下に100が表示されれば、App.jsで設定した値がComponet Cに渡されたことになります。

コンポーネントCの100が表示
コンポーネントCの100が表示

これがContext, useContextを使った最もシンプルな例です。App.jsで設定した値をpropsのようにComponentA, Bを介することなくComponentCで受け取ることができました。

useStateと一緒に利用する

先程は100という値だけuseContextを利用してComponentCに渡しましたが今回はuseStateと一緒に利用します。

App.jsでuseStateをimportとして、useStateで設定したcountとsetCountをvalueに設定します。countの初期値は100に設定しています。


import { createContext, useState } from 'react';
import './App.css';
import ComponentA from './components/ComponentA';

export const UserCount = createContext();

function App() {
  const [count, setCount] = useState(100);
  const value = {
    count,
    setCount,
  };
  return (
    <div className="App">
      <h1>Learn useContext</h1>
      <UserCount.Provider value={value}>
        <ComponentA />
      </UserCount.Provider>
    </div>
  );
}

export default App;

ComponentCで先程と同様の方法で受け取ります。


import {useContext} from 'react'
import { UserCount } from '../App'

const ComponentC = () => {

    const { count, setCount } = useContext(UserCount);

    return (
        <div>
            <p>Componet C</p>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>+</button>
        </div>
    )
}

export default ComponentC

受け取ったsetCountを利用してonClickイベントを利用してcountの値を増やします。

useStateで渡したcountをsetCountで増やす
useStateで渡したcountをsetCountで増やす

ボタンをクリックするとcount数字が1ずつ増えていくことが確認できます。ただの値だけではなくuseStateの値と関数もuseContextを利用してコンポーネント間で共有できることがわかりました。簡単ですね。

useReducerと一緒に利用する

useReducerはuseStateと同様に状態の管理を行うことができるのでuseContextと一緒に利用することも可能です。useStateでもuseReducerでも渡したい値をProviderのvalueに設定するので違いはなく簡単です。

useReducerを使いなれていない人によってはuseStateほど設定方法がシンプルではないので先にuseReducerの設定方法を理解しておく必要があります。

useStateの時はcountとsetCountをvalueで渡していましが、useReducerの場合はstateとdispatchを渡します。countとstateは現在の状態を保持し、setCountとdispatchはどちらも値の更新を行う関数です。actionにINCREMENTを設定しているのでactionがINCREMENTの場合はcountの値を1増やし、それ以外のACTIONの場合はcountの値を1減らします。


import React, { useReducer } from 'react';
import ComponentA from './components/ComponentA';

export const UserCount = React.createContext();

const initialState = {
  count: 100,
};

function App() {
  const reducer = (state, action) => {
    if (action === 'INCREMENT') {
      return { count: state.count + 1 };
    } else {
      return { count: state.count - 1 };
    }
  };

  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div style={{ textAlign: 'center' }}>
      <UserCount.Provider value={{ state, dispatch }}>
        <h1>Learn useContext</h1>
        <ComponentA />
      </UserCount.Provider>
    </div>
  );
}

export default App;

ComponentCではcountとsetCountの代わりにstateとdispatchを利用します。dispatchを利用するためにはACTIONを引数に設定する必要があります。先程設定したINCREMENTを設定しています。


import React, { useContext } from 'react';
import { UserCount } from '../App';

const ComponentC = () => {
  const { state, dispatch } = useContext(UserCount);
  return (
    <div>
      <p>Componet C</p>
      <p>{state.count}</p>
      <button onClick={() => dispatch('INCREMENT')}>+</button>
    </div>
  );
};

export default ComponentC;

ブラウザ上での操作はuseStateを利用した時と変わりません。

Context用のコンポーネントを作成

先ほどはApp.jsファイルの中でcreateContextを実行していましたがより汎用的にするためにContext用のコンポーネントの作成を行います。コードも最初は正直わかりにくいとは思いますが先ほど記述したコードと比較しながら確認していけばどのような処理が行われているか理解できるかと思います。useStateを利用していますがuseReducerでも設定方法は同じです。

srcフォルダの下にcontextフォルダを作成します。フォルダが作成できたらCountContext.jsファイルを作成します。

CountContext.jsファイルの中にはコンポーネント間で共有したいデータ、関数を記述します。


import { createContext, useState, useContext } from 'react';

const CountContext = createContext();

export function useCountContext() {
  return useContext(CountContext);
}

export function CountProvider({ children }) {
  const [count, setCount] = useState(100);

  const value = {
    count,
    setCount,
  };

  return (
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

他のコンポーネントでuseCountContextとCountProviderをimportできるようにexport functionで作成しています。

ContentContext.jsファイルの中でContextに関する処理をほとんど記述しているので他のコンポーネントでの記述量がほとんどありません。

App.jsファイルではCountContextコンポーネントからCountProvider関数をimportします。


import React from 'react';
import './App.css';
import ComponentA from './components/ComponentA.js';
import { CountProvider } from './context/CountContext';

function App() {
  return (
    <div className="App">
      <h1>Learn useContext</h1>
      <CountProvider>
        <ComponentA />
      </CountProvider>
    </div>
  );
}

export default App;

ComponentCコンポーネントではCountContextコンポーネントからuseCountContext関数をimportします。useContextはuseCountContextの中で使われているでComponentCでimportする必要はありません。


import React from 'react';
import { useCountContext } from '../context/CountContext';

const ComponentC = () => {
  const { count, setCount } = useCountContext();

  return (
    <div>
      <p>Componet C</p>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
};

export default ComponentC;

ブラウザ上での表示に変化はありません。

useStateで渡したcountをsetCountで増やす
useStateで渡したcountをsetCountで増やす

再描写の確認

Reactではコンポーネントの再描写がパフォーマンスに影響を与える場合があるのでContext APIではどのような状況で再描写が行われるのか確認を行います。

Component CでCountの値を更新した場合に上の階層のComponent Aで再描写が行われるか確認を行います。


import ComponentB from './ComponentB';

const ComponentA = () => {
  console.log('ComponentA Re-Render');
  return (
    <div>
      <p>Componet A</p>
      <ComponentB />
    </div>
  );
};

export default ComponentA;

ページを開いた直後にはコンソールに”ComponentA Re-Render”が表示されますがボタンをクリックしてCountを増やしても再描写されることはありません。useContextを利用していないコンポーネントでは再描写は行われないことがわかります。

Context APIではuseContext()を利用するコンポーネントをConsumerコンポーネントと呼びます。

useContextを設定すると再描写が発生します。returnの中でcount, setCountを利用しているかどうかは関係ありません。下記はuseCountContextからcountとsetCountを取得して利用はしていません。


import { useCountContext } from '../context/CountContext';
import ComponentB from './ComponentB';

const ComponentA = () => {
  console.log('ComponentA Re-Render');
  const { count, setCount } = useCountContext();
  return (
    <div>
      <p>Componet A</p>
      <ComponentB />
    </div>
  );
};

export default ComponentA;

countを利用した場合は再描写が行われることは理解できるので、setCountの関数のみを取り出した場合にも再描写が発生するか確認します。動作確認するとsetCountだけでも再描写されることがわかりました。


import { useCountContext } from '../context/CountContext';
import ComponentB from './ComponentB';

const ComponentA = () => {
  console.log('ComponentA Re-Render');
  const { setCount } = useCountContext();
  return (
    <div>
      <p>Componet A</p>
      <ComponentB />
    </div>
  );
};

export default ComponentA;

動作確認からContext APIではuseContextを利用するコンポーネント(Consumerコンポーネント)はすべてデータ(状態)の更新が行われると再描写されることがわかります。頻繁に更新が行われ複数のコンポーネントでuseContextが設定されている場合はコンポーネントも頻繁に再描写されることを知っておく必要があります。

他のコンポーネントでも表示

ComponentA, ComponentBでもデータが共有できているのか確認するためにComponetAではcountを表示できるように変更します。setCountは必要ないので必要なcountのみ利用します。


import React from 'react';
import ComponentB from './ComponentB';
import { useCountContext } from '../context/CountContext';

const ComponentA = () => {
  const { count } = useCountContext();
  return (
    <div>
      <p>Componet A</p>
      <ComponentB />
      <p>{count}</p>
    </div>
  );
};

export default ComponentA;

ComponentBではComponentCと同様にボタンを追加し、ボタンをクリックするとcountが増える設定を追加します。


import React from 'react';
import ComponentC from './ComponentC';
import { useCountContext } from '../context/CountContext';

const ComponentB = () => {
  const { count, setCount } = useCountContext();
  return (
    <div>
      <p>Componet B</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <ComponentC />
    </div>
  );
};

export default ComponentB;

ComponentCでは先ほどはcountを表示させていましたがCompnentAで表示させるため削除しています。ComponentAで引き続きcountを表示させても問題はありません。


import React from 'react';
import { useCountContext } from '../context/CountContext';

const ComponentC = () => {
  const { count, setCount } = useCountContext();

  return (
    <div>
      <p>Componet C</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
};

export default ComponentC;

どちらのボタンを押してもcount数はアップします。

複数のコンポーネントで共有
複数のコンポーネントで共有

関数を追加

count数を減らせるconuntDown関数をCountContext.jsファイルに追加して共有を行い、ComponentCで実行できるように変更を行います。

追加したcountDown関数は他のコンポーネントで利用するためにはvalueオブジェクトの中に追加する必要があります。


import { createContext, useState, useContext } from 'react';

const CountContext = createContext();

export function useCountContext() {
  return useContext(CountContext);
}

export function CountProvider({ children }) {
  const [count, setCount] = useState(100);

  const countDown = () => {
    setCount(count - 1);
  };

  const value = {
    count,
    setCount,
    countDown,
  };

  return (
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

これでcountDown関数が他のコンポーネントでも利用可能になったのでComponentCを以下のように更新します。


import React from 'react';
import { useCountContext } from '../context/CountContext';

const ComponentC = () => {
  const { countDown } = useCountContext();

  return (
    <div>
      <p>Componet C</p>
      <button onClick={countDown}>-</button>
    </div>
  );
};

export default ComponentC;

ブラウザで確認すると”+”ボタンをクリックするとCountが増え、”-”ボタンをクリックするとCountが減ります。

countの数を増加、減少
countの数を増加、減少

Context用のコンポーネントを作成した場合は処理はすべてCountContext.jsに追加するだけだけで他のコンポーネントの影響はありません。他のコンポーネントで追加した処理を利用した場合はuseCountContextから取り出す際に追加した関数を指定するだけです。

複数のContextの設定

Context APIのContextは1つではなく複数設定することも可能です。Contextを複数設定した場合の利用方法について確認します。

contextフォルダに新たにAnotherCountContext.jsファイルを作成します。中身はこれまでに利用してきたCountContext.jsファイルと同じで変数の名前だけ変えています。通常は機能や役割によって異なるProviderの作成を行います。


import { createContext, useState, useContext } from 'react';

const AnotherCountContext = createContext();

export function useAnotherCountContext() {
  return useContext(AnotherCountContext);
}

export function AnotherCountProvider({ children }) {
  const [anotherCount, setAnotherCount] = useState(200);

  const value = {
    anotherCount,
    setAnotherCount,
  };

  return (
    <AnotherCountContext.Provider value={value}>
      {children}
    </AnotherCountContext.Provider>
  );
}

作成したAnotherCountProviderをimportしてApp.jsファイルで以下のように設定を行います。CountProviderとAnotherCountProviderの2つのコンポーネントをComponentAにラップします。CountProviderとAnotherCountProviderは逆でも構いません。


import './App.css';
import ComponentA from './components/ComponentA';
import { CountProvider } from './context/CountContext';
import { AnotherCountProvider } from './context/AnotherCountContext';

function App() {
  return (
    <div className="App">
      <h1>Learn useContext</h1>
      <CountProvider>
        <AnotherCountProvider>
          <ComponentA />
        </AnotherCountProvider>
      </CountProvider>
    </div>
  );
}

export default App;

ComponentCで共有したデータと関数を利用します。


import { useAnotherCountContext } from '../context/AnotherCountContext';
import { useCountContext } from '../context/CountContext';

const ComponentC = () => {
  const { count, setCount } = useCountContext();
  const { anotherCount, setAnotherCount } = useAnotherCountContext();

  return (
    <div>
      <p>Componet C</p>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <p>another count: {anotherCount}</p>
      <button onClick={() => setAnotherCount(anotherCount + 1)}>+</button>
    </div>
  );
};

export default ComponentC;

それぞれのカウンターは独立しているのでボタンをクリックするともう一つのカウンターに影響を与えることなくカウント数が増えます。

複数のContext APIの設定
複数のContext APIの設定

1つのContextにまとめることができますが複数の共有データを1つのContext APIでまとめるとあるConsumerコンポーネントでは利用することがないデータが更新されても一緒に再描写されることになります。複数のContextにわけることで無駄な再描写が減少するだけでなくシンプルなコードで記述することもできます。

リロード後にデータを保持したい場合(Local Storage)

Context APIに限定されたことではありませんがページをリロードすると更新したデータは初期値に戻ります。ページがリロードしてもデータが保持できるようにブラウザのローカルストレージを利用します。

hooksフォルダを作成してHook用のファイルuseLocalStorage.jsファイルを作成します。useLocalStorangeでは引数にkey, initialValueを受け取ります。ローカルストレージはkey, valueの形で保存することができ引数のkeyはローカルストレージに保存する際に利用します。useStateで初期値を設定していますがローカルストレージにkeyを持つデータが保存されているか確認を行い保存されている場合は初期値にローカルストレージから取得したvalueを設定し、保存されていない場合は渡されたinitialValueを設定します。

useEffectを利用してkeyまたはvalueに更新があった場合にローカルストレージに更新した値を保存しています。


import { useEffect, useState } from 'react';

const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    const keyValue = localStorage.getItem(key);
    return keyValue ? JSON.parse(keyValue) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
};

export default useLocalStorage;

CountContex.jsファイルで作成したHookのuseLocalStorageをimportして引数のkeyにcount, 初期値に100を設定しています。その他のコードに変更はありません。useContextを利用するConsumerコンポーネントのComponentCも変更はありません。


import { createContext, useContext } from 'react';
import useLocalStorage from '../hooks/useLocalStorage';

const CountContext = createContext();

export function useCountContext() {
  return useContext(CountContext);
}

export function CountProvider({ children }) {
  const [count, setCount] = useLocalStorage('count', 100);

  const value = {
    count,
    setCount,
  };

  return (
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

設定後”+”ボタンをクリックしてカウントをアップした後ページを利用しても値が保持されている場合は正常に動作しています。

ブラウザのデベロッパーツールのアプリケーションのローカルストレージを確認するとキーにcount、値にcountの値が保存されています。ボタンをクリックすると値も増えます。

ローカルストレージの値の確認
ローカルストレージの値の確認

まとめ

useContextの設定方法については前半部分は簡単だと感じてもらえたかもしれませんが後半部分は少し難しく感じられたかもしれません。しかし実際に動作確認してみると思っていたほど難しくないことを理解してもらえたのではないでしょうか。