Reactにおけるレースコンディションの解決: 二段階レンダリングパターンの実装

## はじめに

Reactで非同期データを扱うアプリケーションを開発していると、「レースコンディション」と呼ばれる問題に遭遇することがあります。この記事では、求人サイトのブックマーク機能で発生したレースコンディションの問題と、その解決策として実装した「二段階レンダリング」パターンについて解説します。

## 問題の概要

ブックマークページにアクセスすると、以下のような表示の遷移が発生していました：

1. ローディング表示
2. 「ブックマークした求人はありません」という表示（一瞬だけ表示される）
3. 実際のブックマークした求人の表示

この「ちらつき」は、ユーザー体験を損なう問題でした。

## レースコンディションとは

レースコンディションとは、複数の非同期処理が実行され、それらの完了順序が予測できない状況を指します。今回の問題では、以下の2つの非同期処理が関係していました：

1. セッション情報の取得（useSession）
2. ブックマーク情報の取得（useBookmarks）

これらの処理は以下のような依存関係を持っていました：

- ブックマークコンテキストは、ユーザー情報に依存している
- ユーザー情報は、セッション情報から取得される

コードで表すと、ブックマークコンテキストは以下のような実装になっていました：
```jsx
useEffect(() => {
  async function fetchBookmarks() {
    try {
      if (user) {
        // ログインユーザーの場合はAPIからブックマーク情報を取得
        const response = await fetch(`/api/users/${user.id}/bookmarks`);
        // ...データ取得処理
      } else {
        // 非ログインユーザーの場合はローカルストレージから取得
        // ...
      }
    } catch (error) {
      // エラー処理
    } finally {
      setIsLoading(false);
    }
  }

  fetchBookmarks();
}, [user]); // user を依存配列に指定
```
## 問題の発生メカニズム

問題が発生する流れは以下の通りでした：

1.ページ初期ロード時：
   - セッション情報は "loading" 状態
   - ブックマークコンテキストは初期値の空配列 `[]` を返す

2.セッション情報の取得完了：
   - `status` が "authenticated" に変わる
   - `userId` が利用可能になる

3.ブックマークコンテキストの更新：
   - ユーザーIDを使ってブックマーク情報をAPIから取得
   - `bookmarkedJobs` が実際のブックマークIDの配列に更新される

4.UI表示の問題：
   - ステップ1の空配列に基づいて「ブックマークがない」と表示
   - ステップ3の完了後、実際のブックマークが表示される
   - この急な切り替えがちらつきとして認識される

![問題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2590850/8673f9e7-c05f-4147-9d8a-6d71ccb78162.png)



## 解決策：二段階レンダリングパターン

この問題を解決するために、「二段階レンダリング」パターンを実装しました。このパターンでは、すべての必要なデータが揃うまでは初期化中の状態を維持し、完全なデータが揃った後に本来のUIをレンダリングします。

![解決策.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2590850/fd00cb45-1210-40fb-8ab1-d058a0cd4c7f.png)


実装のポイントは以下の通りです：

1.**初期化フラグの導入**：
   ```jsx
   const [initialized, setInitialized] = useState(false);
   ```

2.**遅延初期化の実装**：
   ```jsx
   useEffect(() => {
     if (status !== "loading") {
       // セッション情報の取得が完了したら、少し遅延を入れて初期化完了とする
       const timer = setTimeout(() => {
         setInitialized(true);
       }, 500);
       
       return () => clearTimeout(timer);
     }
   }, [status]);
   ```

3.**条件付きレンダリングの実装**：
   ```jsx
   // 初期化前はローディング表示
   if (!initialized) {
     return (
       <>
         <Navbar />
         <main>
           <div>
             <h1>{isOwner ? "マイブックマーク" : "ブックマーク一覧"}</h1>
             <div className="loading-spinner">
               <LoadingSpinner />
             </div>
           </div>
         </main>
         <Footer />
       </>
     );
   }
   ```

4.**データ取得の最適化**：
   ```jsx
   useEffect(() => {
     // 初期化が完了していない場合は何もしない
     if (!initialized) return;
     
     async function fetchJobsData() {
       // データ取得処理
     }

     fetchJobsData();
   }, [bookmarkedJobs, initialized]);
   ```

## 解決策の効果

この二段階レンダリングパターンを実装することで、以下の効果が得られました：

1. ちらつきの解消：「ブックマークがない」という表示が一瞬だけ表示される問題が解消されました
2. 一貫したユーザー体験：ユーザーは常に正確な情報を見ることができるようになりました
3. 予測可能な表示：データの取得状態に関わらず、UIの表示が予測可能になりました

## なぜ0.5秒の遅延が必要なのか

解決策では0.5秒の遅延を導入していますが、これには以下の理由があります：

1. **非同期処理の完了を待つため**：セッションの読み込みが完了しても、ブックマークコンテキストの初期化はまだ進行中かもしれません
2. **レンダリングサイクルの同期**：Reactのレンダリングサイクルとデータ取得のタイミングを同期させるためのバッファとして機能します
3. **ネットワーク遅延への対応**：APIからのデータ取得に時間がかかる可能性があります

この遅延は「安全マージン」として機能しており、すべての非同期処理が確実に完了する時間を確保しています。

## まとめ

Reactアプリケーションでは、複数の非同期データソースに依存するコンポーネントを扱う場合、レースコンディションに注意する必要があります。二段階レンダリングパターンは、このような問題を解決するための効果的なアプローチです。

このパターンの本質は、「不完全なデータに基づく中間状態を表示しない」という考え方にあります。ユーザーには、データが完全に揃うまでローディング状態を見せ、すべての準備が整ってから実際のコンテンツを表示することで、一貫性のあるユーザー体験を提供することができます。

React開発において、非同期データの取得と表示のタイミングを適切に管理することは、高品質なアプリケーションを構築する上で非常に重要なスキルです。