window・document is not defined の直し方
実際に出るエラー
サーバー側で実行されるコードから window や document、localStorage を参照したときに出るエラーです。
何が起きているか
window はブラウザにしかありません。サーバー側で実行されるコードから参照すると、未定義の変数を読んだことになって ReferenceError になります。
App Router では、これが2つの場面で起きます。
| 参照した場所 | 落ちるか | なぜ |
|---|---|---|
| Server Component の中 | できない | そもそもサーバーでしか動かないので、必ず落ちる |
| Client Component の描画の中 | できない | 初回表示のために一度サーバーで実行されるため |
| useEffect の中 | できる | ブラウザでしか動かない。ここが正しい置き場所 |
2 を見落とすことが多いです。'use client' を書いたから安心、とはなりません。
Client Component もサーバーで一度動く
- Serverモジュールが読み込まれる
ファイルの先頭に書いた処理はここで動く。window はまだ無い。
- Server初回表示の HTML を描く
描画の途中で window を読むと、ここで落ちる。
- Browserhydration のあとに useEffect
window / localStorage を読んでよいのはここだけ。
名前に引きずられますが、Client Component は「ブラウザだけで動くもの」ではなく「ブラウザにも送られるもの」です。
だから、'use client' の付いたファイルでも、コンポーネントの本体で直接 window を読むと落ちます。
document も localStorage も同じ
ブラウザにしかないものは、window だけではありません。Client Component の本体で読んで、実機で確かめました。
| 読んだもの | ビルドで出た文言 |
|---|---|
| window.innerWidth | ReferenceError: window is not defined |
| document.title | ReferenceError: document is not defined |
| localStorage.getItem('k') | ReferenceError: localStorage is not defined |
どれも、事前に描く段階(Error occurred prerendering page)で止まりました。原因も直し方も、このページの window と同じです。
原因はほぼこの3つ
1. Server Component で参照している
export default function Page() {
const w = window.innerWidth // ← 落ちる
return <div>{w}</div>
}
2. Client Component の本体で参照している
'use client'
export function Widget() {
const saved = localStorage.getItem('key') // ← 最初の1回で落ちる
return <div>{saved}</div>
}
3. import したライブラリが読み込み時に参照している
自分のコードに window が1つも無いのに出る場合はこれです。ライブラリがモジュールの評価時(import した瞬間)にブラウザの API を触っています。
最短の確認手順
- エラーが出たページのファイルを開く
- 1行目に
'use client'があるか見る - 無ければ → Server Component。ブラウザの API は使えません
- あれば → 参照している場所が「コンポーネントの本体」か「
useEffectの中」かを見る - どちらでもなければ、
importしているライブラリを疑う
直し方
'use client' export function Box() { const w = window.innerWidth return <div>{w}</div> }
描画の途中で読んでいる。サーバーで落ちる。
'use client' export function Box() { const [w, setW] = useState(0) useEffect(() => setW(window.innerWidth), []) return <div>{w}</div> }
最初は既定値で描き、ブラウザに着いてから差し替える。
推奨:useEffect の中で読む
useEffect はブラウザでしか動かないので、ここに置けば安全です。
'use client'
import { useEffect, useState } from 'react'
export function Width() {
const [w, setW] = useState<number | null>(null)
useEffect(() => {
setW(window.innerWidth)
}, [])
return <div>{w ?? '計測中'}</div>
}
最初の描画では null になるので、そのときに何を出すかを決めておいてください。
そもそも CSS で済まないか考える
画面幅による出し分けは、多くの場合メディアクエリで足ります。JavaScript で測ると、最初の描画とその後で表示が変わる(チラつく)ので、CSS のほうが素直です。
ライブラリが原因の場合
読み込み自体を遅らせます。
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('some-chart-library'), { ssr: false })
やってはいけない直し方
const w = typeof window === 'undefined' ? 0 : window.innerWidth
エラーは消えます。ですが、
- サーバー側では必ず
0になる - ブラウザでは実際の値になる
- その差が hydration の不一致として現れる
ので、別の問題に置き換わっただけです。値がサーバーとブラウザで違ってよいのは useEffect の後だけ、と覚えてください。
なぜ本番だけで出ることがあるのか
そのページが prerender される構成なら、ビルド中に描画が走るのでビルドが失敗して気づけます。
一方、リクエストごとに描画される構成では、そのページを実際に開くまで出ません。開発中に触っていない画面だと、本番で初めて表面化します。
再発防止
- ブラウザの API を使う場所は
useEffectの中だけ、と決める 'use client'は「ブラウザにも送られる」であって「ブラウザだけ」ではない、と覚える- 新しいライブラリを入れたら、サーバー側で import できるかを最初に確かめる
関連するエラー
フックを Server Component から import した場合は、描画より前のビルド段階で止まります。
This API is only available in Client Components.
こちらは静的に検出できるぶん、気づくのは早いです。
useEffect に移さずに、サーバーとブラウザで違う値を描いてしまうと、今度は hydration の食い違いになります(Hydration failed の直し方)。
出典
- nextjs.org/docs/app/getting-started/server-and-client-components
- developer.mozilla.org/ja/docs/Web/API/Window
このページの内容は上記の一次情報を基準に書いています。個人ブログや二次情報を根拠にしていません。