React cookbook · Advanced
Refresh expired access tokens automatically
Retry API calls transparently when the access token expires, without firing multiple refresh requests at once.
Approach
Wrap fetch: on 401, call the refresh endpoint (refresh token in an HttpOnly cookie) once — concurrent callers share the same in-flight refresh promise ("single flight") — then retry the original request. If refresh fails, clear the session and redirect to login.
Solution
let accessToken: string | null = null;
let refreshing: Promise<string | null> | null = null;
async function refreshAccessToken(): Promise<string | null> {
refreshing ??= fetch("/api/auth/refresh", { method: "POST", credentials: "include" })
.then(async (res) => (res.ok ? ((await res.json()) as { access_token: string }).access_token : null))
.finally(() => { refreshing = null; });
return refreshing;
}
export async function apiFetch(input: string, init: RequestInit = {}, retried = false): Promise<Response> {
const headers = new Headers(init.headers);
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
const res = await fetch(input, { ...init, headers, credentials: "include" });
if (res.status !== 401 || retried) return res;
accessToken = await refreshAccessToken();
if (!accessToken) {
window.location.assign(`/login?next=${encodeURIComponent(location.pathname)}`);
return res;
}
return apiFetch(input, init, true);
}
export function setAccessToken(token: string | null) { accessToken = token; }
Pitfalls
- Storing refresh tokens in localStorage (XSS can steal them).
- Infinite retry loops when refresh keeps returning 401.
- Several tabs refreshing simultaneously with rotating refresh tokens (use a BroadcastChannel or server-side grace period).
Variations
- Cookie-only sessions (BFF pattern) avoid tokens in JavaScript entirely.
- Axios interceptors implement the same single-flight pattern.
Further reading: IETF: OAuth 2.0 for Browser-Based Apps
Reference: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps
More React recipes
- Add optimistic updates to a todo list
- Build an accessible modal dialog
- Build infinite scroll with TanStack Query
- Call a cookie-authenticated API with CSRF protection
- Debounce a search input in React
- Handle errors gracefully with error boundaries
- Implement dark mode that follows the system
- Poll a long-running job's status