Adding Authentication to React AppsEntry 9 of 11
Token Expiration and Automatic Logout
Tokens expire (often after one hour). The app stores the expiration time and logs the user out automatically when it's reached.
Notes
- The backend sets the token's lifetime; the frontend stores a matching expiration timestamp (Entry 4).
getTokenDuration()returns the remaining milliseconds;getAuthToken()returns'EXPIRED'when it's past.- In the root layout, a
useEffectreads the token from the root loader:- No token: do nothing.
'EXPIRED': submit to/logoutimmediately.- Valid: set a timer for the remaining duration, then submit to
/logout.
useSubmit()triggers the logout action from code (see React Router, Entry 16).- Clear the timer in the effect's cleanup so logging out manually doesn't leave a stray timer.
- Even with auto logout, the backend may reject a token early (e.g. revoked). Handle 401 responses by logging out too.
What / Why / When
- What
Client-side expiry tracking.
- Why
The UI shouldn't show a logged-in state with a token the backend will reject.
- When
Any app using expiring tokens.
Code compare
// pages/Root.tsximport { useEffect } from "react";import { Outlet, useLoaderData, useSubmit } from "react-router";import MainNavigation from "../components/MainNavigation";import { getTokenDuration, type AuthToken } from "../util/auth";export default function RootLayout() { const token = useLoaderData() as AuthToken; const submit = useSubmit(); useEffect(() => { if (!token) return; if (token === "EXPIRED") { submit(null, { action: "/logout", method: "post" }); return; } const timer = setTimeout(() => { submit(null, { action: "/logout", method: "post" }); }, getTokenDuration()); return () => clearTimeout(timer); }, [token, submit]); return ( <> <MainNavigation /> <main> <Outlet /> </main> </> );}The timer lives in AuthService (Entry 4): scheduleLogout() runs on startup for a stored token and after every login. To also log out on a 401 from the backend, extend the interceptor:
// interceptors/auth.interceptor.tsexport const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const token = authService.token(); const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req; return next(authReq).pipe( catchError((err: HttpErrorResponse) => { if (err.status === 401 && token) authService.logout(); return throwError(() => err); }), );};Differences and gotchas
- React ties the timer to a component (
RootLayout); Angular ties it to a singleton service, so it's independent of what's rendered. setTimeoutcaps out around 24.8 days (2³¹ − 1 ms). Fine for hour-long tokens, but very long-lived tokens need a different approach.
Quick reference
| Task | React | Angular |
|---|---|---|
| Store expiry | localStorage expiration | Same, inside AuthService |
| Detect expired on load | getAuthToken() === 'EXPIRED' | readStoredToken() returns null |
| Auto logout | useEffect + setTimeout + useSubmit | scheduleLogout() in service |
| Logout on 401 | Check status in services | Interceptor catchError |