Adding Authentication to React AppsEntry 4 of 11

Storing and Reading the Token

After a successful login, the token is saved in localStorage so it survives page reloads, and helper functions read it back.

Notes

  • Store the token (and its expiration, Entry 9) right after the action receives it.
  • Centralize access in a util/auth.ts module: storeAuthToken, getAuthToken, clearAuthToken. No other file touches localStorage directly.
  • getAuthToken() is a plain function, not a hook, so it works in loaders, actions, and components.

What / Why / When

What

Persisting the token on the client.

Why

The user stays logged in across reloads and new tabs.

When

Right after login/signup succeeds.

Code compare

React
// util/auth.tsimport { redirect } from "react-router";​const TOKEN_KEY = "token";const EXPIRATION_KEY = "expiration";const TOKEN_LIFETIME_MS = 60 * 60 * 1000; // 1 hour, matching the backend​export type AuthToken = string | "EXPIRED" | null;​export function storeAuthToken(token: string) {  localStorage.setItem(TOKEN_KEY, token);  localStorage.setItem(EXPIRATION_KEY, new Date(Date.now() + TOKEN_LIFETIME_MS).toISOString());}​export function clearAuthToken() {  localStorage.removeItem(TOKEN_KEY);  localStorage.removeItem(EXPIRATION_KEY);}​export function getTokenDuration(): number {  const stored = localStorage.getItem(EXPIRATION_KEY);  if (!stored) return 0;  return new Date(stored).getTime() - Date.now();}​export function getAuthToken(): AuthToken {  const token = localStorage.getItem(TOKEN_KEY);  if (!token) return null;  if (getTokenDuration() <= 0) return "EXPIRED";  return token;}​// Used as the root route's loader (Entry 6)export function tokenLoader() {  return getAuthToken();}​// Used to protect routes (Entry 8)export function checkAuthLoader() {  const token = getAuthToken();  if (!token || token === "EXPIRED") return redirect("/auth?mode=login");  return null;}
Angular
// services/auth.service.tsimport { computed, inject, Injectable, signal } from "@angular/core";import { HttpClient } from "@angular/common/http";import { Router } from "@angular/router";import { tap } from "rxjs";​const TOKEN_KEY = "token";const EXPIRATION_KEY = "expiration";const TOKEN_LIFETIME_MS = 60 * 60 * 1000;​@Injectable({ providedIn: "root" })export class AuthService {  private http = inject(HttpClient);  private router = inject(Router);  private api = "http://localhost:8080";  private logoutTimer?: ReturnType<typeof setTimeout>;​  private _token = signal<string | null>(this.readStoredToken());  readonly token = this._token.asReadonly();  readonly isAuthenticated = computed(() => this._token() !== null);​  constructor() {    if (this._token()) this.scheduleLogout(this.tokenDuration()); // Entry 9  }​  authenticate(mode: "login" | "signup", credentials: { email: string; password: string }) {    return this.http      .post<{ token: string }>(`${this.api}/${mode}`, credentials)      .pipe(tap(({ token }) => this.storeToken(token)));  }​  logout() {    localStorage.removeItem(TOKEN_KEY);    localStorage.removeItem(EXPIRATION_KEY);    clearTimeout(this.logoutTimer);    this._token.set(null);    this.router.navigate(["/"]);  }​  private storeToken(token: string) {    localStorage.setItem(TOKEN_KEY, token);    localStorage.setItem(EXPIRATION_KEY, new Date(Date.now() + TOKEN_LIFETIME_MS).toISOString());    this._token.set(token);    this.scheduleLogout(TOKEN_LIFETIME_MS);  }​  private tokenDuration(): number {    const stored = localStorage.getItem(EXPIRATION_KEY);    return stored ? new Date(stored).getTime() - Date.now() : 0;  }​  private readStoredToken(): string | null {    const token = localStorage.getItem(TOKEN_KEY);    if (!token || this.tokenDuration() <= 0) {      localStorage.removeItem(TOKEN_KEY);      localStorage.removeItem(EXPIRATION_KEY);      return null;    }    return token;  }​  private scheduleLogout(ms: number) {    clearTimeout(this.logoutTimer);    this.logoutTimer = setTimeout(() => this.logout(), ms);  }}

Differences and gotchas

  • React reads localStorage on demand in loaders; nothing re-renders when it changes, so the UI relies on loader revalidation (Entry 6).
  • Angular keeps the token in a signal, so every component and the interceptor see changes instantly.
  • Never put the token in a URL or query string; URLs end up in logs and browser history.

Quick reference

TaskReactAngular
StorestoreAuthToken(token)AuthService.storeToken() (private)
ReadgetAuthToken()authService.token()
Is logged intoken && token !== 'EXPIRED'authService.isAuthenticated()
ClearclearAuthToken()authService.logout()