はじめに

Reactの非同期画面では、ローディング表示の置き方で使いやすさも保守性も変わる。
よくあるのは、ページ全体に大きなローディングを出す実装である。

しかし、画面の一部だけ待てばよいのに全体を止めると、ユーザーの操作を無駄に遮る。
逆に、細かくローディングを分けすぎると、どの状態がいつ表示されるのかレビューで追いづらくなる。

この記事では、Reactのローディング境界をレビューするときの判断軸を整理する。
中心は、何を待っていて、どこまで操作不能にするのかである。

まず止めたい実装

次のコードは、複数のデータ取得のどれか1つでも待機中ならページ全体をローディングにしている。

ページ全体を止める例
export function DashboardPage() {
  const user = useUserQuery();
  const notifications = useNotificationsQuery();
  const reports = useReportsQuery();

  if (user.isLoading || notifications.isLoading || reports.isLoading) {
    return <FullPageLoading />;
  }

  return (
    <DashboardLayout>
      <UserProfile user={user.data} />
      <NotificationList notifications={notifications.data} />
      <ReportPanel reports={reports.data} />
    </DashboardLayout>
  );
}

この実装では、通知の取得が遅いだけでプロフィールもレポートも表示されない。
レビューでは、画面全体を止める必然性があるか確認したい。

Comment
@Reviewer: すべての取得完了までページ全体をローディングにしているため、独立して表示できる領域まで待たされています。データ依存が独立しているなら、ローディング境界を領域ごとに分けてください。

ローディングは見た目ではなく境界設計

ローディング表示は、スピナーやSkeletonの選択だけではない。
レビューで見るべきなのは、待機中の境界である。

  • ページ全体を止めるのか
  • セクションだけ待たせるのか
  • 既存データを残しながら更新中表示にするのか
  • 入力やボタンを操作不能にするのか
  • 失敗時にどの範囲へエラーを出すのか

この境界が曖昧なままUIだけ作ると、読み込み中、更新中、空データ、エラーが混ざる。

レビュー観点1:初回読み込みと再取得を分けているか

初回読み込みではデータがない。
一方、再取得では古いデータを表示したまま更新中にできることが多い。

初回と再取得を分ける例
export function ReportPanelContainer() {
  const { data, isLoading, isFetching, error } = useReportsQuery();

  if (isLoading) {
    return <ReportPanelSkeleton />;
  }

  if (error) {
    return <ReportPanelError />;
  }

  return (
    <ReportPanel
      reports={data}
      isRefreshing={isFetching}
    />
  );
}

このように分けると、初回だけSkeletonを出し、再取得中は既存データを残せる。
レビューでは、isLoadingisFetching のような状態の意味が混ざっていないかを見る。

Comment
@Reviewer: 再取得中も初回ローディングと同じ表示にしているため、既存データが消えて画面がちらつきます。初回読み込みと更新中を分け、表示済みデータを残せるか検討してください。

レビュー観点2:Skeletonが実際のレイアウトと一致しているか

Skeletonは、単なる灰色の箱ではなく、読み込み後のレイアウトを予告する表示である。
実際のカード数や高さと大きく違うと、読み込み完了時に画面が跳ねる。

レビューでは次を確認する。

  • 読み込み後の主要レイアウトと近い形か
  • 高さが大きく変わらないか
  • 一覧件数が可変なら妥当な数を出しているか
  • ボタンや入力欄をSkeletonにしてよいか
  • 空データ表示とSkeletonが混ざっていないか

Skeletonが雑な場合、ローディング体験だけでなくレイアウト安定性も悪くなる。

レビュー観点3:操作不能範囲が広すぎないか

更新中にページ全体を覆うオーバーレイは、レビューで慎重に見たい。

操作不能範囲が広すぎる例
return (
  <div>
    {isSaving && <BlockingOverlay />}
    <ProfileForm />
    <RecentActivity />
    <HelpLinks />
  </div>
);

プロフィール保存中に、ヘルプリンクや最近の活動まで操作不能にする必要があるとは限らない。
保存対象のフォームだけをpendingにする方が自然なことが多い。

対象フォームだけをpendingにする例
<ProfileForm
  values={values}
  disabled={isSaving}
  onSubmit={submitProfile}
/>
<RecentActivity />
<HelpLinks />

レビューでは、待機中の副作用がどの範囲に影響するかを見る。
保存対象と無関係なUIまで止めているなら、境界を狭める余地がある。

レビュー観点4:Suspense境界の粒度が意味を持っているか

Suspenseを使う場合も、境界の考え方は同じである。

Suspense境界を分ける例
export function DashboardPage() {
  return (
    <DashboardLayout>
      <Suspense fallback={<UserProfileSkeleton />}>
        <UserProfile />
      </Suspense>

      <Suspense fallback={<NotificationSkeleton />}>
        <NotificationList />
      </Suspense>

      <Suspense fallback={<ReportPanelSkeleton />}>
        <ReportPanel />
      </Suspense>
    </DashboardLayout>
  );
}

境界を分けると、独立した領域を順に表示できる。
ただし、データ依存が強い領域を無理に分けると、片方だけ古い表示になる。

レビューでは次を確認する。

  • fallbackの単位が画面の意味単位と一致しているか
  • 親子で必要なデータ依存を分断していないか
  • Error Boundaryと対応しているか
  • 再試行時にどの境界を再読み込みするか分かるか

レビュー観点5:空データ、エラー、読み込み中を区別しているか

ローディング境界が曖昧な画面では、次の状態が同じ表示になりがちである。

状態が混ざる例
if (!items.length) {
  return <p>表示するデータがありません</p>;
}

items が空なのは、まだ読み込み中だからか、本当に0件だからか、失敗して空配列にしたからか分からない。

レビューでは、少なくとも次を分けて読む。

状態 表示
初回読み込み中 Skeletonやローディング
空データ 0件の理由と次の行動
エラー 再試行導線
再取得中 既存データ + 更新中表示

この区別がないと、障害時に「データがないだけ」に見えてしまう。

レビューコメント例

Comment
@Reviewer: 通知取得の遅延でページ全体がローディングになる構造です。プロフィールやレポートは独立して表示できそうなので、待機境界をセクション単位に分けてください。
Comment
@Reviewer: 初回読み込み、空データ、取得エラーが同じ表示に寄っています。ユーザーの次の行動が変わる状態なので、それぞれの表示と再試行導線を分けたいです。

まとめ

Reactのローディングレビューでは、スピナーかSkeletonかよりも、境界を見る。

  • 初回読み込みと再取得を分けているか
  • 独立して表示できる領域まで待たせていないか
  • 操作不能範囲が広すぎないか
  • Suspense境界が画面の意味単位と合っているか
  • 空データ、エラー、読み込み中を区別しているか

ローディングは一時的な表示ではある。
しかし境界が曖昧だと、画面全体の責務まで曖昧になる。レビューでは、何を待っているのかを読める形にすることが重要である。