Server Actionをデータ取得に使う方法
Next.js 14でstableになり、色んな意味で話題になったServer Actions。
「面白そうだな」と思う一方で、使用例がFormの話ばかりなので、Form以外の場所での使い方としてデータ取得を試してみました。
TL;DR
"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 (
<>
{/* ... */}
</>
);
}
"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
を付けてくれないと仮定します。
関数をServer Actionとしてマークするには "use server"
ディレクティブが必要ですが、Client Componentで使うためには、以下のようにファイル単位にする必要があります。
"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
に渡すブロックのこと。
import { useState, useTransition } from "react";
function SomeComponent() {
const [isPending, startTransition] = useTransition();
const [current, setCurrent] = useState(0);
function change(target: number) {
startTransition(() => {
// ココがトランジション
});
}
// ...
}
この startTransition
を useEffect
フック内で使うと、いつもの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回避できるし便利よね)
Discussion