投稿目的
・備忘録
・よりよい方法をご教示いただきたい
はじめに
Reactにおけるcontextは、そのcontextのProvider
配下で参照できるグローバルな情報を保持する機能です。
例えば以下のように使えます。propsのバケツリレーをしなくても、Provider
の子孫componentで情報を参照できます。
// 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
) に分けて型を定義するようにしています。
/**
* 本アプリケーションで用いられる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の値の型
* @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を作成する
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
でstateの操作関数をラップすることで、contextを参照しているcomponentで再レンダリングが発生した場合でもsetter
内の関数が再生成されることを防ぐことができます。
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されているものとします。
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
で取得する場合、以下のようになります。
しかし「contextValue
のみ参照したい」「setter
のみ参照したい」というユースケースも多いと思いますが、その際はsetter.setLoading(true)
のように呼び出さなくてはならず、少々不便です。
const { contextValue, setter } =
useContext<BaseContext<LoadingContext, LoadingContextSetter>>(LoadingContext);
そのため、contextValue
のみを取得するuseContextValue
と、setter
のみを取得するuseSetContext
を作成します。
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 };
};
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
を使用します。
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
を使用します。
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