🔄

【React】Contextを使用してカスタムのローディングUIを作成する

2025/01/20に公開

画面全体を覆って画面中央に出るタイプのローディングを作ってみようと思ったのですが、どうやって実装しようか悩んでいた時に良さそうな方法を見つけたのでまとめてみました。

はじめに

最近Next.jsで開発をしていますが、そもそもNext.jsではローディングUIを実装する時はデフォルトのローディングUIが備えられています。ですが、正直これは画面の初期描画時にしか表示されないので使い勝手が悪いです。
そのため独自でカスタムのローディングUIを用意して、ボタン押下時の待機中などでそれを使うようにしたいです。
なるべくコードを最小限にしたいので、標準機能を使って実装したいところですが、その時にReact HooksのContextが有用そうです。

ということで、今回はContextを使ってカスタムのローディングUIを作っていこうと思います。

実行環境

バージョン
React 19.0.0-rc-02c0e824-20241028
Next 15.0.2
tailwindcss 3.4.1

Contextとは?

まず、Contextが何なのかを少し説明しておきます。

Contextとは、Reactが用意しているフックの1種です。
Reactでは通常、データを伝播するときは、親コンポーネントから子コンポーネントにpropsで渡していくかと思います。
ですが、Contextを使えば、逆に子コンポーネントから親コンポーネントへデータを伝播させることができます。
ローディング処理のような、複数のコンポーネントから共通処理として使いたい場合に便利そうです。

もっと詳細を知りたい方は、Reactの公式ドキュメントを参照してください。
https://ja.react.dev/learn/passing-data-deeply-with-context

Contextでローディング処理を実装

1. LoadingContextを作成する

基本となるContextのクラスを作成していきます。
LoadingContext.Providerについてですが、Providervalueというプロパティを持ち、これに渡された値をコンポーネントツリー内のuseContextフックを介して取得できます。
要はここで、isLoaingの値の変更を監視しています。

contexts/LoadingContext.tsx
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';

const LoadingContext = createContext({
  isLoading: false,
  setLoading: (_loading: boolean) => {},
});

export const LoadingProvider = ({ children }: { children: ReactNode }) => {
  const [isLoading, setLoading] = useState(false);

  return (
    <LoadingContext.Provider value={{ isLoading, setLoading }}>{children}</LoadingContext.Provider>
  );
};

export const useLoading = () => useContext(LoadingContext);

上記では、useContextフックをuseLoadingとして提供しています。

2. ローディングコンポーネントを作成する

ローディングコンポーネント側でuseLoadingをインポートしてisLoadingの値を見ています。
ローディングコンポーネントは、isLoadingtrueならローディングの要素を返し、falseならnullを返すように作成しています。

components/ui/loading.tsx
'use client';
import { useLoading } from '@/contexts/LoadingContext';

const GlobalLoading = () => {
  const { isLoading } = useLoading();
  return isLoading ? (
    <div className="fixed inset-0 flex items-center justify-center w-full h-screen z-10">
      <svg
        aria-hidden="true"
        className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
        viewBox="0 0 100 101"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
          fill="currentColor"
        />
        <path
          d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
          fill="currentFill"
        />
      </svg>
    </div>
  ) : null;
};

export default GlobalLoading;

3. App全体でLoadingProviderを使用する

今回は、画面全体を覆う形でローディングを表示したいので、Layout.tsに記述します。

app/layout.tsx
...
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <LoadingProvider>
          <GlobalLoading />
          {children}
        </LoadingProvider>
      </body>
    </html>
  );
}

4. 各コンポーネントからローディング処理を呼び出す

各コンポーネントからは下記のようにローディング処理を呼び出せます。

'use client';
...
import { useLoading } from '@/contexts/LoadingContext';
...
export default component() => {
...
  const [state, formAction, isPending] = useActionState(serverAction, initialState);
  const { setLoading } = useLoading();
...
  useEffect(() => {
    setLoading(isPending);
  }, [isPending, setLoading]);
...

まとめ

Contextを使えば、今回のローディングUIのような共通処理を簡単に実装できました。
他にも実装に使えそうな場面だとか共通処理がありそうな気がしているので、上手く使っていきたいと思います。

Discussion