【Next.js】サーバー/クライアントコンポーネントの使い分けとつまずく6つのエラーについて紹介
公開日: 2026年9月7日 · 読了目安 13 分
Next.js(App Router)のサーバーコンポーネントとクライアントコンポーネントの使い分けを、"use client" が必要な場面、初心者がつまずく6つのエラーと直し方、props と children、page.tsx や layout.tsx の役割までコード例つきで解説します。
サーバー/クライアントコンポーネントの使い分けで迷うとき
Next.js の App Router で作り始めて、ボタンに useState(状態を覚える仕組み)を書いたらエラーになった。
言われるままに "use client"(この部品はブラウザで動かす、という印)を付けたら動いたものの、どこに書けばよいのかわからない。多くの人が最初にここでつまずきます。
答えはシンプルです。
部品は何も書かずに、サーバーで組み立てる部品として作ります。状態やクリック、ブラウザの機能が必要なファイルにだけ、この1行を書きます。
しかも、なるべく末端の小さな部品に書くのがコツです。
この記事では、次のことをコード例つきで説明します。
サーバーで組み立てる部品と、ブラウザで動く部品の違いと分ける理由
ブラウザで動かす印が必要になる場面の見分け方
初心者がつまずく6つのエラーと直し方
部品に値を渡す方法(props と children)と、app フォルダのファイル名の役割
サーバーコンポーネントとクライアントコンポーネントの違い
コンポーネントは、画面を部品に分けた関数です。
App Router では、何も指定しなければサーバーコンポーネントとして動きます。サーバー側で実行され、できあがった HTML がブラウザに届きます。
クライアントコンポーネントは、ファイルの先頭に "use client" と書いた部品です。最初の HTML はサーバーで作られますが、その後ブラウザでも JavaScript が動き、クリックや入力に反応できるようになります。
サーバーコンポーネントに分けると得られる3つのこと
ブラウザに送る JavaScript が減り、ページが軽くなる
データベースや API からのデータ取得を、コンポーネントの中に直接書ける
API キーなどの秘密の値を、ブラウザに漏らさずに使える
反対に、すべてをクライアントにすると、この3つを手放すことになります。だからこそ「基本はサーバー、必要なところだけクライアント」という分け方が勧められています。
クライアントコンポーネント(use client)が必要な場面
次のどれかを使うファイルには、ブラウザで動かす印が必要です。どれも使わないなら、何も書かなくてかまいません。
部品の状態や処理のタイミングを扱う React のフック(useState、useEffect、useRef など)
クリックや入力に反応するイベントの処理
ブラウザだけにある機能
Motion(旧 Framer Motion)のように、ブラウザで動くことが前提のライブラリ
イベントの処理の例は、onClick(クリックしたとき)や onChange(入力が変わったとき)です。
ブラウザだけにある機能の例は、window(画面やタブの情報)や localStorage(ブラウザに値を保存する場所)です。
書く場所はファイルの1行目です。import より前に置いてください。
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}サーバーとクライアントのコンポーネントを組み合わせるルール
サーバーで組み立てる部品から、ブラウザで動く部品を import することはできます。逆に、ブラウザで動く部品のファイルが読み込んだ部品は、すべてブラウザ側で動きます。
ただし、children(中に入れる要素)や props(部品に渡す値)として渡された部品は例外です。
サーバーで組み立てる部品を、中に入れる要素としてブラウザ側の部品に渡すと、そのままサーバーで描画されます。この仕組みは、後で説明するエラー3の直し方でも使います。
サーバー/クライアントコンポーネントでつまずく6つのエラー
サーバーとクライアントの使い分けで出るエラーは、ほとんどが次の6つのどれかです。エラーの文面は Next.js のバージョンで少し変わるので、内容で見分けてください。
エラー1:useState や useEffect が使えない
「このフックはクライアントコンポーネントでしか動かない」という趣旨のエラーです。そのファイルの先頭に、ブラウザで動かす印を書けば直ります。
ページのファイル(page.tsx)で起きた場合は、フックを使う部分だけを別ファイルに切り出します。そのうえで、この1行を付けましょう。
エラー2:クリック時の関数を親からブラウザ側の部品に渡せない
サーバーで組み立てる部品の中で関数を作り、それを props としてブラウザ側の部品に渡したとします。
すると「イベントハンドラーはクライアントコンポーネントに渡せない」という趣旨のエラーになります。
サーバーで作った普通の関数は、ブラウザへ送れないためです。"use server" を付けたサーバーアクションは例外です。
直し方は、クリック時の処理をブラウザで動く部品の中に移すことです。親から渡すのは、文字列や数値などの値だけにします。
"use client";
import { useState } from "react";
// 処理はクライアント側の中に書く。親からは値だけ受け取る
export function LikeButton({ initial }: { initial: number }) {
const [likes, setLikes] = useState(initial);
return <button onClick={() => setLikes(likes + 1)}>♥ {likes}</button>;
}エラー3:layout.tsx で Context が使えない
テーマやログイン状態を共有しようとして、layout.tsx で createContext を使うとエラーになります。Context はクライアントの機能だからです。
Provider(値を配る部品)だけをブラウザで動く部品に切り出し、layout.tsx から children を包む形にします。
中に入れる要素として渡したページは、サーバーで組み立てる部品のままです。
"use client";
import { createContext, useState, type ReactNode } from "react";
export const ThemeContext = createContext<"light" | "dark">("light");
export function Providers({ children }: { children: ReactNode }) {
const [theme] = useState<"light" | "dark">("light");
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}
// app/layout.tsx 側
// <body><Providers>{children}</Providers></body>エラー4:環境変数がブラウザで読めない
クライアントコンポーネントで process.env.API_KEY を読んでも、値が入りません(undefined や空文字になります)。
ブラウザに届くのは、名前が NEXT_PUBLIC_ で始まる環境変数だけだからです。
秘密のキーを使う処理は、サーバーで組み立てる部品に移します。または、app/api/ フォルダに置くサーバー側の処理(Route Handler)に移してください。
名前に NEXT_PUBLIC_ を付けて解決しようとすると、キーが誰にでも見える状態になります。
エラー5:サーバーとブラウザで表示が食い違う
Hydration(サーバーで作った HTML にブラウザの JavaScript をつなぐ処理)のエラーです。
描画の中で、現在時刻(new Date())や乱数(Math.random())を使うとします。するとサーバーとブラウザで結果が変わり、この食い違いが起きます。
ブラウザの情報を持つ window を描画の中で読んだときも、同じことが起きます。
ブラウザでしか決まらない値は、useEffect(表示のあとに処理を走らせる仕組み)の中で取得して、状態に入れます。最初の描画ではサーバーと同じ表示になるので、食い違いが出ません。
"use client";
import { useEffect, useState } from "react";
export function Clock() {
const [now, setNow] = useState<string | null>(null);
useEffect(() => {
setNow(new Date().toLocaleTimeString());
}, []);
return <p>{now ?? "--:--:--"}</p>;
}エラー6:"use client" を上の階層に書いてページが重くなる
エラーの表示は出ませんが、いちばん多い失敗です。
エラー1を消すために、ページ全体のファイル(page.tsx や layout.tsx)にこの1行を書いたとします。
すると、そこから読み込んだ部品がすべてブラウザ側で動きます。データ取得もサーバーでできなくなります。
Motion でヒーローを動かす場合も同じです。ページ全体ではなく、動く部品のファイルだけにブラウザで動かす印を書き、ページからそれを読み込みます。
"use client";
import { motion } from "motion/react";
import type { ReactNode } from "react";
export function FadeIn({ children }: { children: ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
{children}
</motion.div>
);
}
// app/page.tsx(サーバーのまま)
// <FadeIn><h2>機能の紹介</h2></FadeIn>サーバーコンポーネントでも使える props と children
部品に値を渡す方法は、props と children の2つです。どちらも、サーバー側とブラウザ側の両方の部品で使えます。
props:ページごとに変わる値を渡す
ヘッダーのタイトルのように、ページごとに変わる値は props で受け取ります。受け取った値は読み取り専用で、受け取った側で書き換えてはいけません。
type SiteHeaderProps = { title: string };
export function SiteHeader({ title }: SiteHeaderProps) {
return (
<header className="border-b px-4 py-3">
<h1 className="text-lg font-bold">{title}</h1>
</header>
);
}
// 使う側
// <SiteHeader title="DesignLayer" />children:タグの中身をそのまま受け取る
カードのように、枠は共通で中身だけを変えたい部品には children を使います。タグの間に書いた要素が、そのまま中身として渡されます。
import type { ReactNode } from "react";
export function Card({ children }: { children: ReactNode }) {
return <div className="rounded-lg border p-4 shadow-sm">{children}</div>;
}
// 使う側:中身は自由
// <Card>
// <h2>料金プラン</h2>
// <p>月額1,000円から</p>
// </Card>ファイル名で決まる役割とクライアントコンポーネント
App Router では、決められたファイル名に役割があります。よく使うものは次のとおりです。
page.tsx:その URL の画面
layout.tsx:配下のページで共有する枠。ページを移動しても作り直されない
loading.tsx:データ取得中に自動で表示される画面。読み込み中の仮の表示を置く場所
エラーのときや、ページを移動するたびの表示もファイル名で決まります。
error.tsx:エラーが起きたときの画面。ブラウザで動かす印が必須
not-found.tsx:404 の画面
template.tsx:共有の枠に似ているが、移動のたびに作り直される。入場アニメーションを毎回再生したいときに使う
このうち、忘れやすいのが error.tsx に書くブラウザで動かす印です。
error.tsx はエラーを受け止める仕組み(エラー境界)として、ブラウザ側で動きます。再試行のボタンがなくても、ブラウザで動く部品にする必要があります。
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>エラーが発生しました</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>再試行</button>
</div>
);
}取得・表示・操作でサーバーとクライアントコンポーネントを分ける
1つの部品にデータ取得や表示、ボタンの処理をすべて入れると、あとから直しにくくなります。役割ごとに3つに分けると、サーバーとクライアントの境目も自然に決まります。
取得:サーバーで組み立てる部品(page.tsx など)で、async 関数として書く
表示:渡された値を受け取って描画するだけの部品。ブラウザで動かす印は不要
操作:クリックや入力を扱う小さな部品だけを、ブラウザで動く部品にする
import { LikeButton } from "@/components/LikeButton";
type Post = { id: string; title: string; likes: number };
// 取得:サーバーコンポーネント
export default async function PostsPage() {
const res = await fetch("https://example.com/api/posts");
const posts: Post[] = await res.json();
return <PostList posts={posts} />;
}
// 表示:props を受け取るだけ
function PostList({ posts }: { posts: Post[] }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title}
{/* 操作:ここだけクライアント */}
<LikeButton initial={post.likes} />
</li>
))}
</ul>
);
}動く部品はクライアントコンポーネントに切り出す
ボタンやフォームのような汎用の部品は、毎回ゼロから書く必要はありません。よく使われる shadcn/ui は、コンポーネントのソースを自分のリポジトリにコピーする方式なので、あとから自由に書き換えられます。
npx shadcn@latest add buttonアニメーション付きのクライアントコンポーネントを使う場合
入場アニメーションやスクロール連動のセクションは、自分で書くと1つに半日かかることもあります。
DesignLayer では、動きのある React セクションをブラウザで動かして確かめ、TSX + Tailwind CSS のソースをコピーできます。
ブラウザで動かす印や、必要なパッケージの一覧も含まれています。
そのため、エラー6のようにページ全体をブラウザ側にしてしまう心配がありません。https://design-layer.com/ja/components
Motion を自分で書く場合の基本は、別の記事で解説しています。https://design-layer.com/ja/articles/nextjs-framer-motion-guide
サーバー/クライアントコンポーネントについてよくある質問
最初の HTML はサーバーで作られます。そのうえでブラウザでも JavaScript が動き、操作に反応できるようになります。
"use client" は「ブラウザでも動く部品」という印と考えるとわかりやすいです。
まとめ:Next.js のサーバー/クライアントコンポーネントの使い分け
部品は何も書かずにサーバー側で作り、状態・クリック・ブラウザの機能が要るファイルにだけ "use client" を書く
この1行はなるべく末端の小さな部品に書き、ページ全体や共有の枠には書かない
クリック時の関数はサーバーから渡さない。秘密の値はサーバーで使う。ブラウザでしか決まらない値は表示のあとに取る
中に入れる要素として渡せば、ブラウザ側の部品の中でもサーバーのまま描画できる
app フォルダではファイル名が役割を決める
基本を押さえたら、実際に LP を組んでみるのが早道です。
Cursor を使って1日で作る手順は次の記事にまとめています。https://design-layer.com/ja/articles/cursor-nextjs-landing-page-guide
ヒーローに動きを付ける方法は、こちらで解説しています。https://design-layer.com/ja/articles/nextjs-animated-hero-section-guide


