2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

投稿目的

・備忘録
・よりよい方法をご教示いただきたい

はじめに

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) に分けて型を定義するようにしています。

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.loadingtrueの間はアプリケーションがLoading状態であることを表現したいとします。

LoadingContextの型を定義する

contextValuesetterそれぞれに格納するオブジェクトの型を定義していきます。

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で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の実装は完了です。

contextValuesetterそれぞれのみを取得できるhooksを作成する

現状作成したLoadingContextuseContextで取得する場合、以下のようになります。
しかし「contextValueのみ参照したい」「setterのみ参照したい」というユースケースも多いと思いますが、その際はsetter.setLoading(true)のように呼び出さなくてはならず、少々不便です。

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を使用します。

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

2
1
1

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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?