URLクエリパラメータによる検索画面をSuspenseで実装する3パターン
今回はNext.jsとSuspense
を使ったURLクエリパラメータによる検索画面の実装方法を3つご紹介します。
- Suspense + RSC
- Suspense + CC
- Suspense + useTransition
3番目のパターンのサンプルコードを用意したのでよければご参照ください。
1. Suspense + RSC
RSC(サーバーコンポーネント)でのデータフェッチにはasync await
が使えます。後述するuse
を使うこともできますが、パフォーマンスの観点からasync await
を使うことが推奨されています。
また検索パラメータはURLクエリパラメータから取得しますが、Next.jsのuseSearchParams
はCC(クライアントコンポーネント)でしか使えません。RSCではpage.tsx
のsearchParams
propsからURLクエリパラメータにアクセスできるので、それをpropsとしてRSCに渡します。
// RSCではasync awaitが使える
export const RegionalEmployTable = async ({
// page.tsxのsearchParamsをpropsとして渡す
searchParams
}) => {
const params = new URLSearchParams();
params.set("prefCode", searchParams.prefCode ?? "1");
const { result } = await getRegionalEmploy(params.toString());
};
そしてRSCをSuspense
でラップします。ポイントはkey
を指定していることで、URLクエリパラメータのみ変更による画面遷移ではSuspense
が再実行されないため、searchParams
ごとに異なる識別子をSuspense
に与えます。
URLクエリパラメータのみ変更による画面遷移では
Suspense
が再実行されない
ちなみにこちらの問題の対策として、router.push
や<Link />
によるSPA画面遷移ではなく、<a />
によるMPA画面遷移にしてしまうという方法も考えられます。
// Suspenseに識別子を与える
<Suspense key={JSON.stringify(searchParams)} fallback={<Loading />}>
<RegionalEmployTable searchParams={searchParams} />
</Suspense>
これにより、URLクエリパラメータが変更されるたびにSuspense
のフォールバックUIを表示する検索画面が実装できました。
2. Suspense + CC
先ほど Suspense + RSC で作ったものを Suspense + CC で作ってみます。
今回はCCなのでURLクエリパラメータはuseSearchParams
から取得できます。またPromise
からのデータ取得にはuse
を使ってみます。
export const RegionalEmployTable = () => {
// CCなのでuseSearchParamsを使える
const searchParams = useSearchParams()
// useでPromiseからのデータ取得が可能
const { result: regionalEmploy } = use(getRegionalEmploy(searchParams.toString()));
};
RSCで実行されないfetch
(クライアントサイドフェッチ)にはキャッシュ機構が備わっていないので、キャッシュ機構が必要な場合はTanstack Queryなどのライブラリを組み合わせると良いでしょう。
セキュリティやパフォーマンスなど、Suspense + RSC のメリットを教授できなくなりますが、RSC と CC の使い分けコストや複雑性を考慮して、従来通り Suspense + CC で実装することも十分選択肢に入ると思います。
3. Suspense + useTransition
これは RSC と CC ともに実装できますが、今回は RSC のパターンでご紹介します。まずRSCの実装は Suspense + RSC と同様です。
// RSCの実装はSuspense + RSCと同様
export const RegionalEmployTable = async ({ searchParams }) => {
const params = new URLSearchParams();
params.set("prefCode", searchParams.prefCode ?? "1");
const { result } = await getRegionalEmploy(params.toString());
};
続いてSuspense
の実装ですが、Suspense
は初回データフェッチのフォールバックUIとしてのみ利用するのでkey
の指定は不要です。今回はNext.jsのloading.tsx
を使います。
export default function Loading() {
return (
<p>Loading ....</p>
);
}
そしてここからがポイントで、以下のように実装することでページ遷移状態を取得することができます。
-
router.push
をstartTransition
でラップ(画面遷移をトランジションとしてマーク)し、トランジション中かどうかを表すisSearching
フラグを取得する。 - アプリケーション全体をラップするcontextに対して、1で取得したフラグを渡す。
- 2のcontextから各CCに対してフラグを渡し、「トランジション中はローディング中のUIを表示する」という実装をする。
"use client";
import { createContext, PropsWithChildren, useContext, useState } from "react";
export type RouterTransitionContext = {
isPending: boolean;
setIsPending: (isLoading: boolean) => void;
};
const RouterTransitionContext = createContext<RouterTransitionContext>({
isPending: false,
setIsPending: () => {},
});
// アプリケーション全体をラップするcontextを実装する
export const RouterTransitionProvider = ({ children }: PropsWithChildren) => {
const [isPending, setIsPending] = useState(false);
return (
<RouterTransitionContext.Provider value={{ isPending, setIsPending }}>
{children}
</RouterTransitionContext.Provider>
);
};
// トランジション状態を各CCに提供できるようにする
export const useRouterTransition = () => useContext(RouterTransitionContext);
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useTransition } from "react";
export const useChangeParams = (
key: "prefCode" | "year" | "matter" | "class",
) => {
const router = useRouter();
const searchParams = useSearchParams();
const [isSearching, startTransition] = useTransition();
const { setIsPending } = useRouterTransition();
const handleChange = (v: string) => {
const params = new URLSearchParams(searchParams);
params.set(key, v);
// 画面遷移をトランジションとしてマークする
startTransition(() => {
router.push(`/?${params.toString()}`);
});
};
useEffect(() => {
setIsPending(isSearching);
}, [isSearching]);
return handleChange;
};
// URLクエリパラメータを変更するプルダウンUI
export const ClassSelect = () => {
const handleChange = useChangeParams("class");
// contextからトランジション状態を取得する
const { isPending } = useRouterTransition();
return (
// プルダウンのonChangeで、トランジションでマークされた画面遷移を実行する
<Select defaultValue={CLASSES[0].value} onValueChange={handleChange}>
{/* トランジション中はdisableにする */}
<SelectTrigger disabled={isPending}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{CLASSES.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
上記のようにトランジション状態をプルダウンやテーブルなど各UIに適用することで、このようなアプリケーションを実装することができました。
初期表示としてNext.jsのloading.tsx
による画面全体のフォールバックUIの表示、その後検索条件を変えたときはテーブルを非表示にせずにローディング中を表すUI(CSSによる透過)を表現できています。
通常、検索結果画面のようなUIにおいては、再検索によってテーブル自体が非表示になることは好まれません。
今回はURLクエリパラメータのみ変更による画面遷移だったので上述の通りそもそもSuspense
が再実行されませんが、pathname
が変化する画面遷移においてはSuspense
が再実行されフォールバックUIが表示されます(Suspense + RSC と同様の挙動)が、startTransition
により画面遷移をトランジションとしてマークすることで、Suspense
のフォールバックUIを表示しないということが可能です。そういう意味でも Suspense + useTransition はセットで使うことでより真価を発揮します。
さいごに
今回はURLクエリパラメータを使った検索画面の実装方法を3つご紹介しました。
RSCを使うメリットデメリット、Suspense
単体でできること、Suspense
とuseTransition
を組み合わせることでできること、<a />
遷移によるMPAなど、データフェッチするにもいくつかの選択肢があります。それぞれのメリットデメリットを考慮して、適切な実装方法を選択しましょう。
Discussion