Open4

Next.jsで任意のタイミングでデータ取得するには

Hiroki SAKABEHiroki SAKABE

Next.jsの任意のタイミングでデータ取得したい

例: モーダルの表示時にデータを取得して、その結果をモーダル内に表示したい

Next.jsのデータ取得とデータ更新の考え方

任意のタイミングで解決できるデータ取得

Next.jsのデータ取得は、任意のタイミングで解決できる
ここでの解決とは、fetchが返すPromiseをawaitして戻り値を参照できるようになることを指す

Server Componentにて解決する場合

export async function ServerComponent() {
  const message = await fetch("http://localhost:3000/api")
    .then((res) => res.json())
    .then((data) => data.message);

  return <div>message: {message}</div>;
}

ServerComponentのレンダリング時に解決される

Client Componentにて解決する場合

// サーバーコンポーネント
export default function ServerComponent() {
  const messagePromise = fetch("http://localhost:3000/api")
    .then((res) => res.json())
    .then((data) => data.message);

  return <ClientComponent messagePromise={messagePromise} />;
}

// クライアントコンポーネント
"use client";
import { use } from "react";

export function ClientComponent({ messagePromise }: { messagePromise: Promise<string> }) {
  const message = use(messagePromise);

  return <div>message: {message}</div>;
}

ClientComponentのレンダリング時に解決される

例えば、画面の下の方のコンテンツを、表示するまで解決を遅らせることができる

任意のタイミングで開始できないデータ取得

ただし、データ取得の開始は、任意のタイミングで開始できない

データ取得は、そのロジックが書かれたサーバーコンポーネントのレンダリング時に、開始される

前提として、useEffectで囲わないfetchは、Server Componentでしか使えない

// このコンポーネントのレンダリング時に、fetchが開始される
export async function ServerComponent() {
  const message = await fetch("http://localhost:3000/api")
    .then((res) => res.json())
    .then((data) => data.message);

  return <div>message: {message}</div>;
}
// awaitしていなくても、このコンポーネントのレンダリング時に、fetchが開始される
export default function ServerComponent() {
  const messagePromise = fetch("http://localhost:3000/api")
    .then((res) => res.json())
    .then((data) => data.message);

  return <ClientComponent messagePromise={messagePromise} />;
}

Server Componentのレンダリングは必ずpageに紐づく

そして、Server Componentのレンダリングは必ずpageに紐づく

Server Componentのレンダリングは、そのServer Componentが配置されているpageが表示されたとき、に実行される

例えば、以下のようにして「初期状態ではServer Componentを非表示にしておいて、表示にしたときにレンダリングしよう」としてもうまくいかない

// ServerComponentToFetchData.tsx
export async function ServerComponentToFetchData() {
  const message = await fetch("http://localhost:3000/api")
    .then((res) => res.json())
    .then((data) => data.message);

  return <div>message: {message}</div>;
}

// Toggle.tsx
"use client";
import { useState } from "react";

export function Toggle({ children }: { children: React.ReactNode }) {
  const [show, setShow] = useState(false);

  const toggle = () => setShow((prev) => !prev);

  return (
    <div>
      {show ? (
        <button onClick={toggle}>hide data</button>
      ) : (
        <button onClick={toggle}>show data</button>
      )}
      {show && <>{children}</>}
    </div>
  );
}


// page.tsx
import { ServerComponentToFetchData } from "./ServerComponentToFetchData";
import { Toggle } from "./Toggle";

export default function Page() {
  return (
    <Toggle>
      <ServerComponentToFetchData />
    </Toggle>
  );
}

なぜなら、サーバーでのレンダリング時には、Client Componentは無視されて、以下のようにレンダリングされるからだ

// ServerComponentToFetchData.tsx
export async function ServerComponentToFetchData() {
  const message = await fetch("http://localhost:3000/api")
    .then((res) => res.json())
    .then((data) => data.message);

  return <div>message: {message}</div>;
}

// page.tsx
import { ServerComponentToFetchData } from "./ServerComponentToFetchData";

export default function Page() {
  return (
    <>
      <ServerComponentToFetchData />
    </>
  );
}

// `Toggle.tsx`はクライアント側(ブラウザ)でレンダリングされる

よって「モーダル表示時にServer Componentをレンダリングしてデータを取得する」はできない

また、Client Componentでデータ取得を解決する方法もうまくいかない

// Toggle.tsx
"use client";
import { use, useState } from "react";

export function Toggle({ messagePromise }: { messagePromise: Promise<string> }) {
  const [show, setShow] = useState(false);

  const toggle = () => setShow((prev) => !prev);

  let message: string | null = null;

  if (show) {
    message = use(messagePromise);
  }

  return (
    <div>
      {show ? (
        <button onClick={toggle}>hide data</button>
      ) : (
        <button onClick={toggle}>show data</button>
      )}
      {show && <div>message: {message}</div>}
    </div>
  );
}

// page.tsx
import { Toggle } from "./Toggle";

export default function Page() {
  const messagePromise = fetch("http://localhost:3000/api")
    .then((res) => res.json())
    .then((data) => data.message);

  return <Toggle messagePromise={messagePromise} />;
}

なぜなら、データ取得の解決はモーダル表示時にできるが、データ取得の開始はServer Componentのレンダリング時(ページアクセス時)のままだからだ

URLパラメータを使ってダイアログの表示非表示を変える

では、どうするか
Next.jsで案内されている & 多くのユースケースデメリットがあるのは、URLパラメータを使ってダイアログの表示非表示を変える手法だ
これなら、ダイアログの表示 = ページ遷移となるので、ページ遷移時にServer Componentをレンダリングしてデータを取得すれば良い。ダイアログ非表示の場合、ダイアログコンポーネントは表示されないので、無駄なデータ取得は起こらない

//

また、URLに画面状態を保つことはweb的に良い

Pageに紐づかないServer Componentはない(lazy?)
https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading#importing-server-components

Hackなアプローチ Server Actionsによるデータ取得

あまり推奨されないが、Server Actionsをデータ取得の目的で使うことができる

Server ActionsはJSXを返すことができる
https://azukiazusa.dev/blog/server-actions-return-jsx/

任意のタイミングでデータ取得したいなら

ここで、Route Handler(旧API Handler)の出番

Next.jsでも、Server Componentが全てを代替するとは言われておらず、依然としてRoute Handlerも提供されている

SWR

Suspenseも対応してるよ

KK

client内にServer componentを明示していないfetchを差し込んだ場合
fetchはclient、server両方で使えるのでclient component扱いにならないでしょうか?
nextやったことないですが気になりました
<Toggle>
<ServerComponentToFetchData />
</Toggle>

Hiroki SAKABEHiroki SAKABE

Next.jsでは、コンポーネントはデフォルトでReact Server Componentsとなるので、明示しなくても Server Component扱いになるはずです。

ちなみに、このコードで

<Toggle>
    <ServerComponentToFetchData />
</Toggle>

ServerComponentToFetchData.tsxの先頭に'use client'を記述したら

Error: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client' to a module that was originally written for the server.

となりました。