kk-web

サーバーアクション(Vercel AI)をuseMutationに噛ませたい

2024-09-06

業務で必要になったので作ってみました。

以下ざくっと実装です、本当に最低限だけ。


useServerAction

import { StreamableValue, readStreamableValue } from "ai/rsc";
import { useCallback, useState } from "react";

type UseMutationOptions<TData, TVariables> = {
  mutationFn: (variables: TVariables) => Promise<StreamableValue<TData>>;
};

type UseMutationResult<TData, TVariables> = {
  data?: TData;
  isPending: boolean;
  mutate: (variables: TVariables) => void;
};

export default function useServerAction<TData = unknown, TVariables = void>({
  mutationFn,
}: UseMutationOptions<TData, TVariables>): UseMutationResult<
  TData,
  TVariables
> {
  const [data, setData] = useState<TData>();
  const [isPending, setIsPending] = useState(false);
  const mutate = useCallback<UseMutationResult<TData, TVariables>["mutate"]>(
    async (variables) => {
      setIsPending(true);

      try {
        const data = await mutationFn(variables);

        for await (const partialData of readStreamableValue(data)) {
          if (partialData) {
            setData(partialData);
          }
        }
      } finally {
        setIsPending(false);
      }
    },
    [mutationFn],
  );

  return {
    data,
    isPending,
    mutate,
  };
}

サーバーアクション(Vercel AI)

"use server";
import { openai } from "@ai-sdk/openai";
import { streamObject } from "ai";
import { StreamableValue, createStreamableValue } from "ai/rsc";
import { z } from "zod";

type GenerateData = Partial<{
  fuga: string;
}>;

export default async function generate(): Promise<
  StreamableValue<GenerateData>
> {
  const stream = createStreamableValue<GenerateData>();

  (async (): Promise<void> => {
    const { partialObjectStream } = await streamObject({
      model: openai("gpt-4o"),
      prompt: "hoge",
      schema: z.object({
        fuga: z.string().describe("piyo"),
      }),
    });

    for await (const partialObject of partialObjectStream) {
      stream.update(partialObject);
    }

    stream.done();
  })();

  return stream.value;
}

クライアントコンポーネント

"use client";
import serverAction from "./actions";

export default function ClientComponent(): JSX.Element {
  const {
    data,
    isPending,
    mutate,
  } = useServerAction({ mutationFn: serverAction });

  ...
}

そんな感じです。

© 2018 kk-web