Next.js App RouterのServer Componentsで3週間詰まった話【RSC初心者あるある失敗5選】
Next.js App RouterでReact Server Componentsを使い始めて3週間、同じようなエラーを繰り返した体験記。useClient忘れ・State使用・キャッシュ挙動など典型的な罠と正解ルートを解説。
※当サイトはアフィリエイトプログラムに参加しています。記事内のリンクから商品を購入すると、当サイトに報酬が支払われることがあります。詳しくはプライバシーポリシーをご覧ください。
はじめに:「え、なんで動かないの…」を3週間繰り返した話
4月の最初の週に、久しぶりに新しいNext.jsプロジェクトをゼロから立ち上げた。
クライアントの案件で「App Routerを使いたい」と言われたのが発端です。それまで僕はPages Routerしか触ったことがなかったので、正直かなり身構えた。
結論から言うと、App RouterとReact Server Componentsは「Page Routerの延長線上」ではなく、ほぼ別物です。
その認識がなかったせいで、最初の3週間は本当にひどかった。同じようなエラーを何度も出して、毎回「あれ、なんでだっけ」って1〜2時間くらい溶かしていた。
この記事は、そのときの失敗記録です。同じところでハマっている人の時間を、少しでも節約できたら嬉しい。
(GWも近いし、連休中にApp Routerを触ってみようとしている人もいるんじゃないかと思って、今このタイミングで書いています。)
前提:Server ComponentsとClient Componentsの「本質的な違い」
失敗の話に入る前に、ここだけ確認させてください。
App Routerの世界では、Reactコンポーネントはデフォルトでサーバー上でしか動かない。
これが、Pages Routerとの最大の違いです。
| 特徴 | Server Components | Client Components |
|---|---|---|
| 実行場所 | サーバー | ブラウザ |
| useState / useEffect | 使えない | 使える |
| fetch(直接) | 使える | 使えない(APIルート経由が必要) |
| ブラウザAPI | 使えない | 使える |
| 宣言方法 | 何も書かない(デフォルト) | 'use client' を先頭に書く |
これを頭に入れた上で、失敗談を読んでください。
罠1:useStateを書いたら「You’re importing a component that needs useState」エラー
何が起きたか
新しいコンポーネントを作って、useStateでフォームの状態を管理しようとした。
// src/app/contact/page.tsx(ダメな例)
import { useState } from 'react';
export default function ContactPage() {
const [name, setName] = useState('');
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
</form>
);
}
エラー文:
You're importing a component that needs useState. It only works in a Client Component
but none of its parents are marked with "use client", so they're all Server Components by default.
なぜ起きるか
App Routerでは、src/app/ 以下のコンポーネントはデフォルトでServer Componentsです。Server ComponentsはNode.jsのサーバー上で動くので、ブラウザのReact機能(useState・useEffect・イベントハンドラ)は使えない。
正解ルート
'use client' を先頭に書くだけ。
// src/app/contact/page.tsx(正しい例)
'use client';
import { useState } from 'react';
export default function ContactPage() {
const [name, setName] = useState('');
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
</form>
);
}
ここがポイントなんですが、'use client' はファイルの本当に先頭(importより上)に書かないと動きません。コメントやimport文の後に書いてしまって「なぜか効かない」という目に、僕は一度遭いました。
罠2:Server Componentの中にClient Componentを直接importしたらエラー
何が起きたか
Server ComponentとClient Componentを組み合わせれば効率よく使えると思って、こんなコードを書いた。
// src/app/dashboard/page.tsx(ダメな例)
// このファイルはServer Component('use client'なし)
import InteractiveWidget from '@/components/InteractiveWidget'; // Client Component
async function getData() {
const res = await fetch('https://api.example.com/data');
return res.json();
}
export default async function DashboardPage() {
const data = await getData();
return (
<div>
<h1>ダッシュボード</h1>
{/* Propsとしてfetch結果のオブジェクトをそのまま渡した */}
<InteractiveWidget data={data} />
</div>
);
}
// src/components/InteractiveWidget.tsx
'use client';
export default function InteractiveWidget({ data }: { data: any }) {
// ...
}
一見問題なさそうに見えるんですが、data の中身が「シリアライズできないオブジェクト」だとエラーになります。
なぜ起きるか
Server ComponentからClient ComponentにPropsを渡す場合、そのデータはシリアライズ(JSON化)できる形式でないといけない。クラスインスタンス・関数・DateオブジェクトなどをそのままPropsに渡すと爆発する。
正解ルート
Propsに渡すデータは、JSON.stringifyできる形に変換してから渡す。または、データ取得ロジック自体をClient Component側に移す(SWRやReact Queryを使う)。
// Server Componentからは「プリミティブな値」だけ渡す
export default async function DashboardPage() {
const data = await getData();
return (
<div>
<InteractiveWidget
id={data.id} // string OK
title={data.title} // string OK
count={data.count} // number OK
// createdAt={data.createdAt} // Date型はNG → .toISOString()に変換
createdAt={data.createdAt.toISOString()}
/>
</div>
);
}
罠3:fetchのキャッシュが効きすぎて、データが更新されない
何が起きたか
これが一番ハマった。正直、当時の僕は「え、fetch呼んでるのになんでデータ古いの?」という状態で、2〜3時間くらいうろうろしていた。
// src/app/posts/page.tsx
export default async function PostsPage() {
// デフォルトだとNext.jsがこのレスポンスをキャッシュする
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
管理画面でデータを更新しても、ページをリロードしても古いデータが表示される。「え、ブラウザのキャッシュ?」とChromeのキャッシュをクリアしても変わらない。
なぜ起きるか
Next.js App Routerは、Server Components内のfetchのレスポンスをデフォルトでキャッシュします。これは「同じURLへのfetchは1回だけ実行し、結果を再利用する」という最適化。
問題は、このキャッシュが思ったより強力で、ビルド時にも適用されること。本番デプロイ後にAPIのデータが変わっても、キャッシュが残っていると古いデータを返し続ける。
正解ルート
ユースケースに合わせてキャッシュ戦略を明示する。
// パターン1: キャッシュしない(常に最新データを取得)
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store'
});
// パターン2: 一定時間ごとに再検証(ISR的な動作)
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 } // 60秒ごとに再検証
});
// パターン3: revalidateタグを使って手動で無効化
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] } // 'posts'タグを持つキャッシュをまとめて無効化できる
});
(ちょっと話逸れるけど、このキャッシュ戦略の選択って、Supabase + Next.jsの構成を使うときにも同じ問題が起きます。Supabaseのデータが更新されてもページに反映されない、みたいな。)
罠4:useRouter・usePathname・useSearchParamsが使えない
何が起きたか
現在のURLパラメータを読みたくて、useSearchParamsを使おうとした。
// src/app/search/page.tsx(ダメな例)
import { useSearchParams } from 'next/navigation';
export default function SearchPage() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <div>検索ワード: {query}</div>;
}
エラー:
useSearchParams() should be wrapped in a suspense boundary at the page "/search".
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
なぜ起きるか
useSearchParamsをはじめとしたnext/navigationのhookは、Client Componentsでしか使えません。かつ、useSearchParamsを使うコンポーネントは<Suspense>で囲む必要がある。
正解ルート
// src/app/search/page.tsx(正しい例)
import { Suspense } from 'react';
import SearchResult from '@/components/SearchResult';
export default function SearchPage() {
return (
<Suspense fallback={<div>ローディング中...</div>}>
<SearchResult />
</Suspense>
);
}
// src/components/SearchResult.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchResult() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <div>検索ワード: {query}</div>;
}
useSearchParamsを使うコンポーネントを切り出して、それを<Suspense>で包んだ親コンポーネントから呼び出す構造にする。
罠5:Server ComponentからServer Actionを呼ぼうとして混乱した
何が起きたか
フォームの送信処理をServer Actionで書こうとしたのだが、どのファイルにどう書けばいいかで混乱した。
// src/app/contact/page.tsx(混乱した状態)
'use client'; // フォームのためにClient Componentにした
// でも、Server Actionはどこに書けば...?
// このファイル内に書いていいの?
async function submitForm(formData: FormData) {
'use server'; // ← Client Componentのファイル内にこれを書いたらどうなる?
// ...
}
正解ルート
Server Actionは別ファイルに切り出すのが基本。
// src/app/actions.ts(Server Action専用ファイル)
'use server';
export async function submitForm(formData: FormData) {
const name = formData.get('name') as string;
// DBへの保存など
console.log('サーバー側で受け取った:', name);
}
// src/app/contact/page.tsx(Client Component)
'use client';
import { submitForm } from '@/app/actions';
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="name" />
<button type="submit">送信</button>
</form>
);
}
ここがポイントなんですが、'use server' を持つ関数は、Client Componentのファイル内には書けません(技術的には書けるが、モジュールレベルで 'use server' を宣言したファイルでないと意図通りに動かないケースがある)。専用の actions.ts に切り出すのが最も安全です。
正解ルートのまとめ:RSCで迷わないための判断フロー
3週間の失敗から学んだ、「Server ComponentかClient Componentか」の判断フローです。
新しいコンポーネントを作る
↓
useState / useEffect / イベントハンドラが必要?
YES → 'use client' を書く(Client Component)
NO ↓
fetch / DBアクセス / 秘匿APIキーを使う?
YES → Server Componentのまま(デフォルト)
NO ↓
ブラウザAPIが必要(window / document / localStorage)?
YES → 'use client' を書く
NO → Server Componentのまま(デフォルト)
基本的には「必要になったら'use client'を付ける」という後付けアプローチで問題ありません。最初から全部Client Componentにしてしまうと、Server Componentsのメリット(サーバー側でのデータ取得・バンドルサイズ削減)が消えてしまうので要注意。
ここまで読んでくれた人に正直に言うと
App Routerは、正直最初の1〜2週間はかなりつらいです。
エラーメッセージは英語だし、「Server Component / Client Component」という概念が頭に染み付くまで、毎回「どっちだっけ」ってなります。
でも、仕組みが分かってくると、Pages Routerより明らかに設計がクリーンだと感じるようになります。「このコンポーネントはサーバー側、あっちはクライアント側」と責任が明確になるので、大きなプロジェクトになるほどメリットが出てくる。
関連して、状態管理の選択についても迷いがちです。RSCを使いながらクライアント側の状態管理をどうするかは、Zustand vs Redux の比較記事が参考になります。また、そもそも「Next.jsを選ぶべきかどうか」という段階の方は、Next.js vs React/CRA の比較記事を先に読むといいかもしれません。
TypeScriptの型エラーがRSCの罠と絡まってくるケースも多いので、TypeScript入門記事も合わせて読んでおくと理解が早いです。
よくある質問
Q: Server ComponentsとClient Components、どっちをメインに使えばいいですか?
A: 基本的にはServer Componentsをデフォルトとして使い、インタラクション(クリック・フォーム・アニメーション)が必要な部分だけClient Componentsにするのがおすすめです。App Routerの設計思想がそういう方向を向いています。Client Componentsを多用すると、Pages Routerと変わらなくなってしまいます。
Q: ‘use client’ディレクティブはどのファイルに書けばいいですか?
A: useState・useEffect・イベントハンドラ・ブラウザAPIを使うファイルの先頭に書きます。'use client'は「境界」を定義するもので、そのファイルと、そのファイルがimportするコンポーネント全体がClient Component扱いになります。できるだけツリーの末端(葉)に近いコンポーネントに書くのがベストプラクティスです。
Q: Server Componentsでfetchするとキャッシュされすぎて困るんですが、どうすれば?
A: fetchオプションで cache: 'no-store'(キャッシュしない)または next: { revalidate: N }(N秒ごとに再検証)を指定するのが最速の解決策です。常にリアルタイムデータが必要なAPIにはcache: 'no-store'を、ある程度の静的さが許容できるデータにはrevalidateを使い分けるのが現実的です。
Q: Page RouterからApp Routerに移行するのは難しいですか?
A: 難しいです、正直に言うと。特にgetServerSideProps・getStaticPropsの代替パターンを理解するまでが一番しんどい。ただ、新規プロジェクトからApp Routerで始めるのは今がベストタイミングだと思います。移行コストについてはNext.jsのデプロイ比較記事でも少し触れています。
あわせて読みたい
- Next.js + Tailwind CSSのセットアップ手順(2026年版)
- Next.js vs React/CRA どっちを選ぶべき?(2026年版)
- ZustandとReduxを比較|Next.jsで使う状態管理ライブラリの選び方
- フロントエンドテスト入門|Jest・Vitestの使い方(2026年版)
- Supabase + Next.jsでポートフォリオを作った話
- React vs Vue どっちを学ぶべき?(2026年版)
- BunとNode.jsを1ヶ月使い比べた報告書(2026年版)
【2026年4月追記】この記事を書いた後、Next.js 15.2のリリースがあり、エラーメッセージが少し改善されました。「You’re importing a component that needs…」系のエラーが、より具体的に「どのファイルの何行目が問題か」を示してくれるようになっています。ただ、罠の本質は変わっていないので、この記事の内容はそのまま有効です。GW中にApp Routerを触ってみる方、ぜひ参考にしてください。