Next.js 13 + Recoil + Suspenseでボタンを押したらデータ取得する方法

投稿日 2023-08-14
最終更新日 2023-11-18

Next13でSuspenseを使ったデータ取得に苦労したのでまとめてみます。

概要

今回はサンプルとしてこのような挙動のアプリを作りました。



文章で書くと以下のような挙動をしています。

  1. 「データを取得する」ボタンを押すとデータ取得処理が開始されます。
  2. データ取得処理中はローディングのアニメーションとテキストを表示します。(ここでSuspenseが使えます。)
  3. データ取得終了後、Recoilにデータを格納し、他のページのリンクを表示します。
  4. リンクをクリックし、ページに移動すると取得したデータを閲覧できます。


現在作成しているアプリで同じような挙動を実装する必要があり、そこで苦労したので別プロジェクトで簡単なものを実装してみることにしました。

なお、データの取得はサンプルとしてJSONPlaceholder(https://jsonplaceholder.typicode.com/)の/posts を使用しています。また、ローディング中画面が表示されていることが分かりやすいようにあえて2秒間のスリープを入れています。

技術スタック

- Next.js (App Router, TypeScript)
- Suspense
- Recoil
- SWR
- TailwindCSS

取得する際の問題点

これを実装するために苦労したことがありました。
まず、Next.js 13.4ではサーバーコンポーネントがデフォルトになっていることです。サーバーコンポーネント内では、React Hookを利用することができず、ボタンを押したかどうかを管理するためのuseStateやRecoilのuseRecoilValue等を使えません。最初のロード時にデータを取得し、それをそのページで表示するだけならばサーバーコンポーネントで簡単に実装できますが、今回のように特定の条件下でロードを行い、別ページで表示するとなるとそうはいきませんでした。

解決方法


基本的にはクライアントコンポーネントを使用することで解決します。
Next 13.4でクライアントコンポーネントを使用するには最初の行で以下のように明示的に宣言する必要があります。

"use client";


これでuseStateが使えるようになったのでボタンが押されたらデータフェッチ用のコンポーネントを呼び出す処理を書くことができます。

DataLoader.tsx

const [isButtonClick, setIsButtonClick] = useState<boolean>(false);
  const onButtonClick = async () => {
    setIsButtonClick(true);
  };

return (
   ...
   {isButtonClick && (
        <Suspense fallback={<Loading />}>
          <LoadingPosts />
        </Suspense>
      )}
   ...
);

なおReact v18から追加された<Suspense> を使用することでデータ取得中にローディング用コンポーネントを処理する機能を実現しています。

次のデータフェッチ用のコンポーネントを見ていきます。こちらもクライアントコンポーネントとなっておりuseSetRecoilStateやuseSWRを利用することができます。
LoadingPosts.tsx

// データを取得する(2秒間待つ)
const fetcher: Fetcher<Post[], string> = async (url: string) => {
  const res = await axios.get(url).then(await sleep(2));
  return res.data;
};

const LoaddingPosts = () => {
  // jsonplaceholderからデータを取得する。
  const { data, error, isLoading } = useSWR(
    "https://jsonplaceholder.typicode.com/posts",
    fetcher,
    { suspense: true }
  );

  // Recoilで取得する。
  const setPosts = useSetRecoilState(postsState);
  setPosts(data);

  return (
    <>
      {isLoading || (
        <>
          <div>ローディングが完了しました!!</div>
          <Link href="/posts/">一覧を見る</Link>
        </>
      )}
    </>
  );
};


なお、SWRでSuspenseを利用するには以下のように明示的に使用を許可する必要があります。

const { data, error, isLoading } = useSWR(
    "https://jsonplaceholder.typicode.com/posts",
    fetcher,
    { suspense: true }   // 明示的に宣言する
  );


これで該当の機能が実装できました。コード全体はGithubに上げているのでよければ見てください。
https://github.com/matsunagadaiki151/next-suspense-example/

今後のNext.jsではクライアントコンポーネントとサーバーコンポーネントの使い分けが重要になってくるので、その原理を覚えておく必要がありそうですね。

参考

  • (公式doc) Loading UI and Streaming (https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming)
  • (Udemy) 今後のフロントエンド開発で必須知識となるReact v18の機能を丁寧に理解する (https://www.udemy.com/course/react_v18/)
  • (Zenn) Next.js 13 の React Server Components(RSC) とデータフェッチ (https://zenn.dev/tfutada/articles/36ad71ab598019)