TS×React Reactのcontextの実装パターンとcontextを操作しやすくするhooks

# 投稿目的
・備忘録
・よりよい方法をご教示いただきたい



# はじめに
Reactにおける[context](https://react.dev/reference/react/createContext)は、そのcontextの[`Provider`](https://react.dev/reference/react/createContext#provider)配下で参照できるグローバルな情報を保持する機能です。
例えば以下のように使えます。propsのバケツリレーをしなくても、`Provider`の子孫componentで情報を参照できます。

```TypeScript
// App.tsx
export const SomeContext: Context<string> = createContext<string>("some-context");

const App: FC = () => (
  <SomeContext.Provider value={"some-context"}>
    <Child1 />
  </SomeContext.Provider>
);

// Child1.tsx
export const Child1: FC = () => (
  <div>
    <Child2 />
  </div>
);

// Child2.tsx
export const Child2: FC = () => {
  const someContext: string = useContext<string>(SomeContext);

  return someContext; // "some-context"
};
```

また、`state`を組み合わせることで、子孫component側でcontextの値を操作することもできます。
その際に私が用いている仕様などを本記事で共有させていただきます。



# 実装
### contextの型情報を統一しよう
先述の例のように参照のみ可能なcontextはあまり意味がない (普通に変数をexportすれば事足りる) ので、contextで扱う情報は`state`で定義し操作可能とします。
その際、contextの値 (`contextValue` )と操作関数 (`setter`) に分けて型を定義するようにしています。
```BaseContext.d.ts
/**
 * 本アプリケーションで用いられるContextの基底となる型 \
 * contextValueには参照する値、setterにはcontextValueの中身を操作する関数を格納する
 * @interface
 */
export interface BaseContext<Value, Setter> {
  /**
   * contextで管理するstateの値を格納するオブジェクト
   * @type {Value}
   */
  contextValue: Value;

  /**
   * contextで管理するstateのsetterを格納するオブジェクト
   * @type {Setter}
   */
  setter: Setter;
}
```


### Loading状態の`boolean`をcontextで管理する
`LoadingContext`を作成し、`LoadingContext.contextValue.loading`が`true`の間はアプリケーションがLoading状態であることを表現したいとします。

##### ①`LoadingContext`の型を定義する
`contextValue`と`setter`それぞれに格納するオブジェクトの型を定義していきます。

```LoadingContext.d.ts
/**
 * LoadingContextの値の型
 * @interface
 */
export interface LoadingContextValue {
  /**
   * loading状態か
   * @type {boolean}
   */
  loading: boolean;
}

/**
 * LoadingContextの値を操作する関数を格納するオブジェクトの型
 * @interface
 */
export interface LoadingContextSetter {
  /**
   * loading状態のsetter
   * @param {boolean} loading
   * @returns {void}
   */
  setLoading: (loading: boolean) => void;
}
```

##### ②`React.createContext`でcontextを作成する

```LoadingContext.ts
import { createContext, type Context } from "react";
import type { BaseContext } from "types/contexts/BaseContext";
import type { LoadingContextSetter, LoadingContextValue } from "types/contexts/context/LoadingContext";

/**
 * LoadingContextのデフォルト値
 * @readonly
 * @type {BaseContext<LoadingContextValue, LoadingContextSetter>}
 */
const defaultLoadingContext: BaseContext<LoadingContextValue, LoadingContextSetter> = {
  contextValue: { loading: false },
  setter: { setLoading: () => {} }
} as const;

/**
 * loading状態を保存するcontext
 * @type {Context<BaseContext<LoadingContextValue, LoadingContextSetter>>}
 */
export const LoadingContext: Context<BaseContext<LoadingContextValue, LoadingContextSetter>> =
  createContext<BaseContext<LoadingContextValue, LoadingContextSetter>>(defaultLoadingContext);
```

##### ③`LoadingContext.Provider`に値を供給するためのhooksを作成する
[`useCallback`](https://react.dev/reference/react/useCallback)でstateの操作関数をラップすることで、contextを参照しているcomponentで再レンダリングが発生した場合でも`setter`内の関数が再生成されることを防ぐことができます。

```UseLoadingContext.ts
import { useCallback, useState } from "react";
import type { BaseContext } from "types/context/BaseContext";
import type { LoadingContextValue, LoadingContextSetter } from "types/contexts/context/LoadingContext";

/**
 * LoadingContextをProviderに提供するためのhooks(Provider以外では用いない)
 * @returns {BaseContext<LoadingContextValue, LoadingContextSetter>}
 */
export const useLoadingContext = (): BaseContext<LoadingContextValue, LoadingContextSetter> => {
  const [loading, setLoading] = useState<boolean>(false);

  /**
   * loadingを更新する関数
   * useCallbackを用いることで、context更新時にaddAlertObjectが再生成されることを防ぎ、useEffect(() => callback(), [setLoading]); の不要な実行を回避できる
   * @param {boolean} loading
   * @return {void}
   */
  const setCurrentLoading = useCallback((loading: boolean) => setLoading(loading), []);

  return { contextValue: { loading }, setter: { setLoading: setCurrentLoading } };
};

```

##### ④`LoadingContext.Provider`をrenderする
既に`<ContextsProvider />`はアプリケーション内でrenderされているものとします。

```ContextsProvider.tsx
import { memo, type FC, type ReactNode } from "react";
import { LoadingContext } from "contexts/context/LoadingContext";
import { useLoadingContext } from "hooks/contexts/contexts/UseLoadingContext";
import type { BaseContext } from "types/contexts/BaseContext";
import type { LoadingContextValue, LoadingContextSetter } from "types/contexts/context/LoadingContext";

/**
 * ContextsProviderのProps
 * @interface
 */
interface ContextsProviderProps {
  /**
   * 子要素
   * @type {ReactNode}
   */
  children: ReactNode;
}

/**
 * Layout.tsxにて使用。配下のcomponentsにおいて、contextを使用できるようにする
 * @see https://react.dev/reference/react/createContext#createcontext
 * @type {FC<ContextsProviderProps>}
 */
export const ContextsProvider: FC<ContextsProviderProps> = memo(({ children }) => {
  const loadingContext: BaseContext<LoadingContextValue, LoadingContextSetter> = useLoadingContext();

  return (
    <LoadingContext.Provider value={loadingContext}>
      {children}
    </LoadingContext.Provider>
  );
});

```

以上で`LoadingContext`の実装は完了です。


### `contextValue`と`setter`それぞれのみを取得できるhooksを作成する
現状作成した`LoadingContext`を[`useContext`](https://react.dev/reference/react/useContext)で取得する場合、以下のようになります。
しかし「`contextValue`のみ参照したい」「`setter`のみ参照したい」というユースケースも多いと思いますが、その際は`setter.setLoading(true)`のように呼び出さなくてはならず、少々不便です。

```TypeScript
const { contextValue, setter } = 
  useContext<BaseContext<LoadingContext, LoadingContextSetter>>(LoadingContext);
```

そのため、`contextValue`のみを取得する`useContextValue`と、`setter`のみを取得する`useSetContext`を作成します。

```UseContextValue.ts
import { useContext, type Context } from "react";
import type { BaseContext } from "types/contexts/BaseContext";

/**
 * 本プロジェクトで用いるcontextの値部分のみを取得するhooks
 * @param {Context<BaseContext<Value, Setter>>} context {なぜかSetter=unknownではts(2345)エラーでの型明示を強要されるので、ジェネリクスにSetterを追加}
 * @returns {Value}
 */
export const useContextValue = <Value, Setter>(context: Context<BaseContext<Value, Setter>>): Value => {
  return { ...useContext<BaseContext<Value, Setter>>(context).contextValue };
};
```

```UseSetContext.ts
import { useContext, type Context } from "react";
import type { BaseContext } from "types/contexts/BaseContext";

/**
 * 本プロジェクトで用いるcontextのsetter部分のみを取得するhooks
 * @param {Context<BaseContext<Value, Setter>>} context {なぜかValue=unknownではts(2345)エラーで型明示を強要されるので、ジェネリクスにValueを追加}
 * @returns {Setter}
 */
export const useSetContext = <Value, Setter>(context: Context<BaseContext<Value, Setter>>): Setter => {
  return { ...useContext(context).setter };
};
```


### 画面で使ってみる
Loading状態を示すスピナーを表示するcomponentでは、`LoadingContext.contextValue.loading`のみを参照します。
この際、`useContextValue`を使用します。

```LoadingSpinner.tsx
import { memo, type FC } from "react";
import { LoadingMask } from "components/LoadingMask";
import { LoadingContext } from "contexts/context/LoadingContext";
import { useContextValue } from "hooks/contexts/UseContextValue";
import type { LoadingContextSetter, LoadingContextValue } from "types/contexts/context/LoadingContext";

/**
 * LoadingContextをもとに、`<LoadingMask />`の切り替えを行うcomponent
 * @type {FC}
 */
export const LoadingSpinner: FC = memo(() => {
  const { loading } = useContextValue<LoadingContextValue, LoadingContextSetter>(LoadingContext);

  return loading ? <LoadingMask /> : null;
});
```

データを`fetch`するcomponentでは、`LoadingContext.setter.setLoading`のみを使用します。
この際、`useSetContext`を使用します。

```TypeScript
import { memo, useEffect, useState, type FC } from "react";
import { useSetContext } from "hooks/contexts/UseSetContext";
import type { LoadingContextSetter, LoadingContextValue } from "types/contexts/context/LoadingContext";

const Component: FC = memo(() => {
  const [data, setData] = useState(null);
  const { setContext } = useSetContext<LoadingContextValue, LoadingContextSetter>(LoadingContext);

  useEffect(() => 
    axios.get("https://somepage.com")
      .then((res) => {
        setLoading(true);
        setData(res.json());
      })
      .finally(() => setLoading(false));
  );

  return data || null:
});
```



# 終わりに
いかがでしたでしょうか。
修正すべき点などございましたらご教示いただけますと幸いです m(_ _)m