React RouterにおけるPending UIの実装方針

React Routerでアプリを作る際にload中のUIやsubmit中のUI(以下まとめてPending UI)を表示する実装にはだいたい2種類ある。

  1. useNavigation()を使ってナビゲーションの状態に応じてUIを出し分ける
  2. Suspenseを使って表示するデータがresolveされるまでfallback属性のUIを出す

useNavigation

export default function Index() {
  const navigation = useNavigation();
  const isLoading = navigation.state === "loading";

  return (
    <main>
      {isLoading ? <Skeleton /> : <Content>}
    </main>
  );
}
  • loaderactionが実行中であるかどうかを参照できる。
  • 同時に実行されるloaderのうち、ひとつでも実行中であれば"loading"のまま。

Suspense

export function loader() {
  const usersPromise = fetch("https://example.com/users");
  return { usersPromise };
}

export default function Index({ loaderData }: Route.LoaderArgs) {
  return (
    <main>
      <table>
        <thead>
          <tr>
            <th>name</th>
            <th>email</th>
          </tr>
        </thead>
        <Suspense fallback={<UserRowsSkeleton />}>
          <UserRows users={loaderData.usersPromise} />
        </Suspense>
      </table>
    </main>
  );
}

function UserRows({ users }: { users: Promise<User[]> }) {
  const resolved = use(users);

  return (
    <tbody>
      {resolved.map((user) => (
        <tr key={user.id}>
          <td>{user.name}</td>
          <td>{user.email}</td>
        </tr>
      ))}
    </tbody>
  );
}
  • loaderからコンポーネントのpropsにPromiseを渡している。Single Fetchという仕組みでサーバーからクライアントにPromiseをシリアライズして送信できる。
  • クライアントに渡ったPromiseuseを使って値を取り出せる。useを使うコンポーネントをSuspenseでラップすると、Promiseがresolveされるまでの間にSuspensefallbackで指定したコンポーネントを表示できる。

比較

useNavigation

  • pros: 即座にPending UIを表示できる。
  • pros: submit時にも使える。
  • cons: どれかひとつでもloaderが実行中だとローディング中になるため、細やかなUIには向いていない。

Suspense

  • pros: データの取得に時間がかかる部分だけローディングUIを出すといったことができる。
  • cons: Promiseが返す値やthrowする値がSingle Fetchによってシリアライズできないケースでは使えない。特にResponseをthrowするケースではエラーになることを確認した。

使い分け

  • useNavigation()でレイアウトでページ全体のPending UIを表示しつつ、特に取得に時間がかかる箇所にSuspenseで個別のPending UIを表示する。
  • submit中のPending UIにはuseNavigation()を使う。