LoginSignup
1
0

ReactのuseContextを使いこなす

Last updated at Posted at 2024-03-05

はじめに

Reactのコンポーネント間で値を受け渡すには、propsを使って親から子へ渡すしかないと思っていましたが、実はそれ以外にもあるということを知りました。

その1つが今回紹介するuseContextです。

propsとの違いはなにか、どのようなことができるのかを、基礎から確認していきます。

useContextとは

useContextはコンポーネント間でpropsを介さずに値を受け渡すことができるReactフックです。
propsの代わりにコンテクストと呼ばれるものを使用して値の受け渡しを行います。

propsの問題点

useContextの詳細について確認する前に、propsを使うと問題がある場合について確認します。

以下のようなアプリについて考えてみます。

ライトテーマかダークテーマかを、画面上で選択できるようにしています。
簡略化のために現在選択されているテーマを画面上に文字列として出力しています。

ソースコードは以下のようになります。

ソースコード詳細
WithoutContextApp.js
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;

上記はメインの処理です。
テーマとテーマを管理する用の処理を定義しています。

WithoutContextHeader.js
import WithoutContextContent from "./WithoutContextContent";

const WithoutContextHeader = ({ theme, toggleTheme }) => {
    return (
        <>
            <h1>Header</h1>
            <WithoutContextContent theme={theme} toggleTheme={toggleTheme} />
        </>
    );
};

export default WithoutContextHeader;

上記はヘッダーの処理です。
受け取ったpropsを子コンポーネントに引き渡すだけです。

WithoutContextContent.js
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つずつ確認していきましょう。

コンテクストの生成

ExampleBasicContext.js
import { createContext } from "react";
// コンテクストの生成、コンポーネントの外部で定義
const ExampleContext = createContext('default-txt');

まず、createContextを使用して、コンテクストオブジェクトを生成します。
このcreateContextはコンポーネントの外部で定義します。

引数にはコンテクストのデフォルト値を設定します。
今回はdefault-txtという文字列を設定しましたが、文字列に限らずオブジェクトや関数なども設定できます。
デフォルト値が必要ない場合にはnullとします。

戻り値としてコンテクストオブジェクトを受け取ります。
戻り値を受け取る変数はパスカルケースとします。
このコンテクストオブジェクトを使って、コンテクストの受け渡し、コンテクストの使用を行います。

コンテクストの受け渡し

ExampleBasicContext.js
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)を、さきほど生成したコンテクストオブジェクトで囲みます。
※コンテクストオブジェクトをコンポーネントとして利用したいので、コンテクストオブジェクトが格納されている変数名は必ずパスカルケースにします。

コンテクストオブジェクト.Providervalue属性に、子コンポーネントで使用したい値を設定します。
ここではprovider-txtという文字列を受け渡していますが、文字列以外でも構いません。

このようにすることで、子コンポーネントで、value属性に定義した値をコンテクストとして受け取ることができるようになります。

コンテクストの使用

ExampleBasicContext.js
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にデフォルト値を設定しました。
しかし、先ほど確認した例ではデフォルト値は登場していません。
どのようにして使うかを確認します。

ExampleBasicContext
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の値が設定されます。

どういうことか見てみましょう。

ExampleBasicContext.js
// 親コンポーネントのみ抜粋
// 親コンポーネント
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)で確認してきましたが、通常はコンポーネントごとにファイルを用意するため、ファイルを分割してインポート、エクスポートについて確認します。

generateContext.js
import { createContext } from "react";
// コンテクストの生成
export const ExampleContext = createContext('default-txt');

まず、コンテクストを生成、エクスポートするファイルを用意します。
値の受け渡し、使用はこのコンテクストをインポートして使用することになります。

では、値の受け渡しについて確認します。

ExampleBasicContext.js
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をインポートしています。
他の処理は今までと変わりません。

次に、コンテクストの使用について確認します。

ExampleBody.js
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;

useContextExampleContextをインポートしています。
コンテクストを使用する場合は、生成されたコンテクストをインポートして、useContextの引数として設定するだけです。
基本的な書き方は今まで確認してきた例と変わりません。

このようにして、コンテクストの生成を別ファイルに切り出して、設定したい箇所、使用したい箇所でそれぞれ呼び出して使用することができます。

コンテクストの値を更新する

今まではProviderで渡した文字列を画面に表示させてきましたが、実際の開発では渡された値を更新したい場面もあるはずです。
propsの問題点で確認したような、テーマを切り替えられる実装を考えてみましょう。

画面は以下のようになります。

propsの問題点で示した表示とは若干異なりますが、ボタンを押下することでヘッダー内のテーマを切り替えられるようにしています。

ソースコードは長くなるので折りたたみ状態にしておきます。

ソースコード詳細
ExampleBasicContext.js
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は別々に定義します。

ExampleHeader.js
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;

テーマのコンテクストを受け取って表示するようにしました。

ExampleBody.js
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イベントに紐づけています。

generateContext.js
import { createContext } from "react";
// コンテクストの生成
export const ExampleContext = createContext('light');
export const ExampleUpdateContext = createContext();

テーマ更新用のコンテクストを生成しています。初期値は設定していません。

ポイントは2点あります。

ポイント1

stateProviderの値として設定していること。

useStateで生成したstate(ここではtheme)を設定することで、更新用関数(ボタン押下)で値が更新された場合には、使用箇所が一律で更新されます。
階層構造がどれほど離れていても関係ありません。

ポイント2

デフォルト値は更新されないこと。

切り替えボタンを押下するとわかりますが、Example Headerの値は「light」、「dark」と切り替わるのに対し、Other Bodyの値は「light」のままです。

これは、Other Bodyの値にデフォルト値が使用されているためです。
Providerでラップされていないため、文字列としての「light」が設定されているだけです。
ProviderでラップされているExample Headerstateが設定されているため、ボタンの切り替えに応じてstateが更新され、画面表示が変更されます。

コンテクストを使用する注意点

propsと異なり、コンポーネントを飛び越えて、グローバル変数のように値をやり取りできるコンテクストですが、いくつか注意点があります。

使いすぎ注意

階層構造があればどこからでもアクセスすることができるため、何でもかんでもコンテクストで管理すると、コンポーネントの再利用性や可読性が低下します。
グローバル変数と同じように、使い所は慎重に検討する必要があります。

再レンダリング注意

Providerの値が変更されると、関連するコンポーネントすべてが一律で再レンダリングされます。
直接値を使用していなくても、再レンダリングされるため、規模が大きくなってくるとパフォーマンスへの影響が懸念されます。

まとめ

useContextはグローバル変数のように、コンポーネント間で値をやり取りできることがわかりました。

useStateや、ここでは紹介していませんがuseReducerと組み合わせることで、値の更新もできるため、より効果的にコンテクストを使用することができそうです。

なお、useContextuseReducerの合せ技を個人的に実装するのもいいですが、それをもっと便利に使用できるReduxというライブラリがあるそうです。

これについてもいずれ学習をしてみたいと思っています。

参考にしたサイト

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0