Skip to content

useSessionStorage

Creates a writable signal that automatically syncs with sessionStorage. The signal persists its value during the page session (until the tab/window is closed) but does not sync across tabs like localStorage.

MDN Reference: Window.sessionStorage

Usage

Basic Usage

typescript
import { useSessionStorage } from 'ng-reactive-utils';

@Component({
  template: `
    <div>
      <p>Page views: {{ pageViews() }}</p>
      <button (click)="pageViews.set(pageViews() + 1)">Increment</button>
    </div>
  `,
})
class PageViewsComponent {
  // Value persists during the session (until tab is closed)
  pageViews = useSessionStorage('page-views', 0);
}

Wizard/Multi-Step Form State

typescript
interface WizardState {
  currentStep: number;
  completedSteps: number[];
  formData: {
    personalInfo: { name: string; email: string };
    preferences: { newsletter: boolean; theme: string };
  };
}

@Component({
  template: `
    <div class="wizard">
      <h2>Step {{ wizardState().currentStep }} of 3</h2>
      @if (wizardState().currentStep === 1) {
        <!-- Personal info form -->
      }
      @if (wizardState().currentStep === 2) {
        <!-- Preferences form -->
      }
      <button (click)="nextStep()">Next</button>
    </div>
  `,
})
class WizardComponent {
  wizardState = useSessionStorage<WizardState>('wizard-state', {
    currentStep: 1,
    completedSteps: [],
    formData: {
      personalInfo: { name: '', email: '' },
      preferences: { newsletter: false, theme: 'light' },
    },
  });

  nextStep() {
    this.wizardState.update((state) => ({
      ...state,
      currentStep: state.currentStep + 1,
      completedSteps: [...state.completedSteps, state.currentStep],
    }));
  }
}

Temporary Filters

typescript
interface FilterState {
  category: string;
  priceRange: { min: number; max: number };
  sortBy: string;
}

@Component({
  template: `
    <div>
      <select (change)="updateCategory($any($event.target).value)">
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="books">Books</option>
      </select>
      <button (click)="resetFilters()">Reset</button>
    </div>
  `,
})
class ProductFiltersComponent {
  // Filters persist during browsing session but reset when tab closes
  filters = useSessionStorage<FilterState>('product-filters', {
    category: 'all',
    priceRange: { min: 0, max: 1000 },
    sortBy: 'name',
  });

  updateCategory(category: string) {
    this.filters.update((f) => ({ ...f, category }));
  }

  resetFilters() {
    this.filters.set({
      category: 'all',
      priceRange: { min: 0, max: 1000 },
      sortBy: 'name',
    });
  }
}
typescript
@Component({
  template: `
    <div>
      <p>Pages visited: {{ visitedPages().length }}</p>
      <button (click)="goBack()" [disabled]="!canGoBack()">Back</button>
    </div>
  `,
})
class NavigationComponent {
  visitedPages = useSessionStorage<string[]>('visited-pages', []);

  canGoBack = computed(() => this.visitedPages().length > 1);

  constructor() {
    const router = inject(Router);

    // Track page visits
    effect(() => {
      const currentUrl = router.url;
      this.visitedPages.update((pages) => [...pages, currentUrl]);
    });
  }

  goBack() {
    const pages = this.visitedPages();
    if (pages.length > 1) {
      // Navigate to previous page
      const previousPage = pages[pages.length - 2];
      // ... router.navigateByUrl(previousPage);
    }
  }
}

Temporary Authentication

typescript
@Component({
  template: `
    <div>
      @if (sessionToken()) {
        <p>Logged in</p>
        <button (click)="logout()">Logout</button>
      } @else {
        <button (click)="login()">Login</button>
      }
    </div>
  `,
})
class SessionAuthComponent {
  // Token only persists for this tab session
  sessionToken = useSessionStorage<string | null>('session-token', null);

  login() {
    this.sessionToken.set('temp-session-abc123');
  }

  logout() {
    this.sessionToken.set(null);
  }
}

Scroll Position Restoration

typescript
@Component({
  template: `<div class="content" (scroll)="saveScrollPosition($event)">...</div>`,
})
class ScrollRestoreComponent {
  scrollPosition = useSessionStorage('scroll-position', { x: 0, y: 0 });

  ngAfterViewInit() {
    // Restore scroll position
    const { x, y } = this.scrollPosition();
    window.scrollTo(x, y);
  }

  saveScrollPosition(event: Event) {
    const element = event.target as HTMLElement;
    this.scrollPosition.set({
      x: element.scrollLeft,
      y: element.scrollTop,
    });
  }
}

Tab-Specific Settings

typescript
@Component({
  template: `
    <div>
      <label>
        <input
          type="checkbox"
          [checked]="devMode()"
          (change)="devMode.set(!devMode())"
        />
        Developer Mode (this tab only)
      </label>
    </div>
  `,
})
class TabSettingsComponent {
  // Setting only applies to this tab
  devMode = useSessionStorage('dev-mode', false);
}

Custom Serialization with Date

typescript
interface Session {
  startTime: Date;
  userId: string;
  actions: string[];
}

@Component({
  template: `
    <div>
      <p>Session started: {{ session().startTime.toLocaleString() }}</p>
      <p>Actions: {{ session().actions.length }}</p>
    </div>
  `,
})
class SessionTrackerComponent {
  session = useSessionStorage<Session>(
    'user-session',
    {
      startTime: new Date(),
      userId: '',
      actions: [],
    },
    {
      serializer: {
        read: (value: string) => {
          const parsed = JSON.parse(value);
          return {
            ...parsed,
            startTime: new Date(parsed.startTime),
          };
        },
        write: (value: Session) =>
          JSON.stringify({
            ...value,
            startTime: value.startTime.toISOString(),
          }),
      },
    }
  );
}

Parameters

ParameterTypeDescription
keystringsessionStorage key to store the value under
defaultValueTDefault value if no stored value exists
optionsUseStorageOptions<T> (optional)Configuration options

Options Object

PropertyTypeDefaultDescription
serializer{ read: (value: string) => T; write: (value: T) => string }JSON serializerCustom serialization logic
writeDefaultsbooleantrueWrite default value to storage on initialization if not present

Returns

WritableSignal<T> - A writable signal that syncs with sessionStorage

Notes

  • Writable signal: Returned signal can be updated using .set() and .update()
  • Automatic persistence: Changes to the signal automatically save to sessionStorage
  • Session-scoped: Data is cleared when the tab/window is closed
  • Tab-isolated: Each tab has its own sessionStorage (unlike localStorage)
  • SSR safe: Returns default value on server, hydrates from sessionStorage on client
  • Type safe: Full TypeScript support with generic type parameter
  • JSON by default: Uses JSON.stringify/JSON.parse for serialization by default
  • Custom serialization: Provide custom serializer for complex types (Date, Map, Set, etc.)
  • Null removes: Setting the value to null removes the key from sessionStorage
  • Error handling: Gracefully handles quota exceeded errors and parse errors
  • Storage events: Automatically listens to storage events (though rarely triggered for sessionStorage)
  • Automatic cleanup: Storage event listeners are removed when component is destroyed

sessionStorage vs localStorage

FeaturesessionStoragelocalStorage
LifetimeUntil tab/window closesPersists forever
ScopePer tab/windowShared across all tabs
Storage eventsRarely needed (tab-isolated)Essential for cross-tab sync
Use casesTemporary data, wizard state, filtersPreferences, auth, long-term data
Size limit~5-10MB (same as localStorage)~5-10MB

Common Use Cases

  • Multi-step forms/wizards: Preserve form data during the session
  • Temporary filters: Store filter/sort state during browsing
  • Tab-specific settings: Settings that only apply to current tab
  • Session tracking: Track user actions during the current session
  • Navigation history: Remember where the user has been in this tab
  • Scroll position: Restore scroll position on back navigation
  • Draft data: Auto-save drafts that should be discarded when tab closes
  • Temporary authentication: Short-lived session tokens

When to Use sessionStorage vs localStorage

Use sessionStorage when:

  • ✅ Data should be cleared when the tab closes
  • ✅ Data is specific to a single tab/window
  • ✅ You want to prevent data from persisting too long
  • ✅ Working with temporary wizard/form state

Use localStorage when:

  • ✅ Data should persist across sessions
  • ✅ You need to sync data across tabs
  • ✅ Storing user preferences or settings
  • ✅ Data needs to survive browser restarts

Source

ts
import { WritableSignal } from '@angular/core';
import { useStorage } from '../use-storage-base/use-storage-base.composable';
import { UseStorageOptions } from '../use-storage-base/types';

/**
 * Creates a writable signal that automatically syncs with sessionStorage. The signal persists
 * its value during the page session (until the tab/window is closed) but does not sync across
 * tabs like localStorage.
 *
 * On the server, returns the default value and syncs to actual value once hydrated on the client.
 *
 * @param key - sessionStorage key to store the value under
 * @param defaultValue - Default value if no stored value exists
 * @param options - Configuration options
 *
 * @example
 * ```ts
 * // Wizard state that clears on tab close
 * const wizardStep = useSessionStorage('wizard-step', 1);
 * wizardStep.set(wizardStep() + 1);
 * ```
 *
 * @example
 * ```ts
 * // Temporary filters
 * const filters = useSessionStorage('filters', { category: 'all', sortBy: 'name' });
 * filters.update(f => ({ ...f, category: 'electronics' }));
 * ```
 */
export function useSessionStorage<T>(
  key: string,
  defaultValue: T,
  options: UseStorageOptions<T> = {},
): WritableSignal<T> {
  return useStorage('sessionStorage', key, defaultValue, options);
}

Released under the MIT License.