Open11

use()をuseしてみる

mugimugi

Reactの use() に関してドキュメントが用意された
https://ja.react.dev/reference/react/use

2024/01/18 現在、Canary or Experimental で使えるので、つまり Next.js では使える。

実際のところ use() で何ができるのかを確認しておく

mugimugi

SuspenseやErrorBoundaryとの協調

基本的なケース。
SuspenseやErrorBoundaryで、ロード中やエラー時のフォールバックに簡単に対応できる。

https://ja.react.dev/reference/react/use#reading-context-with-use

mugimugi

クライアントコンポーネントでプロミスを作成するよりも、サーバコンポーネントでプロミスを作成してそれをクライアントコンポーネントに渡すようにしてください。クライアントコンポーネントで作成されたプロミスは、レンダーごとに再作成されます。サーバコンポーネントからクライアントコンポーネントに渡されたプロミスは、再レンダー間で不変です。こちらの例を参照してください。

仮にWebAPIの実行を待ちたい場合、単純に Client Components から直接APIを呼び出し、そのPromiseを use() に放り込めばOKかと言われたらそんなことはない。↑にある通り、Promiseが該当のClient Compoentns 内でレンダーごとに生成されるため、特に何も考えずに実装すると、何度もAPIが実行される。
Next の場合、Server Componentsでasync/awaitを使わずにPromiseを取得し、それをClientComponentsにPropsとして渡すことで処理できる。

APIコールを想定したダミーの関数
export type FetchUserResponse = { name: string };

// API実行想定の関数
// (一定時間後にResolveとなるPromiseを返す)
export const fetchUser = (): Promise<FetchUserResponse> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: "mugi" });
    }, 3000);
  });
};
Promiseを受け取るClient Component
"use client";

import { use } from "react";
import { FetchUserResponse } from "./fetchUser";

export const User = ({
  fetchUserPromise,
}: {
  fetchUserPromise: Promise<FetchUserResponse>;
}) => {
  const user = use(fetchUserPromise);
  return <>{user.name}</>;
};
page.tsx
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と連動して動く

mugimugi

Next.js での page 単位での自動的な Suspense および ErrorBoundary と組み合わせることもできるぽい。

たとえば、次のように loading.tsx を用意する

loading.tsx
export default function Loading() {
  return "loading...(loading.tsx)";
}

そして page.tsx を次のように変更する(Suspenseを消す)

page.tsx
export default function Page() {
  // Promiseを得る
  const fetchUserPromise = fetchUser();

  return <User fetchUserPromise={fetchUserPromise} />;
}

うまく動く

mugimugi

error.tsx もいける。
APIのほうでrejectするようにする

export const fetchUser = (): Promise<FetchUserResponse> => {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject({});
    }, 3000);
  });
};

そして page.tsx の横に error.tsx を置く

error.tsx
"use client";

export default function Error() {
  return <>error!!</>;
}

バッチリ

mugimugi

使い所

これって別に 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をシーケンシャルに呼び出す必要がある
  • 必ず表示するが、表示が多少遅延しても問題ないところ(ヘッダー内の情報とか?)
mugimugi

use() & context

use()に対してcontextを渡せる
useContext() の代替になり、かつ、ifなど条件分岐でも使える
https://ja.react.dev/reference/react/use#reading-context-with-use

mugimugi

これを書いていてふと思ったが、Contextのvalueの中にRSCで取得したPromiseを突っ込むこともできる?

mugimugi

APIは先程のsuspense時のものを流用し、次のようなContextを作る

UserContext
"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>;
};
mugimugi

RSC から ContextProvider へ渡す

export default function Page() {
  // Promiseを得る
  const fetchUserPromise = fetchUser();

  return (
    <UserContextProvider value={{ fetchUserPromise }}>
      <User />
    </UserContextProvider>
  );
}

そしてそれを子孫コンポーネントで使う

User.tsx
"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 が渡せるという時点でいけそうな気はしてたけど、やっぱ普通に動いた

mugimugi

これって実は結構便利では?

  • 既存のNext.jsアプリケーションをRSC化するのが困難
  • ひとまず全体をClient Componentのまま、App Router だけ有効化

みたいなケースは結構あると思う。こういったケースでは、アプリケーションのルートに近い箇所であ全体で使うような共通データを取得し、それをProviderとしてアプリケーション全体で利用していることが多いと思われる。

これを、↑の形で書き換えると、次のような恩恵を簡単に得られそう

  • データfetch部分をバックエンド側に逃がせる
  • 子孫コンポーネントで use() や Suspense を加えるだけで、簡単に部分的なローディングへのfallbackなどが実現できる

特に前者の fetch() をバックエンドに逃がせるのは嬉しい気がする。
Client側のJSサイズも減るし、useEffect() などでの発火も考えなくて良くなる。