ReactのuseContextを使いこなす

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

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

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

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

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

以下のようなアプリについて考えてみます。
<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/83c01898-65bd-e855-7216-0c4b88a008e9.png'>
ライトテーマかダークテーマかを、画面上で選択できるようにしています。
簡略化のために現在選択されているテーマを画面上に文字列として出力しています。

ソースコードは以下のようになります。
<details><summary>ソースコード詳細</summary>

```react: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;
```
上記はメインの処理です。
テーマとテーマを管理する用の処理を定義しています。

```react:WithoutContextHeader.js
import WithoutContextContent from "./WithoutContextContent";

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

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

```react: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`を元に、画面表示項目やラジオボタンが変更されたときの処理を定義しています。

</details>

ソースコードを確認すると分かる通り、下層のコンポーネントに`props`を引き渡すために、`props`のバケツリレーのようなことが行われています。

あるコンポーネントでは渡された`props`を使用しない場合でも、その子コンポーネントで使用したい場合には`props`を受け取って引き渡さなければいけません。

例示では階層構造は浅めですが、これが深い階層になると大変です。
また、`props`自体の数が増えた場合も同様に受け渡しが大変になります。

このように、`props`は使用したい先まで、順番に回していく必要があるため、アプリの規模が大きくなると複雑になっていくという問題点があります。

### useContextの基本
`props`の問題点を解決できるのが、`useContext`です。
コンテクストを使うことで、コンポーネントを飛び越えて値を受け渡すことができます。
* コンテクストの生成
* コンテクストの受け渡し
* コンテクストの使用
の上記3点から成り立っています。

1つずつ確認していきましょう。
#### コンテクストの生成
```react:ExampleBasicContext.js
import { createContext } from "react";
// コンテクストの生成、コンポーネントの外部で定義
const ExampleContext = createContext('default-txt');
```
まず、`createContext`を使用して、コンテクストオブジェクトを生成します。
この`createContext`はコンポーネントの外部で定義します。

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

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

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

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

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

#### コンテクストの使用
```react: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`コンポーネントを別のコンポーネントで呼び出して画面描画しています。

<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/6bcf6135-5216-572f-da25-2a6d79ef0f7b.png'>

画面上には`provider-txt`が表示されているのがわかります。

ここで注目してほしいのは、**子コンポーネントから孫コンポーネントに対しては何も処理をしていない**ということです。
ただ単に呼び出しているだけですが、親コンポーネントで設定した`Provider`の値を孫コンポーネントが受け取っています。

これがコンテクストの大きな特徴です。
階層がどれだけ深くなろうと、値のリレーをすることなく、使用したい箇所で値を受け取ることができます。

## useContextを使いこなす
`useContext`を使うことで、コンポーネントを飛び越えてコンテクストをやり取りできることを確認しました。

ここからは`useContext`を使いこなすために深堀りしていきます。

### useContextのデフォルト値
コンテクストの作成時、`createContext`にデフォルト値を設定しました。
しかし、先ほど確認した例ではデフォルト値は登場していません。
どのようにして使うかを確認します。

```react: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コンポーネント内でコンテクストを使用しています。
画面は以下のように表示されます。

<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/7f4513d0-b773-5de1-c5ff-f9969a6bee38.png'>

OtherBodyでは`createContext`で設定したデフォルト値が表示されています。

基本的に、コンテクストを使用する場合には`Provider`で囲むので、デフォルト値の出番はあまりないかと思います。
`Provider`でラップされていないときにエラーとならないよう、フォールバックとして使うことが多いのかな、と感じました。

### コンテクストの上書き
コンテクストが受け取る`Provider`の値は、親要素をたどっていったときに、一番近い`Provider`の値が設定されます。

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

```react: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`を定義しています。

結果は以下のように表示されます。
<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/92e05b96-adca-3943-14f4-20f09574dbd0.png'>

2つめのExample Bodyは2つの`Provider`にラップされています。
そのうち、一番近い`Provider`である`reloaded-txt`が表示されています。
1つめのExample Bodyは今まで通り、`provider-txt`です。

このように`Provider`が階層構造になっている場合、`Provider`を使用している箇所から一番近いラップされた`Provider`を取得します。

今回はわかりやすいように同じコンポーネント内でラップしていますが、別コンポーネントで新たにラップした場合も同じです。
トップレベルのコンポーネントで`Provider`を提供しておき、一部の子コンポーネントでは別の`Provider`を使う、ということができます。

### コンテクストのインポート、エクスポート
`createComponent`で生成されたコンテクストは、オブジェクトであるため、他のオブジェクトと同様、インポートとエクスポートができます。

今までの例では1つのファイル（`ExampleBasicContext.js`）で確認してきましたが、通常はコンポーネントごとにファイルを用意するため、ファイルを分割してインポート、エクスポートについて確認します。

```react:generateContext.js
import { createContext } from "react";
// コンテクストの生成
export const ExampleContext = createContext('default-txt');
```
まず、コンテクストを生成、エクスポートするファイルを用意します。
値の受け渡し、使用はこのコンテクストをインポートして使用することになります。

では、値の受け渡しについて確認します。
```react: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`をインポートしています。
他の処理は今までと変わりません。

次に、コンテクストの使用について確認します。
```react: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;
```
`useContext`と`ExampleContext`をインポートしています。
コンテクストを使用する場合は、生成されたコンテクストをインポートして、`useContext`の引数として設定するだけです。
基本的な書き方は今まで確認してきた例と変わりません。

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

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

画面は以下のようになります。
<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/e00fcce5-790d-f411-2c08-219e46dcfabf.png'>
propsの問題点で示した表示とは若干異なりますが、ボタンを押下することでヘッダー内のテーマを切り替えられるようにしています。

ソースコードは長くなるので折りたたみ状態にしておきます。
<details><summary>ソースコード詳細</summary>

```react: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`は別々に定義します。

```react: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;
```
テーマのコンテクストを受け取って表示するようにしました。

```react: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`イベントに紐づけています。

```react:generateContext.js
import { createContext } from "react";
// コンテクストの生成
export const ExampleContext = createContext('light');
export const ExampleUpdateContext = createContext();
```
テーマ更新用のコンテクストを生成しています。初期値は設定していません。

</details>

ポイントは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**というライブラリがあるそうです。

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

## 参考にしたサイト
* [useContext - React](https://ja.react.dev/reference/react/useContext)
* [createContext - React](https://ja.react.dev/reference/react/createContext)