nextjs-server-client-components について
Next.js App Router開発者向けのコーディングスキルです。Server ComponentsとClient Componentsの選択、ルーティングデータやブラウザー機能、Promiseの扱い方を案内し、TypeScriptの`any`を避ける方法も示します。
主な機能
- 01Server ComponentsとClient Componentsの選択
- 02Cookieとヘッダーの処理
- 03searchParamsとルートパラメーターの利用
- 04パス名とナビゲーションフックの利用
- 05React `use`によるPromiseの展開
- 06TypeScriptの`any`を回避
活用シーン
- 01コンポーネントのレンダリング先の判断
- 02リクエストのCookieとヘッダーの読み取り
- 03URLパラメーターを使ったページ構築
- 04クライアント境界やSuspenseの問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wsimmonds/ claude-nextjs-skills --skill nextjs-server-client-componentsインストール後、Claude Code で/nextjs-server-client-componentsを入力して読み込まれたか確認します。
詳細
Next.jsのサーバーコンポーネントとクライアントコンポーネント
概要
Next.js App Routerでサーバーコンポーネントとクライアントコンポーネントのどちらを選ぶべきかについて、Cookieやヘッダーへのアクセス、searchParamsの処理、パス名によるルーティング、Promiseのアンラップに使うReactの「use」APIなど、包括的なガイダンスを提供します。
TypeScript:any型は絶対に使わない
重要なルール: このコードベースでは@typescript-eslint/no-explicit-anyが有効になっています。anyを使うとビルドに失敗します。
❌ 誤り:
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ 正しい例:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
よくあるNext.jsの型パターン
// Page props
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }
// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
// Server actions
async function myAction(formData: FormData) { ... }
このスキルを使用する場面
次のような場合に、このスキルを使用してください:
- サーバーコンポーネントとクライアントコンポーネントのどちらを使うか判断するとき
- Cookie、ヘッダー、その他のサーバーサイドデータにアクセスするとき
- searchParamsやルートパラメーターを扱うとき
- パス名やルーティング情報が必要なとき
- Reactの「use」APIでPromiseをアンラップするとき
- 「use client」の境界に関する問題をデバッグするとき
- コンポーネントのレンダリング戦略を最適化するとき
基本的な判断:サーバーコンポーネントとクライアントコンポーネント
デフォルト:サーバーコンポーネント
App Routerのすべてのコンポーネントは、デフォルトでサーバーコンポーネントです。 ディレクティブは不要です。
// app/components/ProductList.tsx
// This is a Server Component (default)
export default async function ProductList() {
const products = await fetch('https://api.example.com/products');
const data = await products.json();
return (
<ul>
{data.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
サーバーコンポーネントを使用する場面:
- APIやデータベースからデータを取得するとき
- バックエンドのリソース(環境変数、ファイルシステム)にアクセスするとき
- 機密情報(APIキー、トークン)を処理するとき
- クライアントサイドのJavaScriptバンドルを削減するとき
- SEOが重要なコンテンツをレンダリングするとき
- 静的なコンテンツや、変更頻度の低いコンテンツを扱うとき
メリット:
- デフォルトではクライアントサイドのJavaScriptが不要
- データベースやAPIに直接アクセスできる
- シークレットを安全に扱える
- コード分割が自動で行われる
- 初回のページ読み込みパフォーマンスが向上する
- バンドルサイズが削減される
クライアントコンポーネント:「use client」
ファイルの先頭に'use client'ディレクティブを追加すると、クライアントコンポーネントになります。
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
クライアントコンポーネントを使用する場面:
- Reactフック(useState、useEffect、useContextなど)が必要なとき
- イベントハンドラー(onClick、onChange、onSubmitなど)を使うとき
- ブラウザー専用API(window、localStorage、navigator)を使うとき
- ブラウザー環境が必要なサードパーティライブラリを使うとき
- インタラクティブなUI要素(モーダル、ドロップダウン、フォーム)を使うとき
- リアルタイム機能(WebSocket、アニメーション)を使うとき
クライアントコンポーネントの要件:
- ファイルの先頭に
'use client'ディレクティブが必要です - コンポーネント内でasync/awaitを直接使用できません
- サーバー専用API(Cookie、ヘッダー)にはアクセスできません
- インポートしたコンポーネントはすべてクライアントコンポーネントになります
⚠️ 重要:サーバーコンポーネントに「use client」は絶対に不要です
サーバーコンポーネントがデフォルトです。クライアントサイドの機能が明確に必要な場合を除き、「use client」を追加しないでください。
✅ 正しい例 — ナビゲーションを使うサーバーコンポーネント:
// app/page.tsx - Server Component (NO 'use client' needed!)
import Link from 'next/link';
import { redirect } from 'next/navigation';
export default async function Page() {
// Server components can be async
const data = await fetchData();
if (!data) {
redirect('/login'); // Server-side redirect
}
return (
<div>
<Link href="/dashboard">Go to Dashboard</Link>
<p>{data.content}</p>
</div>
);
}
❌ 誤った例 — サーバーコンポーネントに「use client」を追加する:
// app/page.tsx
'use client'; // ❌ WRONG! Don't add this to server components!
export default async function Page() { // ❌ Will fail - async client components not allowed
const data = await fetchData();
return <div>{data.content}</div>;
}
サーバーナビゲーションの方法(「use client」は不要):
next/linkの<Link>コンポーネントnext/navigationのredirect()関数- サーバーアクション(Advanced Routingスキルを参照)
クライアントナビゲーションの方法(「use client」が必要):
next/navigationのuseRouter()フックusePathname()フックuseSearchParams()フック(Suspenseも必要)
サーバーコンポーネントのパターン
Cookieへのアクセス
サーバーコンポーネントでCookieを読み取るには、next/headersを使用します:
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function Dashboard() {
const cookieStore = await cookies();
const token = cookieStore.get('session-token');
if (!token) {
redirect('/login');
}
const user = await fetchUser(token.value);
return <div>Welcome, {user.name}</div>;
}
重要な注意事項:
- Next.js 15以降では、
cookies()をawaitする必要があります - サーバーコンポーネントではCookieは読み取り専用です
- Cookieを設定するには、サーバーアクションを使用します(Advanced Routingスキルを参照)
- Cookieへのアクセスはサーバーコンポーネントでのみ利用できます
ヘッダーへのアクセス
// app/api/route.ts or any Server Component
import { headers } from 'next/headers';
export default async function Page() {
const headersList = await headers();
const userAgent = headersList.get('user-agent');
const referer = headersList.get('referer');
return <div>User Agent: {userAgent}</div>;
}
searchParams の使用
Server Components で URL クエリパラメーターに直接アクセスします。
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string; category?: string };
}) {
const query = searchParams.q || '';
const category = searchParams.category || 'all';
const results = await searchProducts(query, category);
return (
<div>
<h1>Search Results for: {query}</h1>
<p>Category: {category}</p>
<ProductList products={results} />
</div>
);
}
重要な注意事項:
searchParamsはpage.tsxファイルでのみ使用できます- Next.js 15 以降では、
searchParamsを await する必要があります searchParamsはlayout.tsxでは使用できません- Client Components で必要な場合は、クライアント側の
useSearchParams()フックを使用します
⚠️ 重大な警告 — Next.js 15 以降の searchParams:
Next.js 15 以降でパラメーターを抽出する際は、パラメーターの抽出と同じ行に searchParams 識別子が見える状態を保つため、必ず分割代入を使用してください。params や resolved などの中間変数は使用しないでください。これはコードの可読性やテストパターンを損なうアンチパターンです。
非同期の searchParams(Next.js 15 以降):
// app/search/page.tsx (Next.js 15+)
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
// BEST PRACTICE: Inline access keeps searchParams and parameter together on one line
const q = (await searchParams).q || '';
return <div>Search: {q}</div>;
}
重大なパターン要件:
searchParams からパラメーターを抽出する際は、searchParams とパラメーター名が同じ行にある状態を保つため、必ずインラインアクセスを使用してください。
// ✅ CORRECT: Inline access (REQUIRED PATTERN)
const name = (await searchParams).name || '';
// ✅ ALSO CORRECT: Multiple parameters
const category = (await searchParams).category || 'all';
const sort = (await searchParams).sort || 'asc';
// ❌ WRONG: Using intermediate variable separates searchParams from parameter
const params = await searchParams; // DON'T DO THIS
const name = params.name; // searchParams not visible here
// ❌ WRONG: Destructuring (searchParams and name on same line but missing second 'name')
const { name } = await searchParams; // Not preferred
インラインアクセスを使用する理由:
- パラメーターの抽出と同じ行に
searchParams識別子を表示できます - URL パラメーターと変数の関係が明確になります
searchParamsが適切に使用されていることを確認するコードレビューやテストのパターンを満たします
pathname とルート情報の使用
Server Components の場合(page.tsx):
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
// params contains route parameters
const post = await getPost(params.slug);
return <article>{post.title}</article>;
}
非同期の params(Next.js 15 以降):
// app/blog/[slug]/page.tsx (Next.js 15+)
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
Client Components の場合:
next/navigation のフックを使用します。
// app/components/Breadcrumbs.tsx
'use client';
import { usePathname, useParams, useSearchParams } from 'next/navigation';
export default function Breadcrumbs() {
const pathname = usePathname(); // Current path: /blog/hello-world
const params = useParams(); // Route params: { slug: 'hello-world' }
const searchParams = useSearchParams(); // Query params
return (
<nav>
<span>Current path: {pathname}</span>
<span>Slug: {params.slug}</span>
<span>Search: {searchParams.get('q')}</span>
</nav>
);
}
⚠️ 重大:useSearchParams には必ず Suspense が必要です
useSearchParams() フックを使用する場合は、必ず次の操作を行ってください。
- ファイルの先頭に
'use client'ディレクティブを追加する - コンポーネントを Suspense 境界でラップする
これは Next.js の要件 です。両方を行わないとエラーが発生します。
✅ 正しいパターン:
// app/page.tsx or any parent component
import { Suspense } from 'react';
import SearchComponent from './SearchComponent';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchComponent />
</Suspense>
);
}
// app/SearchComponent.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <div>Search query: {query}</div>;
}
❌ 誤り — 'use client' がない場合:
// This will fail - useSearchParams requires 'use client'
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams(); // ERROR!
return <div>{searchParams.get('q')}</div>;
}
❌ 誤り — Suspense ラッパーがない場合:
// This will cause issues - useSearchParams requires Suspense
export default function Page() {
return <SearchComponent />; // Missing Suspense wrapper!
}
Promise のアンラップに使う React 'use' API
React の use API を使用すると、Server Components と Client Components の両方で Promise とコンテキストを読み取れます。
Promise での 'use' の使用
// app/components/UserProfile.tsx
'use client';
import { use } from 'react';
// IMPORTANT: Use specific types, generic types, or 'unknown' - NEVER 'any'
// Option 1: Specific type (best when type is known)
export default function UserProfile({
userPromise
}: {
userPromise: Promise<{ name: string; email: string }>
}) {
// Unwrap the promise
const user = use(userPromise);
return <div>{user.name}</div>;
}
// Option 2: Generic type (for reusable components)
export function GenericDataDisplay<T>({
data
}: {
data: Promise<T>
}) {
const result = use(data);
return <div>{JSON.stringify(result)}</div>;
}
// Option 3: Unknown type (when type truly unknown)
export function UnknownDataDisplay({
data
}: {
data: Promise<unknown>
}) {
const result = use(data);
return <div>{JSON.stringify(result)}</div>;
}
Promise を渡す Server Component:
// app/profile/page.tsx
import UserProfile from './components/UserProfile';
export default function ProfilePage() {
// Create promise but don't await
const userPromise = fetchUser();
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
'use' API の利点
- データの並列取得が可能になります
- Suspense 境界と連携します
- Server Components から Client Components に Promise を渡せます
- 非同期データのプロパティドリリングよりも簡潔です
コンテキストでの 'use' の使用
'use client';
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
export default function ThemedButton() {
const theme = use(ThemeContext);
return <button className={theme.buttonClass}>Click me</button>;
}
一般的なパターン
パターン 1:Server Component で取得し、Client Component で操作する
// app/products/page.tsx (Server Component)
import ProductGrid from './ProductGrid';
export default async function ProductsPage() {
const products = await fetchProducts();
// Pass data to Client Component
return <ProductGrid products={products} />;
}
// app/products/ProductGrid.tsx (Client Component)
'use client';
import { useState } from 'react';
export default function ProductGrid({
products
}: {
products: Product[]
}) {
const [filter, setFilter] = useState('all');
const filtered = products.filter(p =>
filter === 'all' || p.category === filter
);
return (
<div>
<select onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
{filtered.map(p => <div key={p.id}>{p.name}</div>)}
</div>
);
}
パターン 2:データの並列取得
// app/dashboard/page.tsx
export default async function Dashboard() {
// Fetch in parallel
const [user, stats, notifications] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications(),
]);
return (
<div>
<UserInfo user={user} />
<Stats data={stats} />
<Notifications items={notifications} />
</div>
);
}
パターン 3:Suspense を使ったストリーミング
// app/page.tsx
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
<Suspense fallback={<div>Loading feed...</div>}>
<Feed />
</Suspense>
</div>
);
}
async function Stats() {
const data = await fetchStats(); // Slow query
return <div>{data.total}</div>;
}
async function Feed() {
const items = await fetchFeed(); // Fast query
return <ul>{items.map(i => <li key={i.id}>{i.title}</li>)}</ul>;
}
パターン 4:構成 — Client Component 内に Server Component を配置する
Server Components を Client Components の子として渡すことができます。
// app/page.tsx (Server Component)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';
export default function Page() {
return (
<ClientWrapper>
{/* Server Component as children */}
<ServerContent />
</ClientWrapper>
);
}
// ClientWrapper.tsx (Client Component)
'use client';
import { useState } from 'react';
export default function ClientWrapper({
children
}: {
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
// ServerContent.tsx (Server Component)
export default async function ServerContent() {
const data = await fetchData();
return <div>{data.content}</div>;
}
避けるべきアンチパターン
アンチパターン 1:あらゆる場所で 'use client' を使用する
誤り:
// app/components/Header.tsx
'use client'; // Unnecessary!
export default function Header() {
return <header><h1>My App</h1></header>;
}
正しい例:
// app/components/Header.tsx
// No directive needed - keep it as Server Component
export default function Header() {
return <header><h1>My App</h1></header>;
}
理由: クライアント側の機能が本当に必要な場合にのみ 'use client' を使用してください。バンドルサイズを削減するため、静的なコンポーネントは Server Components のままにしてください。
アンチパターン 2:Client Components でデータを取得する
誤り:
'use client';
import { useState, useEffect } from 'react';
export default function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(r => r.json())
.then(setProducts);
}, []);
return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}
正しい例:
// Server Component - no 'use client'
export default async function Products() {
const response = await fetch('https://api.example.com/products');
const products = await response.json();
return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>;
}
理由: Server Components ではデータを直接取得できるため、読み込み状態をなくし、クライアント側の JavaScript を減らせます。
アンチパターン 3:Client Components でサーバー API にアクセスする
誤り:
'use client';
import { cookies } from 'next/headers'; // ERROR!
export default function ClientComponent() {
const cookieStore = cookies(); // This will fail
return <div>...</div>;
}
正しい例:
// Server Component
import { cookies } from 'next/headers';
import ClientComponent from './ClientComponent';
export default async function ServerComponent() {
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value;
return <ClientComponent token={token} />;
}
理由: cookies()、headers() などのサーバー専用 API は、Server Components でのみ使用できます。
アンチパターン 4:逐次 await(ウォーターフォール)
誤り:
export default async function Page() {
const user = await fetchUser();
const posts = await fetchPosts(); // Waits for user to finish
const comments = await fetchComments(); // Waits for posts to finish
return <div>...</div>;
}
正しい例:
export default async function Page() {
// Fetch in parallel
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
return <div>...</div>;
}
理由: 並列取得により、合計読み込み時間を大幅に短縮できます。
アンチパターン 5:Server Component を Client Component にインポートする
誤り:
// ClientComponent.tsx
'use client';
import ServerComponent from './ServerComponent'; // This makes it a Client Component!
export default function ClientComponent() {
return <div><ServerComponent /></div>;
}
正しい例:
// ParentServerComponent.tsx (Server Component)
import ClientComponent from './ClientComponent';
import ServerComponent from './ServerComponent';
export default function ParentServerComponent() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
理由: Server Component を Client Component にインポートすると、Client Component に変換されます。代わりに子として渡してください。
Client Components が適切な場合
次の用途には Client Components が適しています。
1. インタラクティブなフォーム
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [email, setEmail] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Handle submission
};
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}
2. リアルタイム機能
'use client';
import { useEffect, useState } from 'react';
export default function LiveChat() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com');
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => ws.close();
}, []);
return <div>{messages.map((m, i) => <div key={i}>{m}</div>)}</div>;
}
3. ブラウザー専用の機能
'use client';
import { useState, useEffect } from 'react';
export default function GeolocationDisplay() {
const [location, setLocation] = useState(null);
useEffect(() => {
navigator.geolocation.getCurrentPosition((pos) => {
setLocation({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
});
});
}, []);
return location ? <div>Lat: {location.lat}, Lng: {location.lng}</div> : null;
}
4. window を必要とするサードパーティライブラリ
'use client';
import { useEffect, useState } from 'react';
import confetti from 'canvas-confetti';
export default function CelebrationButton() {
const handleClick = () => {
confetti();
};
return <button onClick={handleClick}>Celebrate!</button>;
}
5. React Context プロバイダー
'use client';
import { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
クイック判断フローチャート
Need interactivity? (onClick, onChange, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need React hooks? (useState, useEffect, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need browser APIs? (window, localStorage, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need to fetch data?
├─ Yes → Server Component (default)
└─ No → Continue...
Need cookies/headers/searchParams?
├─ Yes → Server Component (default)
└─ No → Server Component (default, unless specific need)
コンポーネントの種類を確認する
コンポーネントの種類を確認するには:
// This works = Server Component
export default async function MyComponent() { ... }
// This works = Server Component
import { cookies } from 'next/headers';
// This works = Client Component
'use client';
import { useState } from 'react';
// This fails = Wrong combination
'use client';
import { cookies } from 'next/headers'; // ERROR!
まとめ
- デフォルトではサーバーコンポーネントを使用する — 高速で安全性も高くなります
- クライアントコンポーネントは、インタラクティブ性やブラウザー API が必要な場合にのみ使用します
- Client Components では useEffect を使ってデータを取得しないでください — サーバーコンポーネントを使用します
- React の 'use' API を使って、クライアントコンポーネントに Promise を渡します
- cookies/headers/searchParams には、サーバーコンポーネントでのみアクセスします
- コンポジションパターンを使って、サーバーコンポーネントとクライアントコンポーネントを組み合わせます
- Promise.all を使って並列に取得し、ウォーターフォールを回避します