•12 min read

Next.js14:サーバー・クライアント境界のデバッグ

Next.js14:サーバー・クライアント境界のデバッグ

Next.js App Router (Next.js 13.4+) は、React Server Components と Client Components を単一のツリーに統合します。ほとんどの場合、これは目に見えない形で機能しますが、問題が発生すると、エラーは不可解で、スタックトレースは触ったことのない内部を指します。このガイドでは、あらゆる種類の境界障害を迅速に診断するための具体的なテクニックを提供します。

Audio Briefing
0:00 / 0:00

両サイド分割の理解

Server Components はサーバー上でのみ実行されます。データベース呼び出しのawait、環境変数の読み取り、Node.js 専用モジュールのインポートが可能です。その出力は RSC ペイロードとしてシリアル化されます。これは、HTML ではない、JSON ではない、JavaScript バンドルではない、コンパクトなストリーミング形式です。Client Components ('use client' とマークされているもの) はブラウザ JS にコンパイルされ、ハイドレーションと再レンダリング中に実行されます。

境界とは、Server Component が Client Component をレンダリングするエッジのことです。そのエッジを越えるものはすべてシリアル化可能でなければなりません。

Advertisement

エラークラス 1 — 非シリアル化可能なプロパティ

表示される内容:

Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
  at stringify (...next/dist/server/app-render/app-render.js:...)

またはクラスインスタンスの場合:

Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components.
Classes or null prototypes are not supported.

原因: Server Component から Client Component へ、Date、Map、Set、クラスインスタンス、または関数をプロパティとして渡すこと。

// ❌ Date object is not serializable
async function Page() {
  const post = await db.post.findFirst()
  return <PostCard publishedAt={post.createdAt} /> // Date → crash
}

// ✅ Serialize to ISO string before passing
async function Page() {
  const post = await db.post.findFirst()
  return <PostCard publishedAt={post.createdAt.toISOString()} />
}

デバッグのヒント: Server Component で渡す前に JSON.stringify(yourProp) をログに出力します。それがスローされるか {} を生成する場合、そのプロパティはシリアル化できません。

エラークラス 2 — ハイドレーションの不一致

ブラウザコンソールに表示される内容:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

Warning: Expected server HTML to contain a matching <div> in <div>.

See more info here: https://nextjs.org/docs/messages/react-hydration-error

根本原因: サーバーから送信された HTML が、クライアントがハイドレーション中にレンダリングする React ツリーと一致しない。一般的なトリガー:

  • Client Component の初期レンダリングで window、localStorage、navigator、または Date.now() を使用している場合
  • サーバーとクライアントで異なるタイムゾーンが異なる日付文字列を生成している場合
  • ブラウザ拡張機能がハイドレーション前に DOM ノードを挿入している場合

修正パターン — ブラウザ専用コードの遅延:

'use client'
import { useEffect, useState } from 'react'

// ❌ Crashes: window is undefined on the server
export function ViewCount() {
  const stored = localStorage.getItem('views') ?? '0'
  return <span>{stored} views</span>
}

// ✅ Mount-safe: reads localStorage only after hydration
export function ViewCount() {
  const [views, setViews] = useState<string | null>(null)

  useEffect(() => {
    setViews(localStorage.getItem('views') ?? '0')
  }, [])

  if (views === null) return <span>— views</span>
  return <span>{views} views</span>
}

日付の不一致の修正: 常に日付をサーバーから ISO 文字列として渡し、クライアント側で new Date(isoString) を使用して解析します。Client Component の初期レンダリング内で new Date() を呼び出さないでください。

エラークラス 3 — RSC ペイロードの検査

Next.js がハイドレーションする前に、サーバーから RSC ペイロードをストリーミングします。生のまま検査して、サーバーが何を送信したかを理解できます。

DevTools → Network タブを開き → Fetch/XHR でフィルタリング → ページをリロードします。Accept: text/x-component を含むページ URL へのリクエストを探します。レスポンスは次のようになります。

0:["$","div",null,{"className":"prose"},["$","h1",null,{},"Hello"]]
1:{"id":"cjld2cyuq0000t3rmniod1fga","title":"Hello World","createdAt":"2026-01-01T00:00:00.000Z"}

これは、サーバーが何をシリアル化し、どのようなツリー形状を生成したかを正確に示します。ここにフィールドがない場合、それはフェッチされなかったか、非シリアル化可能として削除されたかのどちらかです。

プロのヒント: Next.js 開発モードでは、URL に ?_rsc=1 を追加して、純粋な RSC の更新を強制し、ペイロードを単独で検査します。

Advertisement

エラークラス 4 — サーバーモジュールのクライアントへの漏洩

表示される内容:

Module build failed: You're importing a component that needs "server-only"
but none of its parents are marked with "use server", nor are they a Server Component.

またはさらに悪いことに、エラーは発生せず、Node.js API 呼び出しがブラウザバンドルでサイレントに undefined を返すだけです。

server-only パッケージは標準的なガードです。

npm install server-only
// lib/db.ts
import 'server-only'     // ← throws at build time if imported in a Client Component
import { PrismaClient } from '@prisma/client'

const db = new PrismaClient()
export { db }

これで、Client Component またはそれがインポートするファイルが lib/db.ts をインポートしようとすると、データベースの認証情報がブラウザにサイレントに送信される代わりに、明確なエラーでビルドが失敗します。

これを以下のファイルに適用します。

  • データベースの認証情報または API シークレットを含むファイル
  • Node.js の組み込み機能 (fs、crypto、net など) を使用するファイル
  • 公開されていない内部サービスを呼び出すファイル

エラークラス 5 — use server 関数境界

関数 (ファイルではない) の 'use server' は、Server Action を作成します。これはサーバーで実行されますが、Client Component から呼び出すことができる関数です。ルールは厳格です。

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  if (!title) throw new Error('Title is required')
  await db.post.create({ data: { title } })
}

use server 境界でのよくある間違い:

  1. 非シリアル化可能な値を返す — RSC プロパティと同じルールです。プレーンなオブジェクトを返します。
  2. クライアント入力を信頼する — Server Actions は HTTP エンドポイントです。常に Zod で検証します。
'use server'
import { z } from 'zod'

const schema = z.object({ title: z.string().min(1).max(200) })

export async function createPost(formData: FormData) {
  const parsed = schema.safeParse({ title: formData.get('title') })
  if (!parsed.success) throw new Error('Invalid input')
  await db.post.create({ data: parsed.data })
}
  1. 捕捉されないエラーがアクションをサイレントにクラッシュさせる — React 19 では、エラー状態を捕捉するために useActionState を使用します。
'use client'
import { useActionState } from 'react'
import { createPost } from './actions'

export function PostForm() {
  const [error, action, isPending] = useActionState(
    async (_prev: string | null, formData: FormData) => {
      try {
        await createPost(formData)
        return null
      } catch (e) {
        return (e as Error).message
      }
    },
    null
  )

  return (
    <form action={action}>
      <input name="title" />
      {error && <p className="text-red-500">{error}</p>}
      <button disabled={isPending}>
        {isPending ? 'Saving…' : 'Create Post'}
      </button>
    </form>
  )
}

エラークラス 6 — 機密データのための Taint API

React 19 では、機密性の高いサーバーデータが誤って Client Components に渡されるのを防ぐために、experimental_taintObjectReference と experimental_taintUniqueValue が導入されました。

// app/api/user/route.ts
import { experimental_taintObjectReference } from 'react'

async function getUser(id: string) {
  const user = await db.user.findUnique({ where: { id } })
  // Mark the whole object as tainted — passing it as a prop will throw
  experimental_taintObjectReference(
    'Do not pass user objects to Client Components. Serialize only the fields you need.',
    user
  )
  return user
}

next.config.js で有効にします。

/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    taint: true,
  },
}

これは多層防御の手段です。エラーは開発中のレンダリング時に発生し、偶発的なデータがクライアントバンドルに漏洩する前に検出されます。

'use client' 配置戦略

App Router で最も影響の大きいパフォーマンス決定は、クライアント境界をどこに引くかです。よくある間違いは、レイアウトや高レベルのページコンポーネントに 'use client' を配置することです。これにより、すべての子がクライアントバンドルに強制的に含まれてしまいます。

ルール: 'use client' をインタラクティブ性が必要な限り深くプッシュします。

// ❌ Forces everything into client bundle
'use client'
export default function BlogPage({ posts }) {
  const [filter, setFilter] = useState('')
  return (
    <div>
      <input onChange={(e) => setFilter(e.target.value)} />
      {posts.map(p => <PostCard key={p.id} post={p} />)}
    </div>
  )
}

// ✅ Only the filter widget is a Client Component
// BlogPage and PostCard remain Server Components
'use client'
export function FilterInput({ onFilter }: { onFilter: (q: string) => void }) {
  return <input onChange={(e) => onFilter(e.target.value)} />
}

Server Components は、Client Components を介して他の Server Components を children として渡すことができます。これにより、それらをクライアントサイドにする必要はありません。

// This pattern works — children are still Server Components
export default function Layout({ children }: { children: React.ReactNode }) {
  return <ClientShell>{children}</ClientShell>
}

デバッグワークフローチートシート

症状最初に確認することツール
「関数は渡せません」境界を越えるプロパティの型Server Component の JSON.stringify(prop)
ハイドレーションの不一致初期レンダリングでのブラウザ専用 APIDevTools → React タブ → ハイドレーションエラー
RSC ペイロードが間違っているサーバーが実際にシリアル化したものネットワークタブ → text/x-component リクエスト
ブラウザ内のサーバーモジュールserver-only インポートの欠落ビルド出力 → バンドルアナライザー
アクションがサイレントに失敗するServer Action での捕捉されないスローuseActionState エラーキャプチャ
機密データの漏洩Client Component に渡されたオブジェクトexperimental_taintObjectReference

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement