GEMINI LABEN
NANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定ですNANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定です
記事一覧/API / SDK
API / SDK/2026-04-29上級

Gemini API × TanStack Query でストリーミングUI を本番運用する — キャンセル・再試行・キャッシュ整合の設計

TanStack Query は通常の REST/JSON 用に最適化されており、ストリーミング応答との相性は素直ではありません。 Gemini API のSSEを TanStack Query で扱う際の落とし穴と、本番運用に耐える設計パターンを解説します。

Gemini API191TanStack QueryReact2StreamingProduction3

プレミアム記事

Gemini API × TanStack Query でストリーミングUI を本番運用する — キャンセル・再試行・キャッシュ整合の設計

「TanStack Query は便利だから AI チャットの状態管理にも使おう」と思って useMutation で書き始めたものの、ストリーミングが始まった瞬間に手が止まった——そんな経験はないでしょうか。私も最初に試したとき、ストリームの途中経過を画面に出すまでは比較的すぐにできたのですが、ユーザーが連打したときの重複ストリーム、タブ復帰時のリトライ暴走、エラー後の中途半端な表示、と本番で必ず踏む地雷を一つずつ踏んでいきました。

TanStack Query は本来「リクエストを送って JSON が返ってくる」モデル向けに最適化されているライブラリです。ストリーミング応答(SSE や ReadableStream)はその外側にあるため、素直に組み合わせると上手く噛み合いません。ここではGemini API のストリーミングを TanStack Query と統合する際に私が現場で詰まったポイントと、それを解消するための設計パターンを、コピペで動くコード付きで紹介します。

なぜ TanStack Query + ストリーミングは噛み合いにくいのか

TanStack Query が担うのは「同じキーに対するリクエスト結果のキャッシュ・重複排除・再取得」です。前提として「リクエストは一度のレスポンスで完了する」ことが想定されています。一方ストリーミング応答は、複数のチャンクが時間差で届き、途中でキャンセルされる可能性があり、最終結果が「最後のチャンク」ではなく「全チャンクの結合」であるという、根本的に違う性質を持っています。

この性質の違いを理解せずに useQuery で書こうとすると、select で全チャンクを保持しようとして再レンダリングが暴れたり、staleTime の概念が当てはまらず再取得が混乱したりします。私がたどり着いた結論は、ストリーミング自体は useMutation で扱い、確定した結果のみ queryClient.setQueryData でキャッシュに同期する という分離設計です。

データフローを整理するとこうなります。

  • ユーザー入力で mutate() を呼ぶ
  • useMutationmutationFn 内で ReadableStream を消費する
  • 各チャンクを onChunk 相当のローカル state に流してストリーム表示
  • ストリーム完了時に queryClient.setQueryData(['conversation', id], 全文) でキャッシュに反映
  • 他コンポーネントは useQuery で読み取り

この役割分担にすることで、TanStack Query の強み(キャッシュ・無効化・楽観的更新)はそのまま使いつつ、ストリーミング特有の制御は通常の React state で扱えるようになります。

サーバー側:Next.js Route Handler での Gemini SSE 実装

まずはサーバー側を組みます。Next.js の App Router 環境を想定し、Edge Runtime ではなく Node.js Runtime で動かしています(Edge は特定のSDK機能で制約が出る場合があるため、本番では Node.js から始めるのが無難です)。

// app/api/chat/stream/route.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
 
export const runtime = "nodejs";
export const dynamic = "force-dynamic"; // キャッシュ抑止
 
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
 
export async function POST(req: Request) {
  const { messages, conversationId } = await req.json();
 
  // クライアント切断検知用
  const abortSignal = req.signal;
 
  const model = genAI.getGenerativeModel({
    model: "gemini-2.5-flash",
    generationConfig: { temperature: 0.7, maxOutputTokens: 2048 },
  });
 
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        const result = await model.generateContentStream({
          contents: messages.map((m: { role: string; content: string }) => ({
            role: m.role === "assistant" ? "model" : "user",
            parts: [{ text: m.content }],
          })),
        });
 
        for await (const chunk of result.stream) {
          // クライアント側がキャンセルしていたら即終了
          if (abortSignal.aborted) {
            controller.close();
            return;
          }
          const text = chunk.text();
          if (!text) continue;
          // SSE フォーマット: "data: <JSON>\n\n"
          const payload = JSON.stringify({ type: "chunk", text });
          controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
        }
        // 完了シグナル
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ type: "done", conversationId })}\n\n`)
        );
        controller.close();
      } catch (err: unknown) {
        const message = err instanceof Error ? err.message : "stream error";
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ type: "error", message })}\n\n`)
        );
        controller.close();
      }
    },
  });
 
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no", // Nginx/Cloudflare 経由でもバッファされない
    },
  });
}

ここでのポイントは三つあります。第一に、req.signal をループ内で確認することで、クライアントが切断したときに即座に Gemini API への接続を閉じられます。これが無いと、ユーザーがブラウザを閉じてもサーバー側はストリームを最後まで読み続け、無駄なトークン消費が発生します。第二に、エラーをHTTPステータスではなく SSE の type: "error" として送る設計にしています。これは、ヘッダーを送り終えた後にエラーが発生してもクライアント側で型安全に扱えるようにするためです。第三に X-Accel-Buffering: no ヘッダーで中間プロキシのバッファリングを無効化しています。これを忘れると Cloudflare や Nginx の前段で数秒バッファされてしまい、「ストリーミングなのにまとめて届く」現象に悩まされます。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
TanStack Query の useMutation + ReadableStream で詰まっていた人が、キャンセル・再試行・部分結果保持まで含む完成形のコードを今日手に入れられる
ストリーミングの途中で switchTab → refocus が起きたときに発生する重複ストリーム・メモリリークを、queryClient と AbortController で正しく制御する設計を習得できる
本番でユーザーが連打したり接続が切れたりする状況でも、UI が壊れず途中までの応答を保持し、サーバー側のトークン消費も最小化する運用パターンに切り替えられる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API / SDK2026-05-24
Gemini API のリクエスト Hedging で p99 レイテンシのテールを抑える設計
Gemini API のテールレイテンシ問題を、二重リクエスト方式の Hedging で攻略する設計記録です。個人開発の本番アプリ運用で得た p50/p95/p99 の実数値と、課金増を 18% に抑えた節度ある実装パターンを公開します。
API / SDK2026-07-19
静止画か短い動画か — Nano Banana 2 Lite と Omni Flash のコスト桁差で機能の置き所を決める
壁紙アプリの目玉アセットを静止画にするか短い動画にするかで手が止まったとき、決め手になったのは好みではなくコストの桁でした。Nano Banana 2 Lite と Omni Flash の単価を同じ土俵に並べ、動く意思決定関数まで落とし込みます。
API / SDK2026-07-18
Managed Agent の長時間走行がサンドボックス再生成で消える前に — チェックポイントと冪等リジュームの設計
Managed Agents のサンドボックスは再生成されます。40分走った処理が振り出しに戻る前に、進捗を外部へ逃がすチェックポイントと、副作用を二度実行しない冪等リジュームを設計します。SQLite で動く実装つき。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →