こんにちは、かつコーチです。
これまでNext.jsシリーズでは、App Routerの基本からルーティング、Server/Client Components、データ連携、認証、テスト、デプロイまでを一通り解説してきました。
ここからの4本は少し毛色を変えて、実践Tips・つまずき解決編です。
今回はまず、Next.jsを触っていると本当によく遭遇するエラーを、原因と解決法とセットでまとめます。
初心者のうちは「エラーメッセージを読んでも何が起きているか分からない」状態になりがちですが、パターンを知っておくだけで解決スピードがぐっと上がります。
前提知識・何が起きるか
Next.jsのエラーは大きく3種類に分けられます。
1つ目は、Server ComponentとClient Componentの境界を誤解したことによるエラーです。
2つ目は、ビルド時とランタイムの実行環境の違いを意識できていないことによるエラーです。
3つ目は、単純な設定ミスやファイル配置のミスによるエラーです。
まずはこの3つの分類を頭に入れておくと、エラーメッセージを読んだときに「どのカテゴリの話か」が見えやすくなります。
基本の書き方・よく出るエラーへの対処
エラー1: useState is not defined / Hooks系エラー
App Routerでは、ファイルはデフォルトでServer Componentとして扱われます。
Server Componentの中でuseStateやuseEffectなどのHooksを使うと、次のようなエラーが出ます。
Error: You're importing a component that needs `useState`.
This React hook only works in a client component.
To fix, mark the file (or its parent) with the `"use client"` directive.
対処法はシンプルで、ファイルの先頭に"use client"を追加します。
// ❌ Before: use clientがない
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// ✅ After: 先頭にuse clientを追加
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
エラー2: fetch failed / データ取得系エラー
Server Componentの中でfetchが失敗すると、次のようなエラーになります。
Error: fetch failed
cause: Error: connect ECONNREFUSED 127.0.0.1:3000
よくある原因は、APIのURLに相対パスを使ってしまっていることです。
Server Componentはサーバー上で実行されるため、ブラウザのようにホストを自動補完してくれません。
// ❌ Before: 相対パスだとサーバー側で解決できない
const res = await fetch("/api/posts");
// ✅ After: 絶対URLを環境変数から組み立てる
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
const res = await fetch(`${baseUrl}/api/posts`);
エラー3: Event handlers cannot be passed to Client Component props
Server ComponentからClient Componentに関数(イベントハンドラ)をそのまま渡すと、次のエラーが出ます。
Error: Event handlers cannot be passed to Client Component props.
<button onClick={function onClick} children=...>
Server Componentの関数はシリアライズできないため、Client Component側に直接渡すことはできません。
// ❌ Before: Server Componentから関数を直接渡している
export default function Page() {
const handleClick = () => console.log("clicked");
return <ClientButton onClick={handleClick} />;
}
// ✅ After: クリック処理はClient Component側に閉じ込める
"use client";
export function ClientButton() {
const handleClick = () => console.log("clicked");
return <button onClick={handleClick}>押す</button>;
}
エラー4: Hydration failed because the server rendered HTML didn't match the client
サーバーで生成したHTMLとクライアントで再生成したHTMLが一致しないときに出るエラーです。
Error: Hydration failed because the server rendered HTML
didn't match the client.
原因の多くは、Date.now()やMath.random()、windowオブジェクトの参照など、サーバーとクライアントで結果が変わる処理をレンダリング中に呼んでいることです。
// ❌ Before: サーバーとクライアントで結果が変わる
export default function Timestamp() {
return <p>{new Date().toLocaleString()}</p>;
}
// ✅ After: useEffectでクライアント側だけに反映する
"use client";
import { useEffect, useState } from "react";
export default function Timestamp() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
setTime(new Date().toLocaleString());
}, []);
return <p>{time ?? "読み込み中..."}</p>;
}
つまずきポイント・一次情報の体験談
私が実際につまずいた経験を1つ共有します。
デプロイ直後の本番環境だけでHydration failedエラーが出て、ローカルでは全く再現しないという状態にハマったことがあります。
原因を探るのに半日近くかかったのですが、最終的にはブラウザ拡張機能が本番ページのDOMに勝手に属性を差し込んでいたことが原因でした。
拡張機能を無効にしたシークレットウィンドウで確認したところ、エラーが消えたことで初めて気づけました。
Warning: Extra attributes from the server: %s%s
data-darkreader-mode
「自分のコードのミスだ」と思い込んでコード側だけを疑っていると、こういう外部要因には気づけません。
エラーが再現する環境と再現しない環境を比較する、という基本に立ち返ったことで解決できた事例でした。
まとめ
この記事のポイント
- Next.jsのエラーは「Server/Client境界」「実行環境の違い」「設定ミス」の3系統に大別できる
"use client"の付け忘れはHooks系エラーの定番原因- Server Componentでの相対パスfetchは失敗しやすいので絶対URLを使う
- Hydrationエラーはサーバーとクライアントで結果が変わる処理が原因になりやすい
- ブラウザ拡張機能などコード外の要因が原因になることもあるので、シークレットウィンドウでの再現確認も有効
次に読むべき記事
次回は、layout.tsxが再評価されずに古い情報が残り続けてしまうという、もう少し踏み込んだつまずき事例を紹介します。
