はじめに
Next.jsにてデモサイトを作成中に、Suspenseが効かない問題が起き、
前に同様な現象があったのですが備忘録で残しておきたいと思います。
Qiita
の記事をAPIを作成して取得しています。「もっとみる」ボタンを押下したら全記事取得するのですが、
Loading.tsxが出てきませんでした。useState
を使えばすぐに終わるのですが、クライアントコンポーネントにするとNext.jsの良さが消えてしまうのではないかと言う気持ちと、Next.jsではpageコンポーネントはサーバーコンポーネントとして使用することがベストプラクティスとされていることから、できる限りSuspense
を使い倒したかったのです。
実際に、Next.jsの公式チュートリアルで学習を行った時にはできる限りuse client
を使っていなかった様が気がします。
問題
では問題のコードを見てみましょう。
export default async function Home({ searchParams }: { searchParams: { showAll?: string } }) {
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const params = await Promise.resolve(searchParams);
const showAllParam = params?.showAll;
const shouldShowAll = showAllParam === "true";
const limit = shouldShowAll ? 100 : 4;
const response = await fetch(`${API_URL}/api/qiita?limit=${limit}`, { cache: "no-store" });
const data = await response.json();
return (
<div className="p-4 bg-blue-100 min-h-screen pt-10 pb-20">
<h2 className="text-2xl font-bold text-gray-800 mb-2">Qiitaの記事一覧</h2>
<Suspense key={String(params)} fallback={<Loading />}>
<ArticlesList articles={data} />
</Suspense>
{!shouldShowAll && <ShowMoreButton />}
</div>
);
}
まず、書いたコードの一部を解説します。
<Suspense key={String(params)} fallback={<Loading />}>
keyに入れる値は、「この値が変わったら、fallbackのLoadingを呼びたい」っていう時に使うものっぽいです。
Reactのドキュメント上にも、
トランジション中、React は既に表示されているコンテンツを隠さないようにします。しかし、異なるパラメータを持つルートに移動する場合、React にそれが異なるコンテンツであると伝えたいことがあります。これを表現するために、key が使えます。
と書いてありますね。
ではparams
の値が変わったのに、なぜかわらなかったのか。
それは結構簡単なことでした。
解決方法
ArticlesList
コンポーネントにデータを呼び出し、非同期にしてあげる。
たったこれだけです。
理由ですが、
React(Next.js)は、コンポーネントの中で Promise が発生したら一時停止(=suspend)するという仕組みを持っています。
これを使って、
「データ読み込み中だな → fallback 表示しとこ!」
とやってくれるのが Suspense
つまり、
1.コンポーネントが同期で即座に描画できるなら → ローディング不要なので、fallback出す理由がない
2.コンポーネントが 非同期でまだ中身が来てない(Promise中) → 今表示できないから fallback 出す
ということですね。
今回の事象はこの1に当てはまっていたみたいです。
てなわけで、今回のコードはこちら。
export default async function Home({ searchParams }: { searchParams: { showAll?: string } }) {
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const params = await searchParams;
const showAllParam = params?.showAll;
const shouldShowAll = showAllParam === "true";
return (
<div className="p-4 pt-10 pb-20">
<h2 className="text-2xl font-bold text-gray-800 mb-2">Qiitaの記事一覧</h2>
<Suspense key={shouldShowAll ? "all" : "limited"} fallback={<Loading />}>
<FeatureArticleList showAll={shouldShowAll} />
</Suspense>
{!shouldShowAll && <ShowMoreButton />}
</div>
);
}
import { ArticlesList } from "@/app/components/common/articlesList/ArticlesList";
export const FeatureArticleList = async ({ showAll }: { showAll: boolean }) => {
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const limit = showAll ? 100 : 4;
const response = await fetch(`${API_URL}/api/qiita?limit=${limit}`, { cache: "no-store" });
const data = await response.json();
return (
<>
<ArticlesList data={data} />
</>
);
};
FeatureArticleList
を新たに作ってますが、別に前のコンポーネントでもいけます。
また、こういったSuspenseで呼び出す時にはスケルトンUIを使うことがUX上でいいと言われています。
おわりに
Suspense
の技術について改めて復習になりました
参考