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