React Routerでアプリを作る際にload中のUIやsubmit中のUI(以下まとめてPending UI)を表示する実装にはだいたい2種類ある。
useNavigation()
を使ってナビゲーションの状態に応じてUIを出し分けるSuspense
を使って表示するデータがresolveされるまでfallback
属性のUIを出す
useNavigation
export default function Index() {
const navigation = useNavigation();
const isLoading = navigation.state === "loading";
return (
<main>
{isLoading ? <Skeleton /> : <Content>}
</main>
);
}
loader
やaction
が実行中であるかどうかを参照できる。- 同時に実行される
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
をシリアライズして送信できる。- クライアントに渡った
Promise
はuse
を使って値を取り出せる。use
を使うコンポーネントをSuspense
でラップすると、Promise
がresolveされるまでの間にSuspense
のfallback
で指定したコンポーネントを表示できる。
比較
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()
を使う。