"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ステップです。
- 新しいコンポーネントを書く前に、Antigravity に意図を伝える: 「このコンポーネントは検索結果を表示するもので、入力フィールドの状態を持つ」のように、UI と状態の責務を1〜2行で書く
- Antigravity に境界の提案を求める: 「Server / Client / Server Action のどこに何を置くか提案して」と頼む
- 生成された分割案にコメントで反論する: 「なぜここを 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 への移行や、エディタ側の設定で詰まったときは、過去の解説記事も参考にしてみてください。
- Antigravity × Next.js のフレームワーク連携ガイド
- Antigravity でラージコードベースをリファクタリングする実務戦略
- Antigravity の Plan モードと Fast モードの実用的な使い分け
全体を振り返って
Server / Client の境界判断は、慣れれば反射でできますが、その慣れに到達する前に消耗するのが現実です。今日試してみるとしたら、「次に書く新しいページの分割案を、コードを書く前に Antigravity に投げてみる」だけでも、感覚が変わるはずです。私はこの一歩で、"use client" を後から書き換える時間が週に2〜3時間は減りました。