業務のコードでContextが使われているので勉強しようと思いましたが、公式ドキュメントはクラスコンポーネントを知っていないとなかなか難しいものでした。
色々試して、業務でもさわって、大分Contextについては理解できたかなと思うのでまとめておきます。
概要
ReactのContextの概念や考え方、使い方を説明します。
本記事の説明で書いたコードを載せておきますので、実際の挙動も試していただければと思います。
まずは全体像を掴み、その後具体的な実装方法を説明していきます。
React Context APIとは
Contextとは、Propsのバケツリレーをすることなく「state」と「stateを変更するメソッド」をアプリケーション全体で使えるようにするための、ReactのAPIです。
コンポーネントを跨いで値を渡す時や、複数のコンポーネントで同じ値を参照したい時に有用です。
Contextを使うために最低限知っておく必要があるのは、次の3つの要素です。
- Contextオブジェクト
- Context Provider
- Context Consumer
Contextオブジェクト
コンポーネントのツリー上、直接の親子関係にない離れたコンポーネント間で、同じ値を共有するためのものです。Contextオブジェクトを作り、そのContextからデータを取得して使ったり、データを変更するというように使います。
以下のようにcreateContext()を使って生成します。
export const CountContext = createContext(); // Contextオブジェクト生成
console.log(CountContext);
試しにconsole.logで出力してみると、こんな感じ。
Consumer
と Provider
を持っていることがわかります。
Context Provider
Contextオブジェクトが所持する要素で、対象のContextの適用範囲(Contextが持つ値を使うことができるコンポーネントの範囲)を決める役割を持ちます。
以下のようにProviderで子要素を囲むことで、コンポーネントツリー上でContext Providerの内側(下層)にある全てのコンポーネントから、valueに渡した値や関数を利できます。
return (
<CountContext.Provider value={{ count, setCount }}>
{children}
</CountContext.Provider>
);
このvalueに渡した値が更新されると、配下にあるコンポーネントは全て再レンダリングされる点には注意が必要です。(親コンポーネントが再レンダリングされると、子コンポーネントも再レンダリングされるので)
Context Consumer
Context Providerと同じくContextオブジェクトに備わった要素で、Contextの値を利用したいところで使います。
コンポーネントツリー上で自身より外側(上層)にあるContext ProviderのContextに紐づけられた値にアクセスすることができます。
useContextとは
Consumerを簡単シンプルに使えるhooksです。
以下のように書いて、Providerのvalueに渡した値や関数の中から使いたいものを受け取れます。
const { Contextから受け取る値や関数 } = useContext(Contextオブジェクト名);
ここから、「Contextオブジェクト・Providerコンポーネント・useContext」を使ってContext APIを利用する具体的な方法を説明します。
Context APIの使い方
全体の流れ
Contextを使う流れは、以下の3stepです。
① createContext()を使ってContextオブジェクトを生成する。
② Providerコンポーネントをコンポーネントツリー上に設置する。
③ 子孫コンポーネントでContextオブジェクトを使う。
これらを具体的にみていきます。
step① Contextオブジェクトを生成する
まずは子コンポーネントで使う変数や関数を管理するためのContextを作成します。
createContext()を使えばどこでもContextオブジェクトを作ることはできるわけですが、contextsディレクトリを切ってその中にContextのファイルを作ることが多いかと思います。
今回はcontexts/CountContext.jsxでContextを管理していきます。
// src/contexts/CountContexts.jsx
import { useState, createContext } from "react";
// Contextオブジェクトを生成する
export const CountContext = createContext();
// 生成したContextオブジェクトのProviderを定義する
export const CountProvider = ({children}) => {
const [count, setCount] = useState(0)
return (
<CountContext.Provider value={{
count,
setCount
}}>
{children}
</CountContext.Provider>
);
}
ポイントは以下の2つです。
① createContext()でContextオブジェクトを作成し、外部で使えるようにexportする。
② 子孫コンポーネントをContextオブジェクトのProviderで囲むことで、valueに渡した変数や関数を子孫コンポーネントで使用できるようにする。
このときのProviderは、
<Contextオブジェクト名.Provider value={ 子孫コンポーネントに渡す変数・関数 }>
というように書きます。
※createContext()の引数に初期値を与えることも可能ですが、この初期値はuseContextがProviderを見つけられなかったときのみ参照するとのこと。
Contextを使う以上そのようなことは起こらないはずなので、初期値の設定は通常必要ないのかなと思います。
const MyContext = React.createContext(defaultValue);
コンテクストオブジェクトを作成します。React がこのコンテクストオブジェクトが登録されているコンポーネントをレンダーする場合、ツリー内の最も近い上位の一致する
Provider
から現在のコンテクストの値を読み取ります。
defaultValue
引数は、コンポーネントがツリー内の上位に対応するプロバイダを持っていない場合のみ使用されます。このようなデフォルト値は、ラップしない単独でのコンポーネントのテストにて役に立ちます。補足:undefined
をプロバイダの値として渡しても、コンシューマコンポーネントがdefaultValue
を使用することはありません。
step② Providerコンポーネントをコンポーネントツリー上に設置する
①でexportしたProviderコンポーネントを、コンポーネントツリー上に設置します。
Providerコンポーネントを設置した内側にある全てのコンポーネント(設置したコンポーネントの全ての子孫コンポーネント)から、Contextを参照することができます。
今回は以下のようなツリー構造にしたいと思いますので、まずはコンポーネントツリーの一番上層に位置するApp.jsにProviderを設置します。
App (Providerを設置)
|
ComponentA (Contextを使う子コンポーネント)
|
ComponentB (Contextを使う子コンポーネント)
// src/App.js
import { ComponentA } from "./components/ComponentA";
import { CountProvider } from "./contexts/CountContexts";
export default function App() {
return (
<div>
<CountProvider>
<ComponentA /> // ComponentAの配下にあるコンポーネントでContextを利用可能
</CountProvider>
</div>
);
}
これによりの内側にある全てのコンポーネントで、step①のCountProviderで定義した変数と関数を使えるよう準備ができたということになります。
step③ 子孫コンポーネントでContextオブジェクトを使う
親コンポーネントにProviderを設置したので、子コンポーネント「ComponentA」と、孫コンポーネント「ComponentB」を作成します。
ComponentA・ComponentBの両方から、CountContextの値を参照できるように書いていきます。
まずはComponentAを作ります。
// src/components/ComponentA.jsx
import { useContext } from 'react'
import { ComponentB } from './ComponentB'
import { CountContext } from "../contexts/CountContexts";
export const ComponentA = () => {
// const { Contextから受け取る値や関数 } = useContext(Contextオブジェクト名);
const { count, setCount } = useContext(CountContext);
return (
<div>
<p>
ComponentA:{count}
<button onClick={() => setCount(count + 1)}> + </button>
</p>
<ComponentB />
</div>
);
}
ここでのポイントは3つです。
① useContextをimoprtする。
② useContextの引数にCountContextで作成したContextオブジェクト(Step 1で作成)を入れる。
③ 使いたいオブジェクトを取り出す。(ここではcountとsetCount)
あとは、取り出したオブジェクトをコンポーネント内で好きに使うことができます。
ComponentBも作ります。
// src/components/ComponentB.jsx
import { useContext } from "react";
import { CountContext } from "../contexts/CountContexts";
export const ComponentB = () => {
const { count, setCount } = useContext(CountContext);
return (
<div>
<p>
ComponentB:{count}
<button onClick={() => setCount(count + 1)}> + </button>
</p>
</div>
);
};
するとブラウザにはこのように表示されています。
Contextによって何ができるようになったのか?
① ComponentA・ComponentBの両方で、ContextのcountとsetCount()を使えるようになった。
② ComponentA・ComponentB双方での値の変更を、互いに検知できるようになった。
ComponentA・ComponentBのどちらの+ボタンを押しても、表示しているcountは同じように増えて同じ値を表示します。
補足①:ヘルパー関数を自作する
ここまでの例では、子孫コンポーネントそれぞれで毎回 const { count, setCount } = useContext(CountContext);
と記述していますが、これは子孫コンポーネントで使い回す処理なので、予め共通化してヘルパー関数を自作しておくという方法もよく見られます。
その場合はまずContextファイルでヘルパー関数を定義し、exportしておきます。
// src/contexts/CountContexts.jsx
import React, { useState, createContext, useContext } from "react";
export const CountContext = createContext();
// ヘルパー関数を定義してexport
export const useCountContext = () => useContext(CountContext);
export const CountProvider = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CountContext.Provider
value={{
count,
setCount
}}
>
{children}
</CountContext.Provider>
);
};
子孫コンポーネントは以下のように変わります。
// src/components/ComponentA.jsx
//不要になる import React, { useContext } from "react";
//不要になる import { CountContext } from "../contexts/CountContexts";
import { ComponentB } from "./ComponentB";
import { useCountContext } from "../contexts/CountContexts"; // 追記
export const ComponentA = () => {
//不要になる const { count, setCount } = useContext(CountContext);
const { count, setCount } = useCountContext(); // 追記
return (
<div>
<p>
ComponentA:{count}
<button onClick={() => setCount(count + 1)}> + </button>
</p>
<ComponentB />
</div>
);
};
補足②:アプリケーション内で複数のContextを使う
アプリケーション内で複数のContextを使うこともできます。
その場合はProviderコンポーネントをネストさせます。
以下のようにProviderをネストさせて設置すると、MainContainerコンポーネント配下のコンポーネントではCountContextとUserContextの両方を参照することができます。
import { CountProvider } from "./contexts/CountContext";
import { UserProvider } from "./contexts/UserContext";
import { MainContainer } from "./components/MainContainer";
export default function App() {
return (
<div className="App">
<div>
<CountProvider>
<UserProvider>
<MainContainer />
</UserProvider>
</CountProvider>
</div>
</div>
);
}
このようにネストさせた場合、MainContainer配下の全てのコンポーネントは、CountContextかUserContextどちらかの値が更新される度に再レンダーされることになります。(Contextの値を参照していない子孫コンポーネントであっても再レンダーされる。)
memo化して不要なレンダリングは防ぐ注意が必要です。
Contextを複数使う場合のコード例も載せておきます。
Contextの値が更新されることや、console.logを仕込むなどしてレンダリングの挙動も試して見ていただければと思います。
さいごに
一点だけ不便だなと思ったのが、ページ遷移するとContextのstateの値はクリアされてしまうこと。
業務でログイン中のユーザー情報をcontextのstateに保管していたのですが、そこからReact routerなどを使って別のページに遷移するとstateはnullになってしまいます。
Reduxを使えばページ遷移先に値を送れると聞いたので、次はReduxを勉強してみようと思います。
参考記事