tech-book-labs
ガイド(外部統合) · 2 セクション統合

Vercel AI SDK(React)実装ガイド

Vercel AI SDK の React 統合で LLM チャット UI を組むパターン。ストリーミングの基本と、型付きトランスポートの組み方までを 1 ページに統合。

著者:TechBook.net編集部 · 最終検証 2026-05-10
セクション · 2026-05-10 · @ai-sdk/react 3.0.x

Vercel AI SDK で React のチャット UI を作る

Vercel AI SDK(ai + @ai-sdk/react)で useChat を最小ボイラープレートで使い、サーバー側 streamText とつなぐ TypeScript 型安全パターン、tool calling、よく詰まる点を実装メモにまとめる。

検証日: 2026-05-10

使用バージョン: ai@6.x / @ai-sdk/react@3.x

対象: React でチャット UI を作る、LLM の streaming レスポンスを扱う、tool calling を組み込む

ヘッダ付き auth / 型付き UIMessage / 独自 transport が必要な場面は /articles/ai-sdk-react-typed-transport/ を参照。

触って試す

🤖 mock streaming(実 API なし)。文字ずつ append される様子が ai-sdk-react の useChat と同じ UX。
下のサンプル質問を選ぶか、自分で入力してください

このデモは mock streaming(実 API なし)。useChat と同じ UX を再現しています。

最小サンプル(クライアント)

useChat を 1 行呼ぶだけで messages / input / handleSubmit が揃う。送信先 endpoint は api で指定:

"use client";  // Next.js 13+ の app router 想定
import { useChat } from "@ai-sdk/react";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: "/api/chat",
  });

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong> {m.content}
        </div>
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
        <button disabled={isLoading}>Send</button>
      </form>
    </div>
  );
}

useChat が messages 配列の状態管理 + streaming 受信 + 入力フォームの hook をまとめて提供。

最小サンプル(サーバー、Next.js Route Handler)

streamText でモデルにメッセージを渡し、toDataStreamResponse()useChat が読める Streaming レスポンス形式に変換:

// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

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

  const result = await streamText({
    model: openai("gpt-4o"),
    messages,
    system: "あなたは親切なアシスタント。",
  });

  return result.toUIMessageStreamResponse();
}

toUIMessageStreamResponse() が SSE で useChat に必要な形式を返す。

モデルプロバイダの切替

@ai-sdk/openai を別プロバイダに差し替えるだけ:

プロバイダパッケージ
OpenAI@ai-sdk/openai
Anthropic Claude@ai-sdk/anthropic
Google Gemini@ai-sdk/google
Mistral@ai-sdk/mistral
AWS Bedrock@ai-sdk/amazon-bedrock
Ollama(ローカル)ollama-ai-provider
import { anthropic } from "@ai-sdk/anthropic";
const result = await streamText({ model: anthropic("claude-sonnet-4-6"), messages });

UI 側はモデル変更を意識しなくて良い(provider 抽象が効く)。

tool calling(関数呼び出し)

tools オブジェクトに description + Zod schema + execute を定義。LLM が必要と判断すれば関数が呼ばれる:

import { z } from "zod";

const result = await streamText({
  model: openai("gpt-4o"),
  messages,
  tools: {
    getWeather: {
      description: "都市の現在気温を取得",
      parameters: z.object({ city: z.string() }),
      execute: async ({ city }) => {
        const r = await fetch(`https://api.example.com/weather?q=${city}`);
        return r.json();
      },
    },
  },
});

Zod schema で引数の型を縛る → LLM が誤った JSON を返した場合は SDK 側で safeParse + retry。

ストリームの抜き取り(low-level)

UI を自作したい時、useChat を使わず生 stream を扱う:

const response = await fetch("/api/chat", {
  method: "POST",
  body: JSON.stringify({ messages }),
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);
  // SSE をパース
}

useChat に不満が出た時のフォールバック。普通は useChat で十分。

つまずいたポイント

  • api: "/api/chat" の path が違うと無音で動かない:Network タブで 200 OK + SSE が来ているか確認
  • CORS:同一オリジンなら問題なし、別ドメインなら server 側で Access-Control-Allow-Origin 設定
  • API key を client に置くな:process.env.OPENAI_API_KEY をサーバー側 (.env.server) のみに。クライアント露出はアカウント停止リスク
  • messages の構造変更:v3 系で parts 配列に変わった場面がある(text 以外に tool result / image を持つ)。m.content だけで描画すると一部の挙動が漏れる
  • Stream を途中で止める:stop() を呼べる(useChat から取れる)。長いレスポンスの cancel UX に必要

評価

観点評価コメント
学習コストuseChat で 5 行のクライアント
プロバイダ抽象OpenAI / Anthropic / Gemini を差し替え可
TypeScripttool 引数 / 戻り値が end-to-end 型推論
streaming UXSSE / 文字単位 streaming が標準
バンドル30-50KB(プロバイダごとに小分け)

向く / 向かないケース

  • 向く: チャット UI、AI assistant、tool calling、多モデル横断、Next.js / SvelteKit / Nuxt
  • 向かない: ベクター検索など LLM 以外の AI 機能(直接 API を叩く)
  • 向かない: ストリームを使わないシンプルな one-shot 生成(普通の fetch で十分)

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books/9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る
セクション · 2026-05-10 · @ai-sdk/react 3.0.x

Vercel AI SDK で auth ヘッダ + 型付き UIMessage を扱う

AI SDK の useChat を DefaultChatTransport で拡張し、Firebase ID トークンを毎回付ける auth パターン、独自の type 付き request body、UIMessage の generic 型共有、tool calling まで踏み込んだ実装メモ。

検証日: 2026-05-10

使用バージョン: ai@6.x / @ai-sdk/react@3.x

対象: useChat 入門は通っている、認証や複数チャットサービスを扱う本番アプリで踏み込みたい人

入門記事は別途。本稿は「1 つの API に複数のチャットサービスを乗せる」「毎リクエストに認証を付ける」「メッセージ型を server / client で共有する」など、本番運用で必要になる中級パターン。

1. DefaultChatTransport で送信前に介入

useChat はデフォルトで POST /api/chat を叩くが、auth ヘッダの付与・request body のカスタマイズが必要なら DefaultChatTransport を渡して prepareSendMessagesRequest で介入する。

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";

export function useImageFeatureChat(args: { imageUrl: string; locale: string }) {
  const { user } = useFirebaseUser();   // 例: 認証コンテキスト

  return useChat<MyChatUIMessage>({
    transport: new DefaultChatTransport({
      api: "/api/chat",
      prepareSendMessagesRequest: async ({ messages }) => {
        const idToken = await user?.getIdToken();
        if (!idToken) throw new Error("ログインが必要");
        return {
          body: {
            messages,
            type: "image-feature-extraction",  // ← サーバ側でディスパッチに使う識別子
            ...args,
          },
          headers: { Authorization: `Bearer ${idToken}` },
        };
      },
    }),
  });
}

ポイント:

  • prepareSendMessagesRequest は async:Firebase の getIdToken() のように Promise で来る token を待って付与できる
  • body に独自フィールドを追加:type / args を一緒に送れば、1 つの API endpoint で複数サービスをディスパッチ可能
  • headersAuthorization:JWT / OAuth token を毎回付与
  • エラーは throw:認証失敗時に throw すれば、useChat の error state に伝播する

2. Server 側: 1 つの endpoint で複数サービスをディスパッチ

req.body 内の serviceId を見て、それぞれ別の system prompt / モデル / tools セットを streamText に渡す:

import { streamText, type UIMessage } from "ai";
import { z } from "zod";
import { google } from "@ai-sdk/google";

const RequestSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("image-feature-extraction"),
    imageUrl: z.string().url(),
    locale: z.string(),
    messages: z.array(z.any()),
  }),
  z.object({
    type: z.literal("review-summarization"),
    locationId: z.string(),
    messages: z.array(z.any()),
  }),
]);

export async function POST(req: Request) {
  const auth = req.headers.get("authorization");
  const userId = await verifyIdToken(auth);  // 自前

  const body = RequestSchema.parse(await req.json());

  switch (body.type) {
    case "image-feature-extraction": {
      const result = await streamText({
        model: google("gemini-2.5-flash"),
        system: `画像から特徴量を抽出する...(locale=${body.locale})`,
        messages: body.messages as UIMessage[],
      });
      return result.toUIMessageStreamResponse();
    }
    case "review-summarization": {
      const result = await streamText({
        model: google("gemini-2.5-pro"),
        system: "レビューを要約する...",
        messages: body.messages as UIMessage[],
      });
      return result.toUIMessageStreamResponse();
    }
  }
}

ポイント:

  • z.discriminatedUnion("type", [...]) でサービスごとの引数を型付きで束ねる
  • switch で出口を分岐、それぞれ別モデル / 別 system prompt
  • 共通の auth verification を 1 箇所で
  • toUIMessageStreamResponse() でクライアント useChat 互換の SSE を返す

3. UIMessage 型を server / client で共有

@ai-sdk/reactuseChat は generic で UIMessage 型を絞れる。tool calling の戻り値や custom annotations を含めたい時に効く。

import type { UIMessage } from "ai";

export type ImageFeatureChatUIMessage = UIMessage<
  {
    // metadata 部分(annotations 用)
    extractedFeatures?: { tag: string; confidence: number }[];
  },
  {
    // tool 結果の型
    "extract-features": { features: { tag: string; confidence: number }[] };
  }
>;
const { messages, sendMessage } = useChat<ImageFeatureChatUIMessage>({ /* ... */ });

// messages の各 entry が ImageFeatureChatUIMessage として型推論される
messages.forEach((m) => {
  if (m.role === "assistant") {
    // m.metadata.extractedFeatures を補完で出る
    m.parts.forEach((p) => {
      if (p.type === "tool-extract-features") {
        // tool の結果が型付きで取れる
      }
    });
  }
});

server / client で 同じ generic 型を import すれば、API 仕様変更時に両側に型エラーが波及する。

4. tool calling の typed 化

server 側で tools に Zod schema を渡し、execute 関数で実装する。LLM が誤った JSON を返した場合は SDK 内で safeParse + retry を試みる。

import { tool } from "ai";

const result = await streamText({
  model: google("gemini-2.5-flash"),
  messages,
  tools: {
    extractFeatures: tool({
      description: "画像から特徴量タグを抽出",
      inputSchema: z.object({
        imageUrl: z.string().url(),
        maxTags: z.number().int().min(1).max(20).default(10),
      }),
      execute: async ({ imageUrl, maxTags }) => {
        const features = await runVisionModel(imageUrl, maxTags);
        return { features };  // → クライアント側で {tag, confidence}[] に narrow される
      },
    }),
  },
});

UI 側では m.partstool-extractFeatures の entry が混ざってくるので、type narrowing で取り出す。

5. ストリーム中断と error handling

長文生成の途中でユーザがキャンセルしたい場合:

const { messages, sendMessage, status, stop, error } = useChat({ /* ... */ });

// 状態
status === "streaming"  // 受信中
status === "submitted"  // 送信したがまだ stream 開始前
status === "ready"      // 完了 / 待機中
status === "error"      // エラー

// 中断
<button onClick={stop} disabled={status !== "streaming"}>停止</button>

// エラー
{error && <div className="text-red-600 text-sm">{error.message}</div>}

サーバ側で例外が起きると stream は途中で切れて error state にメッセージが入る。ユーザに retry を促す UI を出す。

6. プロバイダ抽象を使い分ける

複数プロバイダを切り替える典型パターン:

import { google } from "@ai-sdk/google";
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";

const models = {
  cheap: google("gemini-2.5-flash"),
  smart: openai("gpt-4o"),
  long_context: anthropic("claude-sonnet-4-6"),
} as const;

const result = await streamText({ model: models[picked], messages });

tools / messages の部分は変わらない。コスト最適化(cheap → smart に escalate)特化(long_context は要約用) を 1 行差で切り替えられる。

つまずいたポイント

  • prepareSendMessagesRequest の型が緩い:messagesUIMessage[] だが、戻り値の bodyunknown 寄りなので satisfies を使って型を縛る
  • UIMessageparts 型変更:v3 系で m.content が deprecate、m.parts 配列に rendering の責任が移った。古い記事の m.content 直アクセスは動かない
  • toUIMessageStreamResponse() のヘッダ:カスタム CORS / cache-control を足したい時は result.toUIMessageStreamResponse({ headers: { ... } })
  • statusstreaming のままで止まる:server 側で例外を catch せずに throw すると stream が hang。route handler 全体を try / catch
  • tool の inputSchema は Zod 必須:standard schema 系(valibot 等)でも動くが、SDK のドキュメントは Zod 想定

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books/9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る

全ガイドは ガイド一覧 から。連載は 連載一覧 へ。