ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-05-01中級

Antigravity で React Server Components の境界を整理する — Next.js 16 で迷わなくなるワークフロー

Server / Client の境界判断は Next.js 16 で最も詰まりやすいポイントです。Antigravity を境界設計のレビュアーとして使い、`use client` の貼り間違いとシリアライズエラーをゼロに近づける個人開発の実践ワークフローを共有します。

antigravity435react-server-componentsnextjs-16typescript26

"use client" を貼り間違えて、Functions cannot be passed directly to Client Components というエラーに半日溶かしたこと、ありませんか。私はあります。React Server Components(RSC)が当たり前になった Next.js 16 でも、この境界判断は今も毎週のように手を止めさせてくれます。

Antigravity に乗り換えてから、この境界の引き直しが「コードと一緒に考える対話」に変わりました。今日は、私が個人プロダクトで毎日使っている、RSC の境界設計を Antigravity に手伝ってもらうワークフローを正直にまとめます。テンプレ的なチュートリアルは省いて、実際に詰まったケースだけを書きます。

なぜ「use client を貼ればいい」では済まないのか

入門記事だと「インタラクティブな部分には use client を付けましょう」で終わります。これは半分正解で、半分は罠です。実際に詰まるのは、たいてい次の4パターンです。

  • Server Component から Client Component に「関数」を渡してしまう: onSelect のようなコールバックは Server 側でクロージャを作るとシリアライズできません
  • use client を貼った瞬間にツリー全体がクライアント送りになる: <Layout> に貼ると配下の Server Component の利点が一気に消えます
  • Server Action と Client Component の Promise の扱いを混同する: await を Client 側で待つか、Server 側で resolve してから渡すかで挙動が違います
  • headers() / cookies() を Client Component の useEffect で呼ぼうとする: 動かないどころか、デプロイ後にだけエラーが出ることもあります

ここで悩ましいのは、これらが「コードを書いた瞬間」ではなく、「ビルド時」や「本番だけ」で発覚することです。ローカルでは動くのにプレビュー環境で落ちる、という体験は地味に消耗します。

Antigravity に「境界の番人」を任せる発想

Antigravity の良いところは、コードを書く前に「設計の対話」ができる点です。私はこれを、RSC の境界判断にも応用しています。具体的には次の3ステップです。

  1. 新しいコンポーネントを書く前に、Antigravity に意図を伝える: 「このコンポーネントは検索結果を表示するもので、入力フィールドの状態を持つ」のように、UI と状態の責務を1〜2行で書く
  2. Antigravity に境界の提案を求める: 「Server / Client / Server Action のどこに何を置くか提案して」と頼む
  3. 生成された分割案にコメントで反論する: 「なぜここを Client にしたのか?」「Server Action ではなく fetch にしない理由は?」と聞き返すと、根拠が返ってくる

このステップを踏むようになってから、"use client" を後から付けたり外したりする手戻りが激減しました。コードを書き終えた後ではなく、書く前に境界を決めるのがポイントです。

実際のプロンプト例(コピペで使える形)

このコンポーネントを Next.js 16 (App Router) で実装したいです。

要件:
- /search で検索結果を一覧表示
- クエリは URL パラメータ ?q= から取得
- 結果クリックで詳細モーダルを開く(モーダル内は別 fetch)
- ページネーションは「もっと見る」ボタン方式

制約:
- Server Component を可能な限り維持し、Client は最小範囲に閉じる
- Server Action は使わずに、Route Handler で API を分離

出力してほしいもの:
1. ファイル分割案(page.tsx / SearchList.tsx / DetailModal.tsx ...)
2. 各ファイルの先頭に貼るディレクティブ("use client" / なし)
3. props として何を渡すか、その型

このようにプロンプトで「制約」と「出力フォーマット」を最初に固定すると、Antigravity の提案が散らからず、レビューしやすくなります。

実践: useState を含むコンポーネントを切り出す3ステップ

私が一番よく使う作業フローを紹介します。たとえば「検索結果ページ」を作るとして、ナイーブに書くとこうなります。

// app/search/page.tsx — これだと全部 Client 送りになる
"use client";
 
import { useState } from "react";
 
export default function SearchPage({ searchParams }) {
  const [query, setQuery] = useState(searchParams.q ?? "");
  const [results, setResults] = useState([]);
  // ... fetch して setResults
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
    </>
  );
}

これを Antigravity に「Server を最大限残したい」と伝えると、次のような分割を返してきます。

// app/search/page.tsx — Server Component のまま
import { SearchInput } from "./SearchInput";
import { ResultList } from "./ResultList";
 
export default async function SearchPage({ searchParams }: { searchParams: { q?: string } }) {
  const query = searchParams.q ?? "";
  // Server 側で fetch 完了させる
  const results = query
    ? await fetch(`${process.env.API_BASE}/search?q=${encodeURIComponent(query)}`, { cache: "no-store" }).then(r => r.json())
    : [];
 
  return (
    <>
      <SearchInput initialQuery={query} />
      <ResultList items={results} />
    </>
  );
}
// app/search/SearchInput.tsx — 入力だけ Client
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useTransition } from "react";
 
export function SearchInput({ initialQuery }: { initialQuery: string }) {
  const router = useRouter();
  const params = useSearchParams();
  const [value, setValue] = useState(initialQuery);
  const [isPending, startTransition] = useTransition();
 
  function submit() {
    const next = new URLSearchParams(params);
    next.set("q", value);
    startTransition(() => router.push(`/search?${next.toString()}`));
  }
 
  return (
    <form onSubmit={e => { e.preventDefault(); submit(); }}>
      <input value={value} onChange={e => setValue(e.target.value)} />
      <button type="submit" disabled={isPending}>検索</button>
    </form>
  );
}

ResultList は静的な表示専用なので Server Component のままです。これで Client 送りされるのは SearchInput の20行程度に圧縮されました。「期待する出力」は、ブラウザの DevTools で SearchInput だけがハイドレーション対象になり、リスト部分は HTML として配信されることです。Lighthouse で計測すると、ナイーブ版より TBT が明らかに減ります。

ポイントは、useTransition を Antigravity に必ず提案してもらうこと。URL 遷移中の「もっさり感」を消すために必要なのに、入門記事では飛ばされがちです。

よくある躓き: Server Action から Client に値を渡したい

次のコードは多くの人が書いて、多くの人がエラーで止まります。

// ❌ こう書くと「Functions cannot be passed」エラー
export default function Page() {
  async function deleteItem(id: string) {
    "use server";
    await db.items.delete(id);
  }
  return <ItemList onDelete={deleteItem} />; // ItemList が Client だと爆発
}

直感的には動きそうですが、Server Component の中で定義した関数は、Client Component の props には渡せません。Antigravity に相談すると、たいてい次の2案を提示してくれます。

// ✅ 案1: actions.ts に切り出して Client 側で import
// app/items/actions.ts
"use server";
export async function deleteItem(id: string) {
  await db.items.delete(id);
}
 
// app/items/ItemList.tsx
"use client";
import { deleteItem } from "./actions";
import { useTransition } from "react";
 
export function ItemList({ items }: { items: Item[] }) {
  const [isPending, startTransition] = useTransition();
  return items.map(item => (
    <li key={item.id}>
      {item.title}
      <button onClick={() => startTransition(() => deleteItem(item.id))} disabled={isPending}>削除</button>
    </li>
  ));
}
// ✅ 案2: bind で id を埋め込んでフォーム経由
import { deleteItem } from "./actions";
 
export function ItemRow({ item }: { item: Item }) {
  const action = deleteItem.bind(null, item.id);
  return (
    <form action={action}>
      <span>{item.title}</span>
      <button type="submit">削除</button>
    </form>
  );
}

私は普段、楽観的更新が必要なら案1、純粋な遷移ならば案2を選びます。Antigravity に「楽観的更新あり/なしのどちらに寄せたいか」を伝えると、useOptimistic までセットで提案してくれることが多いです。

私が個人開発で使っているチェックリスト

新しいページを書き始めるとき、私は次の問いを Antigravity に最初に投げます。これだけで、後から境界を引き直す手戻りがほぼ消えました。

  • このページで「ユーザー操作で状態が変わる」要素はどれか
  • そのうち、URL パラメータで表せるものはないか(表せるなら Server で済む)
  • データ取得は Server で完結できるか、それとも Client で SWR / TanStack Query を使うべきか
  • 認証情報(cookies() / headers())が必要なら、どの境界で読むか
  • フォーム送信は Server Action か Route Handler か(権限チェックの場所が変わる)

こうした「設計レビュー」を1分で済ませてくれるのが、AI と一緒に開発する最大の利点だと感じています。Antigravity 単体では境界を間違えることもありますが、こちらが要件を絞って渡せば、ほぼ正解に近い分割案を返してくれます。

関連する設定・移行のヒント

Next.js 16 への移行や、エディタ側の設定で詰まったときは、過去の解説記事も参考にしてみてください。

全体を振り返って

Server / Client の境界判断は、慣れれば反射でできますが、その慣れに到達する前に消耗するのが現実です。今日試してみるとしたら、「次に書く新しいページの分割案を、コードを書く前に Antigravity に投げてみる」だけでも、感覚が変わるはずです。私はこの一歩で、"use client" を後から書き換える時間が週に2〜3時間は減りました。

個人開発でエージェントを使い倒した 6 週間の所感

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-05-03
Antigravity × TypeScript で作る冪等性キーと重複排除ストア本番設計ガイド
Stripe Webhook の二重課金や Temporal ワークフローの再実行で起きる重複処理を、冪等性キーと重複排除ストアで止める本番設計を、Antigravity を伴走者にして TypeScript で組み上げる実践ガイドです。
アプリ開発2026-04-30
Antigravity × Better Auth でモダンTypeScript認証基盤を構築する
NextAuth疲れに終止符を打つBetter AuthをAntigravityで導入する実践ガイド。スキーマ自動生成・OAuth・RBAC・Passkeyまで本番運用パターンを網羅します。
アプリ開発2026-04-29
Antigravity と Inngest で組み立てるイベント駆動 AI ワークフロー — 本番運用を見据えた実装ガイド
Antigravity の AI エージェントと Inngest を組み合わせて、リトライ・冪等性・ヒューマン・イン・ザ・ループまで備えたイベント駆動 AI ワークフローを本番投入する実装手順を解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →