Skip to content

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_IDisPlatformBrowserを使用(推奨)

原理: 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チェック必須
  • 直接アクセスより可読性が低下

ベストプラクティス

  1. サービス層でのカプセル化

    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;
      }
    }
  2. 初期化タイミングの制御

    • コンストラクタでは絶対にアクセスしない
    • ngOnInitやイベントハンドラ内で実行
  3. フォールバック処理

    typescript
    savePreferences() {
      if (typeof localStorage === 'undefined') {
        // 代替ストレージやAPI連携
        return;
      }
      localStorage.setItem('prefs', data);
    }

根本原因の理解

  • SSRの必要性: パフォーマンス向上・SEO対策のためSSRは推奨
  • 設計原則: ブラウザ依存機能は常にガード条件を設定
  • SSR切り無効化の問題点:
    diff
    // angular.json(非推奨)
    "prerender": false,
    "ssr": false

これらの手法を適用することで、SSRのメリットを維持しつつlocalStorageを安全に利用できます。特にPLATFORM_IDafterNextRenderの組み合わせが、Angular 17以降のベストプラクティスとして推奨されます。