📝

Astro Actions 試してみた

2024/05/09に公開

この記事について

Astro 4.8 で Experimental な機能として登場した Astro Actions をサクッと試してみた記事です。

https://astro.build/blog/astro-480/#experimental-astro-actions

先月 Astro DB 試してみた成果物を作ったので

https://zenn.dev/chot/articles/a03fa6c3c5244c

今回はこれを Astro Actions を使うように改造したいと思います。

環境

"@astrojs/check": "^0.6.0"
"@astrojs/db": "^0.11.1"
"@astrojs/preact": "^3.3.0"
"@astrojs/vercel": "^7.6.0"
"astro": "^4.8.0"
"preact": "^10.21.0"
"typescript": "^5.4.5"

完成品

https://github.com/k1350/astro-db-test/tree/experimental-astro-actions

experimental-astro-actions というブランチにコミットしました。

本文

1. Astro Actions を有効化

まず astro.config.mjs で Astro Actions を有効にします。

astro.config.mjs
 import db from "@astrojs/db";
 import preact from "@astrojs/preact";
 import vercel from "@astrojs/vercel/serverless";
 import { defineConfig } from "astro/config";

 // https://astro.build/config
 export default defineConfig({
   integrations: [db(), preact()],
   output: "server",
   adapter: vercel(),
+  experimental: {
+    actions: true,
+  },
 });

2. Astro Actions を書く

次に src/actions/index.ts にアクションを書きます。
今回はもともと src/pages/api/addLikes.ts に記述していた「いいね」追加処理を移植します。
(ついでに前回の成果物では更新後の「いいね」を取得するために画面ごとリロードしていて劣悪なユーザー体験だったので、今回はここで最新の「いいね」数を返すようにします。)

src/actions/index.ts
import { defineAction, z } from "astro:actions";
import { Like, count, db, eq } from "astro:db";

export const server = {
  addLike: defineAction({
    accept: "form",
    input: z.object({ url: z.string().regex(/^\/.*/) }),
    handler: async ({ url }) => {
      await db.insert(Like).values({ url, createdAt: new Date() });
      const likeCounts = await db
        .select({ count: count() })
        .from(Like)
        .where(eq(Like.url, url));
      const likeCount = likeCounts.length > 0 ? likeCounts[0].count : 0;
      return { likeCount };
    },
  }),
};

Astro Actions の構文について、https://docs.astro.build/en/reference/configuration-reference/#experimentalactions には handlercontext という引数も取るように描いてあるのですが、これだと動きませんでした。
現在は Blog のほうに書いてあるように context 無しの構文が正となります。

Actions RFC によると、もとは第二引数で context を取れる形だったのが、最終的には getApiContext() で取得するように変更され、ドキュメントの更新が今現在は追いついていないようです。
getApiContext() からは cookie などが取得できるみたいですね。

3. クライアントサイドから Astro Actions を呼ぶ

次に「いいね」追加処理を実行していた部分を Astro Actions を使用する形式に直します。
「"astro:actions" が見つからない」的なエラーメッセージがエディタに表示される場合や、actions.addLike の戻り値の型が取得できない場合は一度 astro dev コマンドを実行すると解消すると思います。

src/components/addLikeForm.ts
+ import { actions } from "astro:actions";
  import { useState } from "preact/hooks";

  type Props = {
    url: string;
+   updateLikeCount: (newLikeCount: number) => void;
  };
- export function AddLikeForm({ url }: Props) {
+ export function AddLikeForm({ url, updateLikeCount }: Props) {
    const [isSubmitting, setIsSubmitting] = useState(false);

    async function submit(e: SubmitEvent) {
      e.preventDefault();
      setIsSubmitting(true);
      const formData = new FormData(e.target as HTMLFormElement);
-     const response = await fetch("/api/addLike", {
-       method: "POST",
-       body: formData,
-     });
-     if (response.status === 200) {
-       window.location.reload();
-     }
+     const result = await actions.addLike(formData);
+     updateLikeCount(result.likeCount);
      setIsSubmitting(false);
    }

    return (
      <form onSubmit={submit}>
        <input type="hidden" name="url" value={`${url}`} />
        <button type="submit" disabled={isSubmitting}>
          Add Like
        </button>
      </form>
    );
  }

あとは不要になった src/pages/api/addLikes.ts を消し、諸々整えて Vercel にデプロイします!
デプロイは特に問題なく、Astro Actions を使って「いいね」を更新することができるようになりました🎉

感想

Astro Actions 側で input を Zod のスキーマで定義させる点は優れていると思いました。
accept: "form" の宣言で FormData を受け取るか、JSON を受け取るかを切り替えられるのもわかりやすくて良いですね。

Discussion