動かざることバグの如し

近づきたいよ 君の理想に

Vercel AI SDKのRSCが非推奨になった理由

環境

  • Vercel AI SDK v6

まず結論

Vercel AI SDKのRSC(React Server Components)パッケージは実験的機能として提供されていたが、技術的な限界により開発が事実上停止している。 公式は本番環境での使用を推奨しておらず、AI SDK UIへの移行を強く推奨している状況だ。

なぜ開発止まったのか

AI SDK RSCは以下のような致命的な問題を抱えている:

  • Server Actionsを使ったストリームの中断ができない。これはReactとNext.jsの将来のリリースで改善される予定らしいが、現時点では未解決
  • createStreamableUIstreamUIを使うと、.done()コンポーネントが再マウントされてチラつく
  • Suspense境界を多用するとクラッシュする
  • createStreamableUIを使うと二次関数的にデータ転送量が増える。回避策としてcreateStreamableValueを使ってクライアント側でレンダリングする方法はあるが、本末転倒感が否めない
  • クローズされたRSCストリームが更新の問題を引き起こす

要するに、RSCの限界を押し広げようとした結果、安定した本番環境での使用には耐えられない状態になってしまったわけで、実験的機能のままフェードアウトしていく流れになっている。

一方でVercelはv0の開発を通じて、Webでのベストなチャット体験を作るために相当な時間を投資してきた。その知見を詰め込んだのがAI SDK UIで、言語モデルミドルウェア、マルチステップツール呼び出し、添付ファイル、テレメトリー、プロバイダーレジストリなど、実用的な機能が揃っている。こっちのほうが明らかに本命だ。

どうすればいいの

AI SDK RSCからAI SDK UIへの移行が必要になる。基本的な考え方としては、テキスト生成とUI描画の責務を分離することだ。

RSCではstreamUIが生成と描画を一手に引き受けていたが、UIではstreamTextでストリーミング生成を行い、useChatフックでUIをレンダリングする形に変わる。

移行前: Server Actionでストリーミング

// @/app/actions.ts
import { streamObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { createStreamableValue } from '@ai-sdk/rsc';
import { notificationsSchema } from '@/utils/schemas';

export async function generateSampleNotifications() {
  'use server';

  const stream = createStreamableValue();

  (async () => {
    const { partialObjectStream } = streamObject({
      model: openai('gpt-4o'),
      system: 'generate sample ios messages for testing',
      prompt: 'messages from a family group chat during diwali, max 4',
      schema: notificationsSchema,
    });

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

  stream.done();

  return { partialNotificationsStream: stream.value };
}

クライアント側ではreadStreamableValueでストリームを読み取る

// @/app/page.tsx
'use client';

import { useState } from 'react';
import { readStreamableValue } from '@ai-sdk/rsc';
import { generateSampleNotifications } from '@/app/actions';

export default function Page() {
  const [notifications, setNotifications] = useState(null);

  return (
    <div>
      <button
        onClick={async () => {
          const { partialNotificationsStream } =
            await generateSampleNotifications();

          for await (const partialNotifications of readStreamableValue(
            partialNotificationsStream,
          )) {
            if (partialNotifications) {
              setNotifications(partialNotifications.notifications);
            }
          }
        }}
      >
        Generate
      </button>
    </div>
  );
}

移行後: Route HandlerとuseObjectフック

Route HandlerでstreamObjectを実装する

// @/app/api/object/route.ts
import { streamObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { notificationSchema } from '@/utils/schemas';

export async function POST(req: Request) {
  const context = await req.json();

  const result = streamObject({
    model: openai('gpt-4.1'),
    schema: notificationSchema,
    prompt:
      `Generate 3 notifications for a messages app in this context:` + context,
  });

  return result.toTextStreamResponse();
}

クライアント側ではuseObjectフックを使う

// @/app/page.tsx
'use client';

import { useObject } from '@ai-sdk/react';
import { notificationSchema } from '@/utils/schemas';

export default function Page() {
  const { object, submit } = useObject({
    api: '/api/object',
    schema: notificationSchema,
  });

  return (
    <div>
      <button onClick={() => submit('Messages during finals week.')}>
        Generate notifications
      </button>

      {object?.notifications?.map((notification, index) => (
        <div key={index}>
          <p>{notification?.name}</p>
          <p>{notification?.message}</p>
        </div>
      ))}
    </div>
  );
}

Server ActionからRoute Handlerへの移行、createStreamableValueからuseObjectへの移行がポイントだ。コード量はそこまで変わらないが、責務が明確に分離されて保守性が上がる。

参考リンク