localStorage is not defined の直し方 — SSRで落ちる理由と、use client で直らない理由
先に結論
localStorage is not defined は、localStorage の使い方の問題ではありません。
ブラウザの外でそのコードが走っているという、場所の問題です。
まず、出ているエラーの文面で原因が分かれます。
| エラー | 意味 | どこで起きているか |
|---|---|---|
ReferenceError: localStorage is not defined | そのグローバルが存在しない | Node側(SSR・ビルド・テスト)で実行された |
ReferenceError: window is not defined | 同上 | 同上。window.localStorage と書いている場合 |
ReferenceError: document is not defined | 同上 | 同上。同じ原因の別の顔 |
Hydration failed because the server rendered HTML didn't match the client | サーバーとクライアントで描いた内容が違う | エラーは消えたが、値の出し方が間違っている(詳しく) |
TypeError [ERR_INVALID_ARG_VALUE]: The argument '--localstorage-file' ... | Nodeのフラグ指定が足りない | Nodeで意図的に使おうとしている(詳しく) |
直し方は、使う場面で決まります。
| やりたいこと | 直し方 |
|---|---|
| 画面に出す値を localStorage から読む | マウント後に読む(useEffect / onMounted) |
| 共通の関数の中で触っている | typeof window === 'undefined' で早期 return |
| そのコンポーネント全体がブラウザ専用 | サーバーで描かせない(ssr: false / <ClientOnly>) |
| Node側でも本当に保存したい | Nodeのフラグを使う(詳しく) |
Next.js で "use client" を付けても直りません。
クライアントコンポーネントもサーバーでプリレンダリングされるからです。
公式にそう書いてあります(詳しく)。ここで止まっている人が一番多いところです。
localStorage そのものの使い方、容量の上限、Safariが7日で消す話、 sessionStorage や Cookie との使い分けは localStorage の使い方と、2026年に気をつけること に書きました。 この記事は「ブラウザの外で落ちる」ことだけを扱います。
なぜサーバーには localStorage が無いのか
素のNodeで確かめる
localStorage はブラウザが提供するものです。
Node.js には、既定では存在しません。手元の Node 24.14.1 で確かめました。
$ node -e "console.log('typeof localStorage =', typeof localStorage)"
typeof localStorage = undefined
$ node -e "localStorage.getItem('x')"
ReferenceError: localStorage is not defined
関連するものも、まとめて存在しません。
| グローバル | typeof |
|---|---|
localStorage | undefined |
sessionStorage | undefined |
window | undefined |
document | undefined |
だからSSRで落ちます。
サーバーサイドレンダリングは、ページのHTMLをNodeの上で先に作る仕組みです。
そのときコンポーネントの中身はブラウザではなくNodeで実行されます。
そこに localStorage.getItem() があれば、当然落ちます。
同じ理由で、ビルド時・テスト実行時・静的生成時にも落ちます。 「ブラウザでは動くのに、ビルドで落ちる」はこれです。
Nodeにも localStorage はある(条件つき)
ここは意外と知られていません。Node には Web Storage の実装が入っています。 ただし実験的な機能で、フラグが要ります。
Node 24.14.1 の --help にこう出ています。
--experimental-webstorage experimental Web Storage API
--localstorage-file=... file used to persist localStorage data
実際に動かすと、こうなりました。
| 起動の仕方 | localStorage | sessionStorage |
|---|---|---|
| フラグなし | undefined(触れば ReferenceError) | undefined |
--experimental-webstorage のみ | 触った瞬間に TypeError [ERR_INVALID_ARG_VALUE] | object(使える) |
--experimental-webstorage --localstorage-file=... | object(使える) | object |
2行目のエラーは、文面まで取りました。
TypeError [ERR_INVALID_ARG_VALUE]: The argument '--localstorage-file'
is an invalid localStorage location. Received ''
localStorage は保存先のファイルが要るのに対して、
sessionStorage はメモリ上なのでファイル指定が要らない、という違いです。
名前のとおりの挙動になっています。
ファイルを指定して動かすと、本当に永続化されました。
$ node --experimental-webstorage --localstorage-file=/tmp/ls.test \
-e "localStorage.setItem('a','1'); console.log(localStorage.getItem('a'))"
1
$ node --experimental-webstorage --localstorage-file=/tmp/ls.test \
-e "console.log('別プロセスから', localStorage.getItem('a'))"
別プロセスから 1
別プロセスから読めます。指定したパスに実体のファイルが作られます。
起動時には ExperimentalWarning: Web Storage is an experimental feature and might change at any time が出ます。
これはSSRのエラーを直す手段ではありません。 SSRで落ちているのは「ブラウザ用のコードがサーバーで走っている」ことが問題であって、 サーバーに保存場所を与えてもユーザーごとの値にはなりません。 全アクセスで同じファイルを共有してしまいます。 SSRの直し方はこちらです。 Nodeのフラグが役に立つのは、CLIツールなどNode単体で動くものを書くときだけです。
use client を付けたのに直らない
公式が書いていること
Next.js の App Router で "use client" を付ければブラウザ専用になる、と思われがちです。
そうではありません。公式ドキュメントの記述はこうです。
「Server Components は React Server Component Payload という特別なデータ形式にレンダリングされ、
Client Components は HTML をプリレンダリングし、アプリケーションを操作可能にするために使われます」
— Server and Client Components(Next.js 公式・要約)
クライアントコンポーネントも、サーバーで一度レンダリングされます。
"use client" が意味するのは「ブラウザでしか動かない」ではなく、
「ブラウザでも動く(=ハイドレーションの対象になる)」です。
だから、コンポーネントの本体やモジュールのトップレベルに localStorage があると、
"use client" があってもサーバー実行時に落ちます。
公式は、ブラウザAPIの扱いについてもこう書いています。
「localStorage、window、navigator.geolocation のようなブラウザ専用APIにアクセスするには、
Client Components を使うべきです」
— 同上
つまり "use client" は必要条件であって、十分条件ではありません。
クライアントコンポーネントにしたうえで、さらに実行のタイミングをずらす必要があります。
ssr: false も、置く場所を間違えると使えない
次によく出てくるのが next/dynamic で ssr: false を指定する方法です。
これも、置く場所を間違えると別のエラーになります。
「ssr: false オプションは Server Components ではサポートされていません。
Server Components で使おうとするとエラーが出ます。
代わりに ssr: false を Client Component の中に移すことが推奨されます」
— How to lazy load Client Components and libraries(Next.js 公式・要約)
App Router では、ページやレイアウトは既定で Server Component です。
そこに dynamic(..., { ssr: false }) を書くと通りません。
いったんクライアントコンポーネントを挟んで、その中で書きます。
環境別の切り分け
同じエラーでも、どこで起きているかで触る場所が変わります。 まず「どの工程で落ちたか」を特定してください。
| 環境 | 落ちる工程 | 主な対処 |
|---|---|---|
| Next.js(App Router) | サーバーでのプリレンダリング | "use client" + useEffect。全体を外すなら Client Component 内で ssr: false |
| Next.js(Pages Router) | SSR / SSG | useEffect、または dynamic(..., { ssr: false }) |
| Nuxt | サーバーレンダリング | onMounted、または <ClientOnly> で包む |
| Astro | ビルド時のレンダリング | アイランドを client:only にする |
| Storybook | ビルド・SSR構成時 | ブラウザ判定で分岐する |
| Vitest / Jest | テスト実行 | テスト環境を jsdom にする。既定の node 環境には無い |
| Node で直接実行 | 実行そのもの | そもそもブラウザAPIを使わない設計にする(またはフラグ) |
テストで落ちている場合だけは、対処が逆方向です。
コードを直すのではなく、テスト環境にブラウザのふりをさせます(jsdom)。
ここをコード側で直そうとすると、本番に不要な分岐が増えていきます。
直し方は4つ
① マウント後に読む
一番素直で、ほとんどの場合これで足ります。 画面に出す値なら、これを使ってください。
'use client';
import { useState, useEffect } from 'react';
export default function Theme() {
// サーバーでもクライアントでも、初期値は同じにする
const [theme, setTheme] = useState('light');
useEffect(() => {
// ここはブラウザでしか走らない
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved);
}, []);
return <p>いまのテーマ: {theme}</p>;
}
Vue / Nuxt なら onMounted です。
<script setup>
import { ref, onMounted } from 'vue';
const theme = ref('light');
onMounted(() => {
const saved = localStorage.getItem('theme');
if (saved) theme.value = saved;
});
</script>
② typeof で守る
コンポーネントではなく、共通の関数やユーティリティの中で触っている場合です。 呼び出し元がサーバーかブラウザか分からないので、関数側で守ります。
export function getStored(key) {
// typeof なら、未定義の変数でも ReferenceError にならない
if (typeof window === 'undefined') return null;
try {
return localStorage.getItem(key);
} catch {
// プライベートモード等で落ちることがある
return null;
}
}
typeof を使うのが要点です。
if (window === undefined) と書くと、
その比較の時点で ReferenceError になります。
typeof は未定義の変数に対しても安全に "undefined" を返します。
| 書き方 | サーバーでの結果 |
|---|---|
typeof window === 'undefined' | 安全。true になる |
typeof localStorage === 'undefined' | 安全。true になる |
window === undefined | ReferenceError で落ちる |
!window | ReferenceError で落ちる |
③ サーバーで描かせない
コンポーネント全体がブラウザ専用のときは、サーバーで描くのをやめます。
// Next.js:Client Component の中に書く(Server Component では通らない)
'use client';
import dynamic from 'next/dynamic';
const ComponentC = dynamic(() => import('../components/C'), { ssr: false });
Nuxt には専用のコンポーネントがあります。公式の説明では、既定スロットの中身はサーバーのビルドから除外されます。
<template>
<div>
<Sidebar />
<ClientOnly fallback-tag="span" fallback="Loading comments...">
<Comment />
</ClientOnly>
</div>
</template>
fallback を書けるのが利点です。
サーバー側では代わりの内容を出せるので、ブラウザでマウントされるまでの見た目を自分で決められます。
どれを使うか
| 状況 | 使うもの | 欠点 |
|---|---|---|
| 値を1つ読んで画面に出す | ① マウント後に読む | 一瞬だけ初期値が見える |
| 共通関数の中で触る | ② typeof で守る | サーバー側では常に null が返る前提の設計が要る |
| コンポーネント全体がブラウザ専用 | ③ サーバーで描かせない | SSRの恩恵(初期表示の速さ・SEO)を失う |
| テストでだけ落ちる | テスト環境を jsdom に | — |
③ は効きますが、使いすぎないでください。
ページの大部分を ssr: false にすると、SSRを使っている意味がなくなります。
まず ① で済まないかを考えてください。
本当の落とし穴:hydration mismatch
何が起きるか
useEffect で囲んでエラーが消えた、で終わりにすると次の問題を踏みます。
サーバーが描いたHTMLと、ブラウザが描いた内容が食い違う状態です。
React公式の説明は、かなり強い言い方です。
「ハイドレーションの最中にサーバーとクライアントでレンダリング内容が異なる場合、
React はそれをバグとして扱い、開発者が修正することを期待します。
これらの不一致を無視すると、パフォーマンスの低下や、イベントハンドラが正しく紐づかないといった問題につながります」
— hydrateRoot(React 公式・要約)
「イベントハンドラが正しく紐づかない」が怖いところです。 画面は出ているのにボタンが効かない、という症状になります。 エラーは出ていないので、原因に辿りつくのに時間がかかります。
localStorage でこれが起きるのは、こういう書き方をしたときです。
// これは hydration mismatch になる
const [theme] = useState(
// サーバーでは実行できず、ブラウザでは値が入る
typeof window !== 'undefined' ? localStorage.getItem('theme') : 'light'
);
エラーは出ません。typeof で守っているので落ちません。
しかしサーバーは 'light' で描き、ブラウザは保存値で描くので、
初回のHTMLと一致しません。
公式が示している直し方
React公式が挙げているのは、状態変数で描き分ける方法です。
「サーバーとクライアントのレンダリングの差を制御するために、状態変数の使用を勧めます。
たとえば isClient のような状態変数をエフェクトでセットしてブラウザ専用の描画を管理し、
最初のサーバー出力が完全に一致するようにします」
— 同上
'use client';
import { useState, useEffect } from 'react';
export default function Theme() {
const [theme, setTheme] = useState('light'); // サーバーと同じ初期値
const [mounted, setMounted] = useState(false);
useEffect(() => {
setTheme(localStorage.getItem('theme') ?? 'light');
setMounted(true);
}, []);
// マウント前はサーバーと同じものを返す=一致する
if (!mounted) return <p>いまのテーマ: light</p>;
return <p>いまのテーマ: {theme}</p>;
}
要点は「最初の描画をサーバーと完全に一致させる」ことです。 保存値を使うのは、そのあとです。
suppressHydrationWarning を使っていい場面
Reactには警告を抑える手段もあります。ただし公式は、使いどころを限定しています。
「suppressHydrationWarning は、タイムスタンプのように
サーバーとクライアントで内容が異なると分かっている要素に対して true にできます。
ただし使いすぎるべきではありません。Reactの通常の不一致検出を無効にしてしまうからです」
— 同上
| 場面 | 使ってよいか | 理由 |
|---|---|---|
| 時刻の表示 | 使ってよい | サーバーとクライアントで必ず違う。公式が例として挙げている |
| localStorage の値を出す | 使わない | 一致させられる。上の書き方で消える |
| mismatch の警告がうるさい | 使わない | 検出そのものを切ってしまう。原因が隠れる |
初期値をどう設計するか
ここまでの直し方には共通の弱点があります。 マウントされるまでの一瞬、保存値ではない表示が出ます。 ダークテーマの保存値があるのに、一瞬だけ明るい画面が出る、というやつです。
| 対処 | 内容 | 向き不向き |
|---|---|---|
| そのまま受け入れる | 初期値で描いてから差し替える | ほとんどの値はこれでよい |
| マウントまで出さない | mounted が false の間は該当部分を描かない | レイアウトが動く。その領域の高さを確保しておく |
| Cookie に移す | サーバーがリクエストで値を読めるので、最初から正しく描ける | テーマや言語のような「初期表示に効く値」はこれが本筋 |
| head で先に適用する | 描画前に走る小さなスクリプトでクラスを付ける | テーマ限定の手段。やりすぎない |
3行目が答えになることが多いです。 localStorage はサーバーから見えません。 「サーバーが最初のHTMLを描くときに必要な値」を localStorage に置くこと自体が、設計の選択として合っていません。 初期表示に効く値は Cookie、それ以外は localStorage、という分け方が素直です。
AIに任せるときの注意
| 頼むこと | 向くか | 理由 |
|---|---|---|
| エラーのスタックトレースを貼って「どの工程で落ちた?」 | 向く | ビルドかSSRかテストかを読み分けられる |
コンポーネントを貼って「useEffect に移して」 | 向く | 機械的な移動。差分も小さい |
「typeof window のガードを全部の関数に入れて」 | 向く | 定型。入れ忘れを潰せる |
| 「localStorage is not defined を直して」(コードなし) | 向かない | `"use client"` を付けろ、と返ってくる。それでは直らない |
| hydration mismatch の解消を任せる | 向かない | suppressHydrationWarning で蓋をする答えが返りやすい |
| ちらつきの設計判断 | 向かない | Cookieに移すかどうかは設計の話。一般論では決まらない |
判定の目安です。答えが "use client" を足すだけで終わっていたら、足りていません。
useEffect か typeof か ssr: false のどれかが入っているかを見てください。
よくある質問
"use client" を付けたのに直りません
クライアントコンポーネントもサーバーでプリレンダリングされます(公式の記述)。
"use client" は必要条件であって十分条件ではありません。
useEffect の中に移すか、typeof で守ってください。
dynamic の ssr: false でエラーが出ます
Server Component の中に書いていませんか。
公式が「ssr: false は Server Components ではサポートされていない」と明記しています(詳しく)。
Client Component の中に移してください。
ブラウザでは動くのにビルドで落ちます
ビルド時にもコンポーネントはNodeで実行されるからです(詳しく)。 開発サーバーで気づかず、本番ビルドで初めて出ることがあります。
テストでだけ落ちます
テスト環境が node のままです。jsdom に変えてください(表5)。
ここをコード側の分岐で直すと、本番に不要な条件が増えます。
エラーは消えたのにボタンが効きません
hydration mismatch です。 React公式が「不一致を無視すると、イベントハンドラが正しく紐づかない問題につながる」と書いています(詳しく)。 最初の描画をサーバーと一致させてください。
suppressHydrationWarning で消してもいいですか
localStorage の値なら、やめたほうがいいです。 一致させられるものだからです(表8)。 公式も「使いすぎるべきではない。通常の不一致検出を無効にしてしまう」としています。
一瞬だけ違う表示が出ます
マウントされるまでは保存値を読めないためです。 テーマや言語のように初期表示に効く値は、Cookieに移すのが本筋です(表9)。 サーバーはリクエストでCookieを読めるので、最初から正しく描けます。
Node で localStorage を使うことはできますか
できます。--experimental-webstorage と --localstorage-file が要ります(表4)。
ただしSSRのエラーを直す手段ではありません。
全アクセスで同じファイルを共有してしまいます。
window === undefined ではだめですか
だめです。その比較の時点で ReferenceError になります(表6)。
typeof window === 'undefined' と書いてください。
まとめ
localStorage is not definedは使い方の問題ではなく、場所の問題。ブラウザの外で走っている。- 素のNode 24では
localStorageもwindowもdocumentもsessionStorageもすべてundefined(実測)。 "use client"では直らない。クライアントコンポーネントもサーバーでプリレンダリングされる(Next.js公式)。ssr: falseは Server Component では使えない。Client Component の中に移す(Next.js公式)。- 直し方は、① マウント後に読む ②
typeofで守る ③ サーバーで描かせない。まず ① を考える。 window === undefinedと書くとその行で落ちる。必ずtypeof。- エラーが消えても終わりではない。hydration mismatch はイベントハンドラが紐づかない形で出る(React公式)。
- 最初の描画をサーバーと完全に一致させる。
suppressHydrationWarningで蓋をしない。 - 初期表示に効く値は Cookie に移す。localStorage はサーバーから見えない。
- Nodeにも Web Storage はあるがフラグ2つが要り、SSRの解決策にはならない。
localStorage そのものの使い方、容量、Safariが7日で消す話、sessionStorage・Cookie・IndexedDBの使い分けは localStorage の使い方と、2026年に気をつけること にまとめました。 同じ「エラー文字列から切り分ける」型の話は Unexpected token '<' ... is not valid JSON の原因と直し方 と MySQLの「Incorrect string value」の直し方、 SPAでの計測は GA4でSPAのページビューが計測されない5つの原因 にあります。
Nodeでの typeof とフラグの挙動は、2026年10月25日に Node 24.14.1 で実際に実行して確認したものです。
Web Storage は実験的な機能で、起動時に「いつ変わるかわからない」という警告が出ます。
フレームワーク側の仕様も変わります。
実装する前に、使っているバージョンの公式ドキュメントで確認してください。