はじめに
Reactのコンポーネント間で値を受け渡すには、props
を使って親から子へ渡すしかないと思っていましたが、実はそれ以外にもあるということを知りました。
その1つが今回紹介するuseContext
です。
props
との違いはなにか、どのようなことができるのかを、基礎から確認していきます。
useContextとは
useContext
はコンポーネント間でprops
を介さずに値を受け渡すことができるReactフックです。
props
の代わりにコンテクストと呼ばれるものを使用して値の受け渡しを行います。
propsの問題点
useContext
の詳細について確認する前に、props
を使うと問題がある場合について確認します。
以下のようなアプリについて考えてみます。
ライトテーマかダークテーマかを、画面上で選択できるようにしています。
簡略化のために現在選択されているテーマを画面上に文字列として出力しています。
ソースコードは以下のようになります。
ソースコード詳細
import { useState } from "react";
import WithoutContextHeader from "./WithoutContextHeader";
const WithoutContextApp = () => {
const [theme, setTheme] = useState('light');
const toggleTheme = (e) => {
const val = e.target.value;
setTheme(val);
}
return (
<WithoutContextHeader theme={theme} toggleTheme={toggleTheme} />
);
};
export default WithoutContextApp;
上記はメインの処理です。
テーマとテーマを管理する用の処理を定義しています。
import WithoutContextContent from "./WithoutContextContent";
const WithoutContextHeader = ({ theme, toggleTheme }) => {
return (
<>
<h1>Header</h1>
<WithoutContextContent theme={theme} toggleTheme={toggleTheme} />
</>
);
};
export default WithoutContextHeader;
上記はヘッダーの処理です。
受け取ったprops
を子コンポーネントに引き渡すだけです。
import React from "react";
const WithoutContextContent = ({ theme, toggleTheme }) => {
const THEMES = ['light', 'dark'];
return (
<>
<h2>Content</h2>
<p>Current theme:{theme}</p>
{THEMES.map(t => {
return (
<React.Fragment key={t}>
<label htmlFor={t}>{t} theme</label>
<input id={t} name="theme" type="radio" value={t} onChange={toggleTheme} checked={theme === t} />
</React.Fragment>
);
})}
</>
);
};
export default WithoutContextContent;
上記は実際に画面に出力される処理です。
受け取ったprops
を元に、画面表示項目やラジオボタンが変更されたときの処理を定義しています。
ソースコードを確認すると分かる通り、下層のコンポーネントにprops
を引き渡すために、props
のバケツリレーのようなことが行われています。
あるコンポーネントでは渡されたprops
を使用しない場合でも、その子コンポーネントで使用したい場合にはprops
を受け取って引き渡さなければいけません。
例示では階層構造は浅めですが、これが深い階層になると大変です。
また、props
自体の数が増えた場合も同様に受け渡しが大変になります。
このように、props
は使用したい先まで、順番に回していく必要があるため、アプリの規模が大きくなると複雑になっていくという問題点があります。
useContextの基本
props
の問題点を解決できるのが、useContext
です。
コンテクストを使うことで、コンポーネントを飛び越えて値を受け渡すことができます。
- コンテクストの生成
- コンテクストの受け渡し
- コンテクストの使用
の上記3点から成り立っています。
1つずつ確認していきましょう。
コンテクストの生成
import { createContext } from "react";
// コンテクストの生成、コンポーネントの外部で定義
const ExampleContext = createContext('default-txt');
まず、createContext
を使用して、コンテクストオブジェクトを生成します。
このcreateContext
はコンポーネントの外部で定義します。
引数にはコンテクストのデフォルト値を設定します。
今回はdefault-txt
という文字列を設定しましたが、文字列に限らずオブジェクトや関数なども設定できます。
デフォルト値が必要ない場合にはnull
とします。
戻り値としてコンテクストオブジェクトを受け取ります。
戻り値を受け取る変数はパスカルケースとします。
このコンテクストオブジェクトを使って、コンテクストの受け渡し、コンテクストの使用を行います。
コンテクストの受け渡し
import { createContext } from "react";
// コンテクストオブジェクトの生成、コンポーネントの外部で定義
const ExampleContext = createContext('default-txt');
// 親コンポーネント
const ExampleBasicContext = () => {
return (
<>
{/* 受け渡したいコンテクストの設定。子コンポーネントをラップしている。 */}
<ExampleContext.Provider value="provider-txt">
<ExampleHeader />
</ExampleContext.Provider>
</>
);
};
// 子コンポーネント
const ExampleHeader = () => {
return (
<h1>Example Header</h1>
);
};
コンテクストを受け渡すにはProvider
を使用します。
値を受け取りたいコンポーネント(ここではExampleHeader
)を、さきほど生成したコンテクストオブジェクトで囲みます。
※コンテクストオブジェクトをコンポーネントとして利用したいので、コンテクストオブジェクトが格納されている変数名は必ずパスカルケースにします。
コンテクストオブジェクト.Provider
のvalue
属性に、子コンポーネントで使用したい値を設定します。
ここではprovider-txt
という文字列を受け渡していますが、文字列以外でも構いません。
このようにすることで、子コンポーネントで、value
属性に定義した値をコンテクストとして受け取ることができるようになります。
コンテクストの使用
import { createContext, useContext } from "react";
// コンテクストの生成、コンポーネントの外部で定義
const ExampleContext = createContext('default-txt');
// 親コンポーネント
const ExampleBasicContext = () => {
return (
<>
{/* 受け渡したいコンテクストの設定。子コンポーネントをラップしている。 */}
<ExampleContext.Provider value="provider-txt">
{/* 子コンポーネント */}
<ExampleHeader />
</ExampleContext.Provider>
</>
);
};
// 子コンポーネント
const ExampleHeader = () => {
return (
<>
<h1>Example Header</h1>
{/* 孫コンポーネント */}
<ExampleBody />
</>
);
};
// 孫コンポーネント
const ExampleBody = () => {
// コンテクストの使用
const txt = useContext(ExampleContext);
return (
<>
<h2>Example Body</h2>
{/* 受け取ったコンテクストを設定 */}
<p>{txt}</p>
</>
);
};
export default ExampleBasicContext;
コンポーネントを飛び越えて値を受け渡しできることを確認するため、孫コンポーネントも追加しました。
コンテクストを使用するにはuseContext
を使用します。
引数にはコンテクストオブジェクトを設定します。
戻り値にはProvider
で設定したvalue
の値が設定されます。
上記のコードを画面に表示させると以下のようになります。
※ここには記載していませんが、ExampleBasicContext
コンポーネントを別のコンポーネントで呼び出して画面描画しています。
画面上にはprovider-txt
が表示されているのがわかります。
ここで注目してほしいのは、子コンポーネントから孫コンポーネントに対しては何も処理をしていないということです。
ただ単に呼び出しているだけですが、親コンポーネントで設定したProvider
の値を孫コンポーネントが受け取っています。
これがコンテクストの大きな特徴です。
階層がどれだけ深くなろうと、値のリレーをすることなく、使用したい箇所で値を受け取ることができます。
useContextを使いこなす
useContext
を使うことで、コンポーネントを飛び越えてコンテクストをやり取りできることを確認しました。
ここからはuseContext
を使いこなすために深堀りしていきます。
useContextのデフォルト値
コンテクストの作成時、createContext
にデフォルト値を設定しました。
しかし、先ほど確認した例ではデフォルト値は登場していません。
どのようにして使うかを確認します。
import { createContext, useContext } from "react";
// コンテクストの生成、コンポーネントの外部で定義
const ExampleContext = createContext('default-txt');
// 親コンポーネント
const ExampleBasicContext = () => {
return (
<>
{/* 受け渡したいコンテクストの設定。子コンポーネントをラップしている。 */}
<ExampleContext.Provider value="provider-txt">
{/* 子コンポーネント */}
<ExampleHeader />
</ExampleContext.Provider>
{/* Providerでラップされていない */}
<OtherBody />
</>
);
};
/* 子、孫コンポーネントの記載を省略 */
// その他のコンポーネント
const OtherBody = () => {
// コンテクストの使用
const othTxt = useContext(ExampleContext);
return (
<>
<h1>OtherBody</h1>
{/* 受け取ったコンテクストを設定 */}
<p>{othTxt}</p>
</>
);
};
デフォルト値は、Provider
でラップされていないコンポーネントでコンテクストを使用した場合に設定されます。
今回はOtherBodyコンポーネントを用意して、Provider
の外部に設定しました。
そして、OtherBodyコンポーネント内でコンテクストを使用しています。
画面は以下のように表示されます。
OtherBodyではcreateContext
で設定したデフォルト値が表示されています。
基本的に、コンテクストを使用する場合にはProvider
で囲むので、デフォルト値の出番はあまりないかと思います。
Provider
でラップされていないときにエラーとならないよう、フォールバックとして使うことが多いのかな、と感じました。
コンテクストの上書き
コンテクストが受け取るProvider
の値は、親要素をたどっていったときに、一番近いProvider
の値が設定されます。
どういうことか見てみましょう。
// 親コンポーネントのみ抜粋
// 親コンポーネント
const ExampleBasicContext = () => {
return (
<>
{/* 受け渡したいコンテクストの設定。子コンポーネントをラップしている。 */}
<ExampleContext.Provider value="provider-txt">
{/* 子コンポーネント */}
<ExampleHeader />
{/* Providerをネスト、別のコンテクストを設定 */}
<ExampleContext.Provider value="reloaded-txt">
{/* 子コンポーネント */}
<ExampleHeader />
</ExampleContext.Provider>
</ExampleContext.Provider>
{/* Providerでラップされていない */}
<OtherBody />
</>
);
};
Provider
内で別のProvider
を定義しています。
2つめのExample Bodyは2つのProvider
にラップされています。
そのうち、一番近いProvider
であるreloaded-txt
が表示されています。
1つめのExample Bodyは今まで通り、provider-txt
です。
このようにProvider
が階層構造になっている場合、Provider
を使用している箇所から一番近いラップされたProvider
を取得します。
今回はわかりやすいように同じコンポーネント内でラップしていますが、別コンポーネントで新たにラップした場合も同じです。
トップレベルのコンポーネントでProvider
を提供しておき、一部の子コンポーネントでは別のProvider
を使う、ということができます。
コンテクストのインポート、エクスポート
createComponent
で生成されたコンテクストは、オブジェクトであるため、他のオブジェクトと同様、インポートとエクスポートができます。
今までの例では1つのファイル(ExampleBasicContext.js
)で確認してきましたが、通常はコンポーネントごとにファイルを用意するため、ファイルを分割してインポート、エクスポートについて確認します。
import { createContext } from "react";
// コンテクストの生成
export const ExampleContext = createContext('default-txt');
まず、コンテクストを生成、エクスポートするファイルを用意します。
値の受け渡し、使用はこのコンテクストをインポートして使用することになります。
では、値の受け渡しについて確認します。
import ExampleHeader from "./ExampleHeader";
import OtherBody from "./OtherBody";
import { ExampleContext } from "./generateContext";
// 親コンポーネント
const ExampleBasicContext = () => {
return (
<>
{/* 受け渡したいコンテクストの設定。子コンポーネントをラップしている。 */}
<ExampleContext.Provider value="provider-txt">
{/* 子コンポーネント */}
<ExampleHeader />
{/* 別のコンテクストを設定 */}
<ExampleContext.Provider value="reloaded-txt">
{/* 子コンポーネント */}
<ExampleHeader />
</ExampleContext.Provider>
</ExampleContext.Provider>
{/* Providerでラップされていない */}
<OtherBody />
</>
);
};
export default ExampleBasicContext;
子、孫、その他コンポーネントはそれぞれ別のファイルに定義し直しました。
コンテクストの生成処理がなくなった代わりに、ExampleContext
をインポートしています。
他の処理は今までと変わりません。
次に、コンテクストの使用について確認します。
import { ExampleContext } from "./generateContext";
import { useContext } from "react";
// 孫コンポーネント
const ExampleBody = () => {
// コンテクストの使用
const txt = useContext(ExampleContext);
return (
<>
<h2>Example Body</h2>
{/* 受け取ったコンテクストを設定 */}
<p>{txt}</p>
</>
);
};
export default ExampleBody;
useContext
とExampleContext
をインポートしています。
コンテクストを使用する場合は、生成されたコンテクストをインポートして、useContext
の引数として設定するだけです。
基本的な書き方は今まで確認してきた例と変わりません。
このようにして、コンテクストの生成を別ファイルに切り出して、設定したい箇所、使用したい箇所でそれぞれ呼び出して使用することができます。
コンテクストの値を更新する
今まではProvider
で渡した文字列を画面に表示させてきましたが、実際の開発では渡された値を更新したい場面もあるはずです。
propsの問題点で確認したような、テーマを切り替えられる実装を考えてみましょう。
画面は以下のようになります。
propsの問題点で示した表示とは若干異なりますが、ボタンを押下することでヘッダー内のテーマを切り替えられるようにしています。
ソースコードは長くなるので折りたたみ状態にしておきます。
ソースコード詳細
import { useState } from "react";
import ExampleHeader from "./ExampleHeader";
import OtherBody from "./OtherBody";
import { ExampleContext, ExampleUpdateContext } from "./generateContext";
// 親コンポーネント
const ExampleBasicContext = () => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}
return (
<>
{/* 受け渡したいコンテクストの設定。子コンポーネントをラップしている。 */}
<ExampleContext.Provider value={theme}>
<ExampleUpdateContext.Provider value={toggleTheme}>
{/* 子コンポーネント */}
<ExampleHeader />
</ExampleUpdateContext.Provider>
</ExampleContext.Provider>
{/* Providerでラップされていない */}
<OtherBody />
</>
);
};
export default ExampleBasicContext;
useState
を使用して、設定したい値と、値を更新する関数を取得します。
テーマを設定するProvider
とテーマを更新するProvider
は別々に定義します。
import { useContext } from "react";
import ExampleBody from "./ExampleBody";
import { ExampleContext } from "./generateContext";
// 子コンポーネント
const ExampleHeader = () => {
const theme = useContext(ExampleContext);
return (
<>
<h1>Example Header</h1>
{/* 受け取ったコンテクストを設定 */}
<p>Current theme:{theme}</p>
{/* 孫コンポーネント */}
<ExampleBody />
</>
);
};
export default ExampleHeader;
テーマのコンテクストを受け取って表示するようにしました。
import { ExampleUpdateContext } from "./generateContext";
import React, { useContext } from "react";
// 孫コンポーネント
const ExampleBody = () => {
// コンテクストの使用
const toggleTheme = useContext(ExampleUpdateContext);
return (
<>
<h2>Example Body</h2>
{/* コンテクストを更新する処理を設定 */}
<button type="button" onClick={toggleTheme} >切り替え</button>
</>
);
};
export default ExampleBody;
テーマを更新する用のコンテクストを受け取って、ボタンのonClick
イベントに紐づけています。
import { createContext } from "react";
// コンテクストの生成
export const ExampleContext = createContext('light');
export const ExampleUpdateContext = createContext();
テーマ更新用のコンテクストを生成しています。初期値は設定していません。
ポイントは2点あります。
ポイント1
state
をProvider
の値として設定していること。
useState
で生成したstate
(ここではtheme
)を設定することで、更新用関数(ボタン押下)で値が更新された場合には、使用箇所が一律で更新されます。
階層構造がどれほど離れていても関係ありません。
ポイント2
デフォルト値は更新されないこと。
切り替えボタンを押下するとわかりますが、Example Header
の値は「light」、「dark」と切り替わるのに対し、Other Body
の値は「light」のままです。
これは、Other Body
の値にデフォルト値が使用されているためです。
Provider
でラップされていないため、文字列としての「light」が設定されているだけです。
Provider
でラップされているExample Header
はstate
が設定されているため、ボタンの切り替えに応じてstate
が更新され、画面表示が変更されます。
コンテクストを使用する注意点
props
と異なり、コンポーネントを飛び越えて、グローバル変数のように値をやり取りできるコンテクストですが、いくつか注意点があります。
使いすぎ注意
階層構造があればどこからでもアクセスすることができるため、何でもかんでもコンテクストで管理すると、コンポーネントの再利用性や可読性が低下します。
グローバル変数と同じように、使い所は慎重に検討する必要があります。
再レンダリング注意
Provider
の値が変更されると、関連するコンポーネントすべてが一律で再レンダリングされます。
直接値を使用していなくても、再レンダリングされるため、規模が大きくなってくるとパフォーマンスへの影響が懸念されます。
まとめ
useContext
はグローバル変数のように、コンポーネント間で値をやり取りできることがわかりました。
useState
や、ここでは紹介していませんがuseReducer
と組み合わせることで、値の更新もできるため、より効果的にコンテクストを使用することができそうです。
なお、useContext
とuseReducer
の合せ技を個人的に実装するのもいいですが、それをもっと便利に使用できるReduxというライブラリがあるそうです。
これについてもいずれ学習をしてみたいと思っています。