use()をuseしてみる
Reactの use()
に関してドキュメントが用意された
2024/01/18 現在、Canary or Experimental で使えるので、つまり Next.js では使える。
実際のところ use()
で何ができるのかを確認しておく
SuspenseやErrorBoundaryとの協調
基本的なケース。
SuspenseやErrorBoundaryで、ロード中やエラー時のフォールバックに簡単に対応できる。
クライアントコンポーネントでプロミスを作成するよりも、サーバコンポーネントでプロミスを作成してそれをクライアントコンポーネントに渡すようにしてください。クライアントコンポーネントで作成されたプロミスは、レンダーごとに再作成されます。サーバコンポーネントからクライアントコンポーネントに渡されたプロミスは、再レンダー間で不変です。こちらの例を参照してください。
仮にWebAPIの実行を待ちたい場合、単純に Client Components から直接APIを呼び出し、そのPromiseを use()
に放り込めばOKかと言われたらそんなことはない。↑にある通り、Promiseが該当のClient Compoentns 内でレンダーごとに生成されるため、特に何も考えずに実装すると、何度もAPIが実行される。
Next の場合、Server Componentsでasync/awaitを使わずにPromiseを取得し、それをClientComponentsにPropsとして渡すことで処理できる。
export type FetchUserResponse = { name: string };
// API実行想定の関数
// (一定時間後にResolveとなるPromiseを返す)
export const fetchUser = (): Promise<FetchUserResponse> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: "mugi" });
}, 3000);
});
};
"use client";
import { use } from "react";
import { FetchUserResponse } from "./fetchUser";
export const User = ({
fetchUserPromise,
}: {
fetchUserPromise: Promise<FetchUserResponse>;
}) => {
const user = use(fetchUserPromise);
return <>{user.name}</>;
};
import { Suspense } from "react";
import { User } from "./User";
import { fetchUser } from "./fetchUser";
export default function Page() {
// Promiseを得る
const fetchUserPromise = fetchUser();
return (
<Suspense fallback="loading...">
<User fetchUserPromise={fetchUserPromise} />
</Suspense>
);
}
Server Components (page.tsx) から Client Components (User) に Promise を Props として渡してることがわかる。
これで、Suspenseと連動して動く
Next.js での page 単位での自動的な Suspense および ErrorBoundary と組み合わせることもできるぽい。
たとえば、次のように loading.tsx を用意する
export default function Loading() {
return "loading...(loading.tsx)";
}
そして page.tsx を次のように変更する(Suspenseを消す)
export default function Page() {
// Promiseを得る
const fetchUserPromise = fetchUser();
return <User fetchUserPromise={fetchUserPromise} />;
}
うまく動く
error.tsx もいける。
APIのほうでrejectするようにする
export const fetchUser = (): Promise<FetchUserResponse> => {
return new Promise((_, reject) => {
setTimeout(() => {
reject({});
}, 3000);
});
};
そして page.tsx の横に error.tsx を置く
"use client";
export default function Error() {
return <>error!!</>;
}
バッチリ
使い所
これって別に Server Component で await しちゃえばいいのでは?という気持ちになる。
Should I resolve a Promise in a Server or Client Component?
のあたりを見るとわかりやすい
But using await in a Server Component will block its rendering until the await statement is finished. Passing a Promise from a Server Component to a Client Component prevents the Promise from blocking the rendering of the Server Component.
RSC で async/await させるとレンダリングがブロックされる。
use()
と組み合わせることで、レンダリングを続行し、いち早くクライアントにレスポンスを返すことができるようになる。
ここからは推測だが、次のようなケースで使えるかも?
- 大量のデータをfetch()する
- 複数のAPIをシーケンシャルに呼び出す必要がある
- 必ず表示するが、表示が多少遅延しても問題ないところ(ヘッダー内の情報とか?)
use()
& context
use()に対してcontextを渡せる
useContext() の代替になり、かつ、ifなど条件分岐でも使える
これを書いていてふと思ったが、Contextのvalueの中にRSCで取得したPromiseを突っ込むこともできる?
APIは先程のsuspense時のものを流用し、次のようなContextを作る
"use client";
import { ReactNode, createContext } from "react";
import { FetchUserResponse } from "./fetchUser";
type ContextValue = {
fetchUserPromise: Promise<FetchUserResponse>;
};
export const UserContext = createContext<{
fetchUserPromise: Promise<FetchUserResponse>;
}>({
fetchUserPromise: Promise.resolve({ name: "" }),
});
export const UserContextProvider = ({
children,
value,
}: {
children: ReactNode;
value: ContextValue;
}) => {
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
RSC から ContextProvider へ渡す
export default function Page() {
// Promiseを得る
const fetchUserPromise = fetchUser();
return (
<UserContextProvider value={{ fetchUserPromise }}>
<User />
</UserContextProvider>
);
}
そしてそれを子孫コンポーネントで使う
"use client";
import { use, useContext } from "react";
import { UserContext } from "./UserContext";
export const User = () => {
const { fetchUserPromise } = useContext(UserContext);
const user = use(fetchUserPromise);
return <>{user.name}(Context)</>;
};
Server Components → Client Components で Promise が渡せるという時点でいけそうな気はしてたけど、やっぱ普通に動いた
これって実は結構便利では?
- 既存のNext.jsアプリケーションをRSC化するのが困難
- ひとまず全体をClient Componentのまま、App Router だけ有効化
みたいなケースは結構あると思う。こういったケースでは、アプリケーションのルートに近い箇所であ全体で使うような共通データを取得し、それをProviderとしてアプリケーション全体で利用していることが多いと思われる。
これを、↑の形で書き換えると、次のような恩恵を簡単に得られそう
- データfetch部分をバックエンド側に逃がせる
- 子孫コンポーネントで use() や Suspense を加えるだけで、簡単に部分的なローディングへのfallbackなどが実現できる
特に前者の fetch() をバックエンドに逃がせるのは嬉しい気がする。
Client側のJSサイズも減るし、useEffect() などでの発火も考えなくて良くなる。