【React + Jotai】状態管理でスマートにローディング画面を切り替えよう！

# はじめに
こんにちは。<a href="https://x.com/Kei_dev_1213">Kei_dev_1213</a>と申します！
タイトルの通り、本記事ではJotaiというライブラリを使って、ボタン押下時などにオシャレなローディングオーバレイの画面を表示させる方法について共有したいと思います！

# 前提

使用技術は以下のとおりです。

| ライブラリ名        | バージョン      |
|---------------------|-----------------|
| **React**           | 19.0.0         |
| **Jotai**           | 2.12.3         |
| **Vite**            | 6.3.1          |
| **Tailwind CSS**    | 4.1.5          |
| **Framer Motion**   | 12.9.4         |

### 修正前

まずは説明をするにあたって、修正前の画面の挙動とソースを見てみましょう。
今回はViteでReactプロジェクトを作成しています（構築の手順は省略）。

#### 画面の挙動
![record.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3832049/045fa344-480b-4026-9178-02bddb7a029d.gif)
こちら、「画面遷移」ボタンをクリックすると、2秒後に別ページに遷移するだけの超簡易ページとなります。次にソースを見てみましょう↓。

#### ソース（修正前）

```diff_tsx:App.tsx（page1とpage2の親コンポーネント）
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Page1 from "./Page1";
import Page2 from "./Page2";

function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Page1 />} />
          <Route path="/page2" element={<Page2 />} />
          <Route path="*" element={<h1>Not Found Page</h1>} />
        </Routes>
      </BrowserRouter>
    </>
  );
}

export default App;

```

```diff_tsx:page1.tsx（遷移前）
import { useNavigate } from "react-router-dom";
import { Button } from "./components/ui/button";

const Page1 = () => {
  const navigate = useNavigate();

  const handleButton = () => {
    setTimeout(() => {
      navigate("/page2");
    }, 2000);
  };

  return (
    <>
      <Button className="cursor-pointer" onClick={handleButton}>
        画面遷移
      </Button>
    </>
  );
};

export default Page1;
```

```diff_tsx:page2.tsx（遷移後）
const Page2 = () => {
  return <div>Page2</div>;
};

export default Page2;
```
遷移前ページでは、「画面遷移」ボタンをクリックするとsetTimeoutで2秒後に画面を遷移させています。遷移後のページは特に何もしていませんね。
ただ、このままではボタンクリック時にローディングしているのかしていないのか分からないので、今回はボタンクリック時に画面全体を覆うオシャレなローディング画面を表示させるように改修してみましょう。

# 修正後

では、まずは修正後の画面の挙動をお見せします↓。

![record.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3832049/6ba98424-4915-4bce-ba51-5623c1aee1a0.gif)

いかがでしょうか。超オシャレになりましたね！
こちらの機能は、実はJotaiを使うと、思いの外簡単に実装することができます。
（Jotaiとは一言でいうと、`「atom（アトム）」という最小単位ごとに状態を分割して様々なコンポーネント間でシンプルに管理できる、軽量な状態管理ライブラリ`です。詳細は是非以下の記事を参照してください。）

**↓Jotaiの詳細記事**

https://qiita.com/Sicut_study/items/ee41f1bb59fcda45a50b

今回の修正方針としては、`画面のローディングコンポーネントの表示フラグをJotaiで管理し、page1のボタンをクリックしたタイミングで表示フラグをtrueに変更、遷移後のpage2の初期処理で表示フラグをfalseに変更する方式`としましょう。また、Jotaiの管理をカスタムフックにすることで、他のページでも簡単にフラグを変更できるようにすれば汎用性が高くなりそうですね。

#### ソース（修正後）

まず、画面に表示するおしゃれなローディング画面のコンポーネントを作成します。
これは、今回は<a href="https://v0.dev/">v0</a>（AIベースのUI生成ツール）を使うこととします。これを使うと、テキストで「○○なUIを作って」と指示するだけで、AIが即座にUIを生成してくれます。使わないのは超絶もったいないので、ご存じない方はぜひ使ってみましょう。
<br/>

<details><summary>v0でのUI生成</summary>
<br/>
v0に投げるプロンプトは割と適当でもしっかりしたUIを生成してくれます。
今回は、以下のプロンプトを投げてみました。

```:プロンプト
画面全体を覆うカラフルでオシャレなローディングコンポーネントを作成してください。
コンポーネントの引数で、ローディングの表示有無のフラグを渡すようにしてください。
```

```tsx:loading-overlay.tsx
import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils"

interface LoadingOverlayProps {
  isLoading?: boolean
  className?: string
}

export function LoadingOverlay({ isLoading = true, className }: LoadingOverlayProps) {
  return (
    <AnimatePresence>
      {isLoading && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.5, ease: "easeInOut" }}
          className={cn(
            "fixed inset-0 z-50 flex items-center justify-center bg-gray-200/70 backdrop-blur-sm",
            className,
          )}
        >
          <motion.div
            initial={{ y: 20, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            transition={{ delay: 0.2, duration: 0.4, ease: "easeOut" }}
            className="flex flex-col items-center"
          >
            <div className="relative h-16 w-16">
              {/* 外側の円 */}
              <div className="absolute inset-0 animate-spin">
                <div className="h-full w-full rounded-full border-4 border-transparent border-t-pink-500 border-r-blue-500 border-b-yellow-500 border-l-green-500" />
              </div>

              {/* 内側の円 */}
              <div className="absolute inset-2 animate-pulse">
                <div className="h-full w-full rounded-full border-4 border-transparent border-t-purple-500 border-r-teal-500" />
              </div>

              {/* 中心の点 */}
              <div className="absolute inset-0 flex items-center justify-center">
                <div className="h-3 w-3 animate-ping rounded-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500" />
              </div>
            </div>
            <motion.p
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              transition={{ delay: 0.4, duration: 0.3 }}
              className="mt-4 text-sm font-medium bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
            >
              読み込み中...
            </motion.p>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

```

↑のソースを見ると何やら凄そうですが、プロンプトで指定した通り、isLoadingで表示/非表示を切り替えてくれていそうです。
また、こちらはshadcn（UIコンポーネントを提供するツール）とframer-motion（react向けのアニメーションライブラリ）を使っているようなので、合わせてプロジェクトに組み込むことにしましょう。

```:framer-motionのインストール
npm i framer-motion
```

shadcnについては、<a href="https://ui.shadcn.com/docs/installation/vite">こちら</a>を参照してください。
なお、v0では無料枠で1日の生成回数の上限が決まっているようなのでご注意ください（1日10回？）。また、使用するライブラリについてもプロンプトの中で「これを使って/使わないで」と指定することでその通りにしてくれるようです。本当にすごいですね！
</details>

<br/>

それでは、まずは以下の通りJotaiのインストールを行います。

```:Jotaiのインストール
npm install jotai
```

次に、以下の通りローディングコンポーネントの表示切り替えを行うカスタムフックを作成します。
```tsx:loading/index.tsx
import { atom, useAtom } from "jotai";
import { useEffect } from "react";

// atom
export const isLoadingOverlayAtom = atom(false);

export const useLoadingOverlay = () => {
  const [isLoadingOverlay, setIsLoadingOverlay] = useAtom(isLoadingOverlayAtom);

  // 初期表示時にローディング解除
  useEffect(() => {
    setIsLoadingOverlay(false);
  }, []);

  return { isLoadingOverlay, setIsLoadingOverlay };
};
```
`Jotaiでは、状態管理を行いたい変数をatom関数（引数は初期値）で宣言`します。
今回は、ローディングの表示有無を管理したいということで、初期値にfalseを指定します。
useAtomにatomを渡すことにより、指定した状態を読み書きできます（useAtomはuseStateと同じノリで扱うことができます。分かりやすいですね！）ので、これらをreturnすることで、このフックを使用すればローディングの表示有無を切り替えられることとなります！

また、useEffectでローディングを解除していますが、これは、useLoadingOverlayを宣言するだけでローディング画面を非表示にするために記述しています。詳細は後ほど見てみましょう。


```diff_tsx:App.tsx（page1とpage2の親コンポーネント）
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Page1 from "./Page1";
import Page2 from "./Page2";
+ import { LoadingOverlay } from "./LoadingOverlay";
+ import { useLoadingOverlay } from "./loading";

function App() {
+  const { isLoadingOverlay } = useLoadingOverlay();

  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Page1 />} />
          <Route path="/page2" element={<Page2 />} />
          <Route path="*" element={<h1>Not Found Page</h1>} />
        </Routes>
      </BrowserRouter>
+      <LoadingOverlay isLoading={isLoadingOverlay} />
    </>
  );
}

export default App;
```
親コンポーネントでは、v0で作成したローディングコンポーネント（LoadingOverlay）を組み込んでいます。また、上で作成したローディング用カスタムフックからJotaiで管理しているフラグを取得して渡すことで、他コンポーネントからローディング表示有無を切り替えられるようにしておきます。
<br/>

```diff_tsx:page1.tsx（遷移前）
import { useNavigate } from "react-router-dom";
import { Button } from "./components/ui/button";
+ import { useLoadingOverlay } from "./loading";

const Page1 = () => {
+  const { setIsLoadingOverlay } = useLoadingOverlay();
  const navigate = useNavigate();

  const handleButton = () => {
+    setIsLoadingOverlay(true);
    setTimeout(() => {
      navigate("/page2");
    }, 2000);
  };

  return (
    <>
      <Button className="cursor-pointer" onClick={handleButton}>
        画面遷移
      </Button>
    </>
  );
};

export default Page1;
```
遷移前の画面では、ボタンクリック時にローディングを表示したいので、カスタムフックからJotaiで管理している切り替えフラグの設定用関数を呼び出してtrueを設定します。これにより、「画面遷移」ボタンをクリックすることで画面全体にローディング画面が表示されることとなります。


```diff_tsx:page2.tsx（遷移後）
+ import { useLoadingOverlay } from "./loading";

const Page2 = () => {
+  useLoadingOverlay();

  return <>Page2</>;
};

export default Page2;
```
遷移後の画面では、ローディング用カスタムフックを読み込んでいますが、遷移後の画面に関してはこれだけでOKです。なぜなら、カスタムフック内のuseEffectでローディングを非表示にする設定を行っているためですね。この方法で実装すれば、page3や4が追加されても簡単にローディングの画面表示を実装できますね！

# おわりに
いかがだったでしょうか。
見た目の割に思いの外簡単にできたという印象を持っていただけたのではないかと思います。特に、v0を使うと、時間をかけずにリッチなUIコンポーネントを実装できますので、ご存じない方はぜひお使いいただければと思います（一昔前からすると魔法のようですね）。
githubも公開しますので、よろしければこちらもぜひご活用ください！↓

https://github.com/Kei-dev-1213/sample-loading

## JISOUのメンバー募集中🔥
プログラミングコーチングJISOUではメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか？
気になる方はぜひHPからライン登録お願いします！👇

https://projisou.jp/