Angular SSR環境でのlocalStorage未定義エラーの解決策
Angular 17にアップグレード後、Server-Side Rendering(SSR)環境でlocalStorage is not defined
エラーが発生する問題は、サーバーサイドでブラウザ固有APIが利用できないために発生します。この問題をSSRを無効化せずに解決する方法を解説します。
問題の本質
- ブラウザAPIの制約:
localStorage
はブラウザ固有のAPIで、サーバーサイド(Node.js環境)では利用不可 - SSRの動作: Angular Universalはコンポーネントをサーバーで事前レンダリングするため、コンストラクタや初期ライフサイクルで
localStorage
にアクセスするとエラー発生
解決策 1:PLATFORM_ID
とisPlatformBrowser
を使用(推奨)
原理: Angularのプラットフォーム判定APIで実行環境を検出
typescript
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({/* ... */})
export class ExampleComponent {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
// ブラウザ環境でのみ実行
const userData = localStorage.getItem('user');
console.log(userData);
}
}
}
特徴
- 信頼性◎: Angular公式で推奨されるアプローチ
- 環境判定: サーバーサイド実行を完全に回避
- 汎用性: サービスやインターセプターでも利用可能
解決策 2:afterNextRender
ライフサイクルフック(Angular 17以降)
原理: ブラウザでの最初のレンダリング後にコードを実行
typescript
import { Component, afterNextRender } from '@angular/core';
@Component({/* ... */})
export class StorageComponent {
loggedInUser: any;
constructor() {
afterNextRender(() => {
// クライアントサイドでのみ実行
const authData = localStorage.getItem('auth');
if (authData) {
this.loggedInUser = JSON.parse(authData);
}
});
}
}
特徴
- タイミング最適化: DOMレンダリング後の安全な実行を保証
- SSR互換: サーバーサイドではスキップされる
- 可読性▲: 直感的なコードフロー
選択基準
ケース | 推奨方法 |
---|---|
初期化処理が必要 | PLATFORM_ID + isPlatformBrowser |
DOM操作後のデータ取得 | afterNextRender |
サービス層での制御 | PLATFORM_ID 依存注入 |
解決策 3:DOCUMENT
依存注入(代替案)
原理: Document
オブジェクト経由でブラウザAPIに安全にアクセス
typescript
import { Component, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({/* ... */})
export class LocalStorageComponent {
constructor(@Inject(DOCUMENT) private document: Document) {
const browserWindow = this.document.defaultView;
if (browserWindow?.localStorage) {
browserWindow.localStorage.setItem('key', 'value');
}
}
}
注意点
- 冗長なコードになりやすい
defaultView
のnullチェック必須- 直接アクセスより可読性が低下
ベストプラクティス
サービス層でのカプセル化
typescript// storage.service.ts import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class StorageService { constructor(@Inject(PLATFORM_ID) private platformId: Object) {} getItem(key: string): string | null { if (isPlatformBrowser(this.platformId)) { return localStorage.getItem(key); } return null; } }
初期化タイミングの制御
- コンストラクタでは絶対にアクセスしない
ngOnInit
やイベントハンドラ内で実行
フォールバック処理
typescriptsavePreferences() { if (typeof localStorage === 'undefined') { // 代替ストレージやAPI連携 return; } localStorage.setItem('prefs', data); }
根本原因の理解
- SSRの必要性: パフォーマンス向上・SEO対策のためSSRは推奨
- 設計原則: ブラウザ依存機能は常にガード条件を設定
- SSR切り無効化の問題点:diff
// angular.json(非推奨) "prerender": false, "ssr": false
これらの手法を適用することで、SSRのメリットを維持しつつlocalStorage
を安全に利用できます。特にPLATFORM_ID
とafterNextRender
の組み合わせが、Angular 17以降のベストプラクティスとして推奨されます。