どうも、ノマドクリエイターのショウヘイ(@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 の 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 」が更新されます。
まずは、 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 メソッドを曽孫コンポーネントで使えるようになります。