React の useContext フックの使い方を具体例をまじえて解説【 TypeScript 】

ショウヘイ

どうも、ノマドクリエイターのショウヘイ(@shohei_creator)です。

ふーちゃん

ブログアシスタントのふーちゃんです。

React でアプリ開発すると、何階層ものコンポーネントが積みあがることになります。 state (変数の状態)を上層コンポーネントで管理する場合は、もしも下層コンポーネントで state や state を更新する関数が必要なら、 props を利用したバケツリレーをおこなわなければいけません。

React には、 props によるバケツリレーを解消する手段として、 useContext フックが用意されています。下層コンポーネントのどこからでも state や state を更新する関数を取得できるため、コードの保守性を高められます。

この記事では、 React の useContext フックの使い方について説明します。

目次

React の useContext フックとは

React の useContext フックは、『上層コンポーネントで定義された state や state の更新関数』を下層コンポーネントで簡単に取得できる機能です。

React に Context 機能が導入されるまでは、上層コンポーネントから下層コンポーネントに向けて、 state や state の更新関数を props として、バケツリレーしていました。中継となるコンポーネントでも、いちいち下層コンポーネントに渡す props を書かなければいけないので、かなり面倒です。

小規模のアプリならともかく、中規模以上のアプリ開発となると、バケツリレーは開発者体験を損なう問題となりました。この問題を背景にして、 state を一元管理できる Redux のようなライブラリが次々に登場してきました。

やがて、 React 側にも、クラスコンポーネントに Context 機能が実装されました。時間が経って、関数コンポーネントを利用した実装の需要が大きくなるに伴い、 useContext フックも導入されました。

ショウヘイ

コンポーネント props を利用したバケツリレーは、コード量が増えるだけでなく、のちのちの改修を困難にする問題も含んでいます。

バケツリレーの中継となるコンポーネントに不具合が起きると、そこから下のコンポーネントは、 props を受け取れなくなる危険性が生じるからです。

ふーちゃん

ちょっとした爆弾みたいなものですね。

うかつにコードを編集できないです。


React の useContext フックの基本構文と使い方

React で useContext フックを利用するためには、少し準備が必要です。

まずは、 state を管理する上層コンポーネントにて、 React.createContext メソッドを使う必要があります。これにより、 useContext フックで必要になる Context オブジェクトを生成します。

React.createConte メソッドには、引数として初期値を渡せます。のちほど説明するプロバイダーコンポーネントの value プロパティの設定値が参照できなかった時に、代わりに初期値が参照されます。あまり重要ではないので、空文字や空オブジェクトなど、任意の値を設定して構いません。あるいは、参照エラーを意味するメッセージを入れておくと、デバッグ時に役立ちます。

// stateを管理する上層コンポーネント

import React from "react";
 
const Context = React.createContext(defaultValue)

上層コンポーネントには、プロバイダーコンポーネントを設置します。このプロバイダーコンポーネントの中に下層コンポーネントを配置します。

プロバイダーコンポーネントの value プロパティには、 useContext フックで参照したい state を設定します。実際に使う時は、大括弧 {} で囲んでオブジェクト形式にすることが多いです。

// stateを管理する上層コンポーネント

<Context.Provider value={ {var1, var2, var3} }>
  <Chidren />
</Context.Provider>

プロバイダーコンポーネントの value プロパティの中身( state や state を更新する関数)を取り出したい下層コンポーネントでは、 useContext フックを使います。

まずは、上層コンポーネントより、 Context オブジェクト impot しておきます。そして、 useContext フックの引数として、 Context オブジェクトを設定します。すると、 value プロパティを参照できるようになります。

あとは、分割代入などで必要な変数( state や state を更新する関数 )を取得するだけです。

// 下層コンポーネント
import React. { useContext } from "react";
import { Context } from "./Parent";

export const Chidren = () => {
  const { var1, var2, var3} = useContext(Context);

  return (
    (省略)
  );
};

React の useContext フックの具体例( useState フックと連携)

ショウヘイ

React の useContext フックと useState フックの連携させた具体例を紹介します。

下の画像のように、 3 つの入力欄を持つ 4 階層コンポーネントを実装します。

React の useContext と useState を連携させた具体例
ふーちゃん

まだ React の useState フックの使い方を知らない人は、先にこちらの記事に目を通してみてくださいね。

useContext フックを使わなかった場合

ショウヘイ

まずは、 React の useContext フックを使わなかった場合です。

最上位のコンポーネントから、順にコードを説明していきます。

まずは、 App コンポーネントを定義している App.tsx ファイルです。 index.html ファイルの <div id=”root”> タグにマウントします。

App コンポーネントの中には、親コンポーネントである <Parent > タグを設置しています。なお、 <Container> タグは、レイアウト調整のために導入している Bootstrap の固有タグです。

import { Parent } from "./Parent";
import { Container } from "react-bootstrap";

export default function App() {
  return (
    <Container className="mt-4">

      {/* 親コンポーネント */}
      <Parent />

    </Container>
  );
}

次は、親コンポーネントを定義している Parent.tsx ファイルです。

親コンポーネントで state を一元管理したいので、子・孫・曽孫についての useState フックを使って、 state と setState メソッドを生成します。

また、下層コンポーネントに setState メソッドを渡すために、子コンポーネントである <Children> タグの props として、 3 つの setState メソッドを設定します。

import React, { useState } from "react";
import { Alert, Row, Col } from "react-bootstrap";
import { Chidren } from "./Children";

export const Parent: React.VFC = () => {

  // 入力欄の値を保持するための各種useStateフック
  const [child, setChild] = useState<number>(0);
  const [grandchild, setGrandchild] = useState<number>(0);
  const [greatGrandchld, setGreatGrandchild] = useState<number>(0);

  return (
    <Alert variant="primary">
      <p>親コンポーネント</p>
      <Row className="mb-4">
        <Col>子:{child}</Col>
        <Col>孫:{grandchild}</Col>
        <Col>曽孫:{greatGrandchld}</Col>
      </Row>

      {/* 子・孫・曽孫にsetStateメソッドを渡すために、propsとしてバケツリレー */}
      <Chidren
        setChild={setChild}
        setGrandchild={setGrandchild}
        setGreatGrandchild={setGreatGrandchild}
      />

    </Alert>
  );
};

次は、子コンポーネントを定義している Children.tsx ファイルです。

親コンポーネントから渡された props から、 3 つの setState メソッドを取り出します。ボタンのクリックイベントの実行関数として、 setChild メソッドを設定します。残りの setState メソッドは、孫コンポーネントの props に設定しておきます。

import React from "react";
import { Alert } from "react-bootstrap";
import { Grandchild } from "./Grandchild";

// propsのための型定義
type Props = {
  setChild: React.Dispatch<React.SetStateAction<number>>;
  setGrandchild: React.Dispatch<React.SetStateAction<number>>;
  setGreatGrandchild: React.Dispatch<React.SetStateAction<number>>;
};

export const Chidren: React.VFC<Props> = (props) => {

  // propsからsetStateメソッドを分割代入で取り出す
  const { setChild, setGrandchild, setGreatGrandchild } = props;
  
  return (
    <Alert variant="success">
      <p>子コンポーネント</p>
      <input
        className="mb-4"
        type="number"
        defaultValue="0"
        onChange={(e) => setChild(Number(e.target.value))}
      />

      {/* 孫・曽孫にsetStateメソッドを渡すために、propsとしてバケツリレー */}
      <Grandchild
        setGrandchild={setGrandchild}
        setGreatGrandchild={setGreatGrandchild}
      />

    </Alert>
  );
};

次は、孫コンポーネントを定義している Grandchild.tsx ファイルです。

子コンポーネントから渡された props から、 2 つの setState メソッドを取り出します。ボタンのクリックイベントの実行関数として、 setGrandchild メソッドを設定します。残りの setGreatGrandchild メソッドは、孫コンポーネントの props に設定しておきます。

import React from "react";
import { Alert } from "react-bootstrap";
import { GreatGrandchild } from "./GreatGrandchild";

// propsのための型定義
type Props = {
  setGrandchild: React.Dispatch<React.SetStateAction<number>>;
  setGreatGrandchild: React.Dispatch<React.SetStateAction<number>>;
};

export const Grandchild: React.VFC<Props> = (props) => {
  
  // propsからsetStateメソッドを分割代入で取り出す
  const { setGrandchild, setGreatGrandchild } = props;

  return (
    <Alert variant="warning">
      <p>孫コンポーネント</p>
      <input
        className="mb-4"
        type="number"
        defaultValue="0"
        onChange={(e) => setGrandchild(Number(e.target.value))}
      />

      {/* 曽孫にsetStateメソッドを渡すために、propsとしてバケツリレー */}
      <GreatGrandchild setGreatGrandchild={setGreatGrandchild} />

    </Alert>
  );
};

最後に、曽孫コンポーネントを定義している GreatGrandchild.tsx ファイルです。

孫コンポーネントから渡された props から、最後の setState メソッドを取り出して、ボタンのクリックイベントに設定します。

import React from "react";
import { Alert } from "react-bootstrap";

// propsのための型定義
type Props = {
  setGreatGrandchild: React.Dispatch<React.SetStateAction<number>>;
};

export const GreatGrandchild: React.VFC<Props> = (props) => {
  
  // propsからsetStateメソッドを分割代入で取り出す
  const { setGreatGrandchild } = props;

  return (
    <Alert variant="danger">
      <p>曽孫コンポーネント</p>
      <input
        className="mb-4"
        type="number"
        defaultValue="0"
        onChange={(e) => setGreatGrandchild(Number(e.target.value))}
      />
    </Alert>
  );
};

以上のファイル構成とコードにより、このようなアプリが出来上がります。

ふーちゃん

う~ん…… setState メソッドのバケツリレーが酷いですね。

アプリの規模が大きくなるほど、もっと酷いことになるでしょうね。

ショウヘイ

Redux のように、 React の状態管理に関するライブラリの需要があったのも、このバケツリレーの影響が大きいのだろうね。

useContext フックを使った場合

ショウヘイ

今度は、 useContext フックを使った場合です。

最上位のコンポーネントから、順にコードを説明していきます。

まずは、 App.tsx ファイルです。 App コンポーネントの記述は、 useContext フックを使わなかった場合と同じです。

import { Parent } from "./Parent";
import { Container } from "react-bootstrap";

export default function App() {
  return (
    <Container className="mt-4">

      {/* 親コンポーネント */}
      <Parent />

    </Container>
  );
}

次は、親コンポーネントを定義している Parent.tsx ファイルです。

親コンポーネントでは、下層コンポーネントで useContext フックを使うために、 createContext メソッドを使います。引数の初期値には空のオブジェクト {} を渡しておき、 3 つの setState の型を定義した setTypeObject で型アサーションします。また、下層コンポーネントで使うことになるので、 export しておきます。

 下層コンポーネントである <Children> タグ は、 createContext メソッドで生成した定数に「 Provider 」をつけた <setStateContext.Provider> タグで囲みます。また、 value プロパティに、オブジェクト形式で setState メソッドを設定します。

これにより、 下層コンポーネントは、 useContext フックを使って、各種 setState メソッドを取得できるようになります。

import React, { useState } from "react";
import { Alert, Row, Col } from "react-bootstrap";
import { Chidren } from "./Children";

// createContextでsetStateメソッドを使うための型定義
type setTypeObject = {
  setChild: React.Dispatch<React.SetStateAction<number>>;
  setGrandchild: React.Dispatch<React.SetStateAction<number>>;
  setGreatGrandchild: React.Dispatch<React.SetStateAction<number>>;
};

// createContextに空オブジェクトを渡して、setTypeObjectで型アサーション
export const setStateContext = React.createContext({} as setTypeObject);

export const Parent: React.VFC = () => {

  // 入力欄の値を保持するための各種useStateフック
  const [child, setChild] = useState<number>(0);
  const [grandchild, setGrandchild] = useState<number>(0);
  const [greatGrandchld, setGreatGrandchild] = useState<number>(0);

  return (
    <Alert variant="primary">
      <p>親コンポーネント</p>
      <Row className="mb-4">
        <Col>子:{child}</Col>
        <Col>孫:{grandchild}</Col>
        <Col>曽孫:{greatGrandchld}</Col>
      </Row>

      {/* 下層コンポーネントがuseContextフックを使えるようにするために、setStateContext.Providerタグで囲む */}
      <setStateContext.Provider
      
      // valueに対して、オブジェクト形式でsetStateメソッドを設定しておく。
        value={{ setChild, setGrandchild, setGreatGrandchild }}
      >
        <Chidren />
      </setStateContext.Provider>

    </Alert>
  );
};

次は、子コンポーネントを定義している Children.tsx ファイルです。

はじめに、親コンポーネントで定義した setStateContext を import します。そして、 useContext フックの引数として設定します。すると、 <setStateContext.Provider> タグ の value プロパティの設定値を参照できるようになるので、分割代入で setChild メソッドを取得します。

あとは、ボタンのクリックイベントの実行関数として、 setChild メソッドを設定します。

import React, { useContext } from "react";
import { Alert } from "react-bootstrap";
import { Grandchild } from "./Grandchild";

// 親コンポーネントで定義したsetStateContextを取得
import { setStateContext } from "./Parent";

export const Chidren: React.VFC = () => {

  // useContextフックにsetStateContextを渡す。
  // setChildメソッドを分割代入で取り出す。
  const { setChild } = useContext(setStateContext);

  return (
    <Alert variant="success">
      <p>子コンポーネント</p>
      <input
        className="mb-4"
        type="number"
        defaultValue="0"
        onChange={(e) => setChild(Number(e.target.value))}
      />
      <Grandchild />
    </Alert>
  );
};

次は、孫コンポーネントを定義している Grandchild.tsx ファイルです。

子コンポーネントと同様の手順を踏んで、 setGrandchild メソッドを取得して、ボタンのクリックイベントの実行関数として設定します。

import React, { useContext } from "react";
import { Alert } from "react-bootstrap";
import { GreatGrandchild } from "./GreatGrandchild";

// 親コンポーネントで定義したsetStateContextを取得
import { setStateContext } from "./Parent";

export const Grandchild: React.VFC = () => {
  
  // useContextフックにsetStateContextを渡す。
  // setGrandchildメソッドを分割代入で取り出す。
  const { setGrandchild } = useContext(setStateContext);

  return (
    <Alert variant="warning">
      <p>孫コンポーネント</p>
      <input
        className="mb-4"
        type="number"
        defaultValue="0"
        onChange={(e) => setGrandchild(Number(e.target.value))}
      />
      <GreatGrandchild />
    </Alert>
  );
};

最後に、曽孫コンポーネントを定義している GreatGrandchild.tsx ファイルです。

 子・孫コンポーネントと同様の手順を踏んで、 setGreatGrandchild メソッドを取得して、ボタンのクリックイベントの実行関数として設定します。 

import React, { useContext } from "react";
import { Alert } from "react-bootstrap";

// 親コンポーネントで定義したsetStateContextを取得
import { setStateContext } from "./Parent";

export const GreatGrandchild: React.VFC = () => {

  // useContextフックにsetStateContextを渡す。
  // setGreatGrandchildメソッドを分割代入で取り出す。
  const { setGreatGrandchild } = useContext(setStateContext);

  return (
    <Alert variant="danger">
      <p>曽孫コンポーネント</p>
      <input
        className="mb-4"
        type="number"
        defaultValue="0"
        onChange={(e) => setGreatGrandchild(Number(e.target.value))}
      />
    </Alert>
  );
};

以上のファイル構成とコードにより、 useContext フックを使わない場合と同じアプリが出来上がります。

ふーちゃん

親コンポーネント側のコード量が少し増えますが、その代わりに、下層コンポーネントのコード量がグッと減りましたね! 

setState メソッドのバケツリレーも無くなって、見た目がスッキリとしました。

ショウヘイ

ほんの一手間をかけるだけで、かなり保守性が向上する。

これが useContext フックの魅力だね。

React の useContext フックの具体例( useReduecr と連携)

ショウヘイ

React の useContext フックは、 useReduer フックと組み合わせて使われることも多いです。

下層コンポーネントに対して、 useReducer フックで生成した dispatch メソッドを渡す具体例も紹介しておきます。

ふーちゃん

まだ React の useReducer フックの使い方を知らない人は、先にこちらの記事に目を通してみてくださいね。

ショウヘイ

React の useState フックと連携させた時と同様に、多層コンポーネントを用意しました。

親コンポーネントで生成した dispatch メソッドを曽孫コンポーネントに渡す実装です。

曽孫コンポーネントのボタンをクリックすると、 dispatch メソッドを経由して、親コンポーネントの「 count 」が更新されます。

React の useContext と useReducer を連携させた具体例

まずは、 App.tsx ファイルです。 <Parent> タグを置いているだけですね。

import { Parent } from "./Parent";
import { Container } from "react-bootstrap";

export default function App() {
  return (
    <Container className="mt-4">

      {/* 親コンポーネント */}
      <Parent />

    </Container>
  );
}

次に、 Parent.tsx ファイルです。 useReducer フックについての記述もまとめているので、そこそこのコード量になっています。

まずは、 useReducer フックに関わる State 型と Action 型、そして createContext メソッドの型アサーションで使うための Reducer 型を定義しておきます。

次に、 dispatch メソッドを経由して state を更新できるように、 reducer 関数を定義します。

下層コンポーネントで useContext フックを使うために、 createContext メソッドを使います。引数の初期値には空のオブジェクト {} を渡しておき、 Reducer 型でアサーションします。また、 下層コンポーネントで使うことになるので、 export しておきます。

 下層コンポーネントである <Children> タグは、 createContext メソッドで生成した定数に「 Provider 」をつけた <reducerContext.Provider> タグで囲みます。また、 value プロパティに、オブジェクト形式で dispatch メソッドと initialState (初期値)を設定します。

import React, { useReducer } from "react";
import { Alert } from "react-bootstrap";
import { Chidren } from "./Children";

// stateの型定義
type State = {
  count: number;
};

// Actionの型定義
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset"; payload: State };

// createContextで使う型アサーション
type Reducer = {
  dispatch: React.Dispatch<Action>;
  initialState: State;
};

// useReducerフックの引数として使う初期値
const initialState: State = {
  count: 0
};

// reducer関数を定義
const reducer: React.Reducer<State, Action> = (
  state: State,
  action: Action
): State => {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    case "reset":
      return action.payload;
    default:
      throw new Error();
  }
};

// createContextに空オブジェクトを渡して、Reducerで型アサーション
export const reducerContext = React.createContext({} as Reducer);

export const Parent: React.VFC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <Alert variant="primary">
      <p>親コンポーネント</p>
      <p>count: {state.count}</p>

      {/* 下層コンポーネントがuseContextフックを使えるようにするために、reducerContext.Providerタグで囲む */}
      <reducerContext.Provider
        // valueに対して、オブジェクト形式でdispatchメソッドを設定しておく。
        value={{ dispatch, initialState }}
      >
        <Chidren />
      </reducerContext.Provider>
    </Alert>
  );
};

次に、 Children.tsx ファイルと Grandchild.tsx ファイルです。簡単なコードなので、 2 ファイル分をまとめて載せておきます。

// Children.tsxファイルの内容

import React from "react";
import { Alert } from "react-bootstrap";
import { Grandchild } from "./Grandchild";

export const Chidren: React.VFC = () => {
  return (
    <Alert variant="success">
      <p>子コンポーネント</p>
      <Grandchild />
    </Alert>
  );
};


~~~~~~~~~~~~~~~~~~~~

// Grandchild.tsxファイルの内容

import React from "react";
import { Alert } from "react-bootstrap";
import { GreatGrandchild } from "./GreatGrandchild";

export const Grandchild: React.VFC = () => {
  return (
    <Alert variant="warning">
      <p>孫コンポーネント</p>
      <GreatGrandchild />
    </Alert>
  );
};

最後に、 GreatGrandChild.tsx ファイルです。

はじめに、親コンポーネントで定義した reducerContext を import します。そして、 useContext フックの引数として設定します。すると、 <reducerContext.Provider> タグ の value プロパティの設定値を参照できるようになるので、分割代入で dispatch メソッドと initalState を取得します。

あとは、各種ボタンのクリックイベントに、 dispatch メソッド(引数として、 state の更新方法をオブジェクト形式で定義)を設定するだけです。

import React, { useContext } from "react";
import { Alert, Button } from "react-bootstrap";

// 親コンポーネントで定義したreducerContextを取得
import { reducerContext } from "./Parent";

export const GreatGrandchild: React.VFC = () => {

  // useContextフックにreducerContextを渡す。
  // dispatchメソッドとinitialStateを分割代入で取り出す。
  const { dispatch, initialState } = useContext(reducerContext);

  return (
    <Alert variant="danger">
      <p>曽孫コンポーネント</p>
      <Button
        onClick={() => dispatch({ type: "reset", payload: initialState })}
      >
        リセット
      </Button>{" "}
      <Button onClick={() => dispatch({ type: "decrement" })}>1つ減らす</Button>{" "}
      <Button onClick={() => dispatch({ type: "increment" })}>1つ増やす</Button>
    </Alert>
  );
};

以上のファイル構成とコードにより、親コンポーネントで生成した dispatch メソッドを曽孫コンポーネントで使えるようになります。


この記事を知り合いに共有する
URLをコピーする
URLをコピーしました!
目次
閉じる