localStorage is not defined の直し方 — SSRで落ちる理由と、use client で直らない理由

先に結論

localStorage is not defined は、localStorage の使い方の問題ではありません。 ブラウザの外でそのコードが走っているという、場所の問題です。

まず、出ているエラーの文面で原因が分かれます。

表1:エラー文字列から原因を引く
エラー意味どこで起きているか
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で意図的に使おうとしている(詳しく)

直し方は、使う場面で決まります。

表2:場面別の直し方
やりたいこと直し方
画面に出す値を 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

関連するものも、まとめて存在しません。

表3:素のNode 24.14.1 での typeof(実測)
グローバルtypeof
localStorageundefined
sessionStorageundefined
windowundefined
documentundefined

だから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

実際に動かすと、こうなりました。

表4:Node 24.14.1 でのWeb Storage(実測)
起動の仕方localStoragesessionStorage
フラグなし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 }) を書くと通りません。 いったんクライアントコンポーネントを挟んで、その中で書きます。

環境別の切り分け

同じエラーでも、どこで起きているかで触る場所が変わります。 まず「どの工程で落ちたか」を特定してください。

表5:環境別・落ちる場所と直し方
環境落ちる工程主な対処
Next.js(App Router)サーバーでのプリレンダリング"use client" + useEffect。全体を外すなら Client Component 内で ssr: false
Next.js(Pages Router)SSR / SSGuseEffect、または 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" を返します。

表6:判定の書き方
書き方サーバーでの結果
typeof window === 'undefined'安全。true になる
typeof localStorage === 'undefined'安全。true になる
window === undefinedReferenceError で落ちる
!windowReferenceError で落ちる

③ サーバーで描かせない

コンポーネント全体がブラウザ専用のときは、サーバーで描くのをやめます。

// 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 を書けるのが利点です。 サーバー側では代わりの内容を出せるので、ブラウザでマウントされるまでの見た目を自分で決められます。

どれを使うか

表7:直し方の使い分け
状況使うもの欠点
値を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の通常の不一致検出を無効にしてしまうからです」
— 同上

表8:suppressHydrationWarning の判断
場面使ってよいか理由
時刻の表示使ってよいサーバーとクライアントで必ず違う。公式が例として挙げている
localStorage の値を出す使わない一致させられる。上の書き方で消える
mismatch の警告がうるさい使わない検出そのものを切ってしまう。原因が隠れる

初期値をどう設計するか

ここまでの直し方には共通の弱点があります。 マウントされるまでの一瞬、保存値ではない表示が出ます。 ダークテーマの保存値があるのに、一瞬だけ明るい画面が出る、というやつです。

表9:ちらつきへの対処
対処内容向き不向き
そのまま受け入れる初期値で描いてから差し替えるほとんどの値はこれでよい
マウントまで出さないmounted が false の間は該当部分を描かないレイアウトが動く。その領域の高さを確保しておく
Cookie に移すサーバーがリクエストで値を読めるので、最初から正しく描けるテーマや言語のような「初期表示に効く値」はこれが本筋
head で先に適用する描画前に走る小さなスクリプトでクラスを付けるテーマ限定の手段。やりすぎない

3行目が答えになることが多いです。 localStorage はサーバーから見えません。 「サーバーが最初のHTMLを描くときに必要な値」を localStorage に置くこと自体が、設計の選択として合っていません。 初期表示に効く値は Cookie、それ以外は localStorage、という分け方が素直です。

AIに任せるときの注意

表10: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 そのものの使い方、容量、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 は実験的な機能で、起動時に「いつ変わるかわからない」という警告が出ます。 フレームワーク側の仕様も変わります。 実装する前に、使っているバージョンの公式ドキュメントで確認してください。