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.tsmodule:storeAuthToken,getAuthToken,clearAuthToken. No other file toucheslocalStoragedirectly. 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
// 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;}// 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
localStorageon 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
| Task | React | Angular |
|---|---|---|
| Store | storeAuthToken(token) | AuthService.storeToken() (private) |
| Read | getAuthToken() | authService.token() |
| Is logged in | token && token !== 'EXPIRED' | authService.isAuthenticated() |
| Clear | clearAuthToken() | authService.logout() |