useState・useRouter を Server Component で使えない
実際に出るエラー
Server Component のファイルから useState などのフックを import したときに出るビルドエラーです。
何が起きているか
App Router のファイルは、既定で Server Component として解決されます。useState はブラウザ側の状態を扱う API なので、Server Component のモジュールからは import できません。
重要なのは、エラーが出るのは「使った場所」ではなく「import したモジュール」だという点です。エラー文も "importing a module that depends on useState" と書いています。自分では useState を書いていなくても、間接的に依存しているものを import しただけで同じエラーになります。
これは描画より前、モジュールを解決する段階で検出されます。だから next build も next dev も、ページを開く前に止まります。
- Buildモジュールを解決する
import をたどって、そのファイルが何に依存しているかを調べる。ここで止まる。
- Server(到達しない)サーバーで描画する
解決の段階で失敗しているので、描画までは進まない。
- Browser(到達しない)ブラウザで操作できるようにする
だから「開いたら壊れていた」ではなく、ビルドで気づける。
useState 以外のフックでも同じ文言
useRouter・usePathname のような next/navigation のフックも、useEffect も、Server Component から読み込むと同じ形の文言になります。4つのページで確かめました。
| 読み込んだもの | 文言 |
|---|---|
| useRouter(next/navigation) | depends on `useRouter` |
| usePathname(next/navigation) | depends on `usePathname` |
| useSearchParams(next/navigation) | depends on `useSearchParams` |
| useEffect(react) | depends on `useEffect` |
ビルドは、4つのファイルの分を1回でまとめて報告しました(failed with 4 errors)。直し方はどれも同じで、フックを使う部品に 'use client' を書きます。useRouter の使い方は useRouter の使い方にまとめています。
原因はほぼこの3つ
1. 'use client' を書き忘れている
いちばん多い形です。
// app/counter/page.tsx
import { useState } from 'react' // ← 先頭に 'use client' が無い
export default function Page() {
const [n, setN] = useState(0)
return <button onClick={() => setN(n + 1)}>{n}</button>
}
2. 'use client' の位置が間違っている
ファイルのいちばん先頭でなければ効きません。import の後ろに書いても無効です。コメントより前である必要はありませんが、import 文より前に置いてください。
3. import したライブラリが内部でフックを使っている
自分のコードにフックが1つも無いのに出る場合は、これです。UI ライブラリ、フォームライブラリ、アニメーションライブラリなどが該当します。
最短の確認手順
- エラー文に出ているファイル名を開く
- 1行目が
'use client'かどうかを見る - 違うなら、そのファイルは Server Component
- 自分のコードにフックが無い場合は、import 文を上から順に疑う
直し方
// page.tsx 'use client' export default async function Page() { const items = await db.item.findMany() const [asc, setAsc] = useState(true) }
エラーは消えるが、データ取得までブラウザ側の依存になる。
// page.tsx(印を書かない) export default async function Page() { const items = await db.item.findMany() return <SortButton items={items} /> }
状態を持つ部分だけを別ファイルに切り出す。取得はサーバーに残る。
推奨:フックを使う部品だけを切り出す
ページ全体に 'use client' を付けるのではなく、状態を持つ部分だけを別ファイルにします。
// components/Counter.tsx
'use client'
import { useState } from 'react'
export function Counter() {
const [n, setN] = useState(0)
return <button onClick={() => setN(n + 1)}>{n}</button>
}
// app/counter/page.tsx(Server Component のまま)
import { Counter } from '@/components/Counter'
export default async function Page() {
const title = await getTitle() // サーバー側でデータを取れる
return (
<>
<h1>{title}</h1>
<Counter />
</>
)
}
こうすると、ブラウザへ送られる JavaScript はカウンターの分だけで済みます。
ライブラリが原因の場合
そのライブラリを使う部分を、'use client' を書いた薄いファイルで包みます。
// components/Chart.tsx
'use client'
export { Chart } from 'some-chart-library'
なぜページ全体に付けてはいけないのか
'use client' は「ここから下をブラウザにも送る」という宣言です。ページの先頭に書くと、そのページが import しているものすべてが対象になります。
- 表示するだけの部分まで JavaScript がブラウザへ送られる
- DB やサーバー専用の処理を、そのファイルから直接呼べなくなる
付けるほど重くなるので、置く場所はできるだけ葉に近い側です。
// app/page.tsx 'use client' import { formatPrice } from '@/lib/format' export default function Page() { const [n, setN] = useState(0) return <div>{formatPrice(n)}</div> }
formatPrice の実装まで、そのままブラウザへ運ばれる。
// app/page.tsx(印を書かない) import { formatPrice } from '@/lib/format' import { Counter } from './Counter' export default function Page() { return <Counter label={formatPrice(0)} /> }
整形はサーバーで済ませ、結果だけを渡す。運ばれるのはボタンの分だけ。
開発中に出るのか
出ます。これは描画前の解決段階で検出されるので、next dev でもそのページに移動した時点で止まります。本番だけで出るタイプのエラーではありません。
再発防止
- フックを使うファイルは、作った瞬間に
'use client'を書く - 逆に、
'use client'を書いたファイルには「ブラウザが必要な処理」しか置かない - 新しいライブラリを入れたら、Server Component から使えるかを最初に確かめる
関連するエラー
window や document を Server Component で参照した場合は、ビルドではなく描画の段階で別のエラーになります。
ReferenceError: window is not defined
こちらは import では検出できないので、実際に描画されるまで出ません。
出典
このページの内容は上記の一次情報を基準に書いています。個人ブログや二次情報を根拠にしていません。