🌐

Server Actionをデータ取得に使う方法

2023/11/20に公開

Next.js 14でstableになり、色んな意味で話題になったServer Actions。
「面白そうだな」と思う一方で、使用例がFormの話ばかりなので、Form以外の場所での使い方としてデータ取得を試してみました。

TL;DR
ClientComponent.tsx
"use client";
import { useEffect, useState, useTransition } from "react";
import { getData } from "./server-actions";
import type { Data } from "~/api";

export default function ClientComponent() {
  const [isPending, startTransition] = useTransition();
  const [data, setData] = useState<Data | null>(null);
  
  useEffect(() => {
    startTransition(async () => {
      setData(await getData());
    });
  }, []);
  
  return (
    <>
      {/* ... */}
    </>
  );
}
server-actions.ts
"use server";
import { ApiClient } from "~/api";

export async function getData(arg1: string, arg2: string) {
  return ApiClient.getData({ arg1, arg2 });
}

動機

Server Componentはサーバサイドで完結するので、データ取得に際しCORSの制限はありません。
しかし、イベントハンドラやフックを使いたい時など、Client Componentでの実装が必要なケースもあります。

Client Componentにそのままデータ取得の処理を書くと、 Access-Control-Allow-Origin の設定次第でCORSの制限に引っ掛かってしまいます。
自分で設定を弄れるならちゃんと設定すればいいのですが、そうではない場合はProxyが必要になってきます。
Next.js に App Router がなかった頃は API Routes でProxyを建てたりしたものですが、これを Server Action で実現できないか?と思い立ったのです。

Server Action の準備

ZodiosなどでAPIをクライアントライブラリ化していて、そのAPIが Access-Control-Allow-Origin を付けてくれないと仮定します。
https://www.zodios.org/

関数をServer Actionとしてマークするには "use server" ディレクティブが必要ですが、Client Componentで使うためには、以下のようにファイル単位にする必要があります。

server-actions.ts
"use server"; // <-
import { ApiClient } from "~/api";

export async function getData(arg1: string, arg2: string) {
  return ApiClient.getData({ arg1, arg2 });
}

関数の引数と返り値 は、いずれもserializableである必要がありますが、引数返り値でサポートされるデータ型に違いがあるので注意が必要です。

Client Component で使う

Reactのドキュメントには、以下のようにForm以外でも呼び出せる事が示されています:

サーバアクションはサーバ側の公開エンドポイントであり、クライアントコードのどこからでも呼び出すことができます。
フォームの外部でサーバアクションを使用する場合、トランジション内でサーバアクションを呼び出すようにしてください。

ここでの「トランジション」は、CSSの transition ではなく、ビルトインのフック useTransition() の返り値 startTransition に渡すブロックのこと。

https://ja.react.dev/reference/react/useTransition

import { useState, useTransition } from "react";

function SomeComponent() {
  const [isPending, startTransition] = useTransition();
  const [current, setCurrent] = useState(0);

  function change(target: number) {
    startTransition(() => {
      // ココがトランジション
    });
  }

  // ...
}

この startTransitionuseEffect フック内で使うと、いつものClient Componentでのデータ取得と同じ形でできます。
また、 isPending によってロード中かどうかも判別できます。

import { useState, useTransition } from "react";
import { getData } from "./server-actions";

function SomeComponent() {
  const [isPending, startTransition] = useTransition();
  const [data, setData] = useState(null);

  useEffect(() => {
    startTransition(async () => {
      setData(await getData());
    });
  }, []);

  // ...
}

useEffect 内で呼ぶのをwrapした関数に変えれば、データ更新も簡単に実現できそうですね。

おわりに

冒頭にも書きましたが、非推奨の使い方ですので、あしからず。
(でもSWR使わなくていいしCORS回避できるし便利よね)
https://swr.vercel.app/ja

Discussion