DEV_NET_CORE
GET_STARTED
ReactProduction data access, API clients, and frontend auth

Browser storage trade-offs: localStorage, sessionStorage, memory storage, and cookies

Overview

Browser storage trade-offs are about choosing where a React application keeps client-side state, preferences, cached data, and authentication-related values. The common options are localStorage, sessionStorage, in-memory state, and cookies. Each option has different behavior for persistence, tab isolation, JavaScript access, automatic request inclusion, security, and user experience.

This topic matters because storage decisions often become security decisions. A token placed in localStorage is easy to read and attach to headers, but it is also easy for injected JavaScript to steal. A cookie can be protected from JavaScript with HttpOnly, but cookies are sent automatically by the browser and can introduce CSRF concerns. Memory storage avoids disk persistence, but it disappears on refresh and requires careful session recovery.

In React apps, storage choices appear in auth providers, API clients, RTK Query base queries, Axios interceptors, route guards, theme settings, feature flags, form drafts, and offline-friendly experiences. Interviewers ask about this topic because it shows whether a candidate can connect frontend convenience with a realistic browser threat model.

The practical goal is not to memorize one universal answer. The goal is to match storage to data sensitivity, required lifetime, cross-tab behavior, server auth model, and recovery UX.

Core Concepts

Browser Storage Threat Model

Browser storage is client-controlled. Anything stored in the browser should be treated as readable or modifiable by the user, browser extensions, malware on the device, or injected JavaScript if the application has an XSS vulnerability.

Important questions:

  • Is the value sensitive?
  • Does the value grant access?
  • Does it need to survive refresh?
  • Does it need to survive browser restart?
  • Should it be shared across tabs?
  • Should JavaScript be able to read it?
  • Should the browser automatically send it to the server?
  • What happens if the value is stale, deleted, or modified?

Good storage design starts with these questions instead of starting with a favorite API.

localStorage

localStorage is a browser key-value store scoped to an origin. Its data persists across browser sessions until explicitly cleared by code, the user, browser policy, or storage eviction behavior.

Example:

Code
localStorage.setItem("theme", "dark");

const theme = localStorage.getItem("theme");

localStorage.removeItem("theme");

Common uses:

  • Theme preference.
  • Dismissed onboarding banners.
  • Non-sensitive feature preferences.
  • Small cached UI hints.
  • Last selected workspace or region identifier.

Strengths:

  • Simple API.
  • Persists after refresh and browser restart.
  • Shared across tabs for the same origin.
  • Useful for non-sensitive preferences.

Risks:

  • Accessible to any JavaScript running on the origin.
  • Exposed by XSS.
  • Not automatically encrypted by the web platform.
  • Synchronous API can block the main thread if abused.
  • Values are strings, so objects need serialization.
  • All apps on the same origin share visibility.

localStorage should not be treated as a secure place for session identifiers, refresh tokens, or other credentials.

sessionStorage

sessionStorage is similar to localStorage, but it is scoped to a browser tab or page session. Data normally lasts until the tab or browsing context closes.

Example:

Code
sessionStorage.setItem("checkoutStep", "shipping");

const checkoutStep = sessionStorage.getItem("checkoutStep");

Common uses:

  • In-progress wizard state.
  • Temporary UI state for a single tab.
  • Non-sensitive form recovery during one tab session.
  • Return path after login in a specific tab.

Strengths:

  • Does not persist as long as localStorage.
  • Usually isolated per tab.
  • Good for temporary, tab-specific state.

Risks:

  • Still accessible to JavaScript.
  • Still exposed by XSS.
  • Lost when the tab closes.
  • Not a secure token store.
  • Can be awkward for multi-tab workflows.

sessionStorage reduces persistence, but it does not make sensitive data safe from injected scripts.

Memory Storage

Memory storage means keeping values in JavaScript memory, such as React state, module-level variables, Redux state, Zustand state, or TanStack Query cache.

Example:

Code
let accessToken: string | null = null;

export function setAccessToken(token: string | null) {
  accessToken = token;
}

export function getAccessToken() {
  return accessToken;
}

Common uses:

  • Short-lived access tokens.
  • Current user object.
  • In-flight request state.
  • UI state that should reset on refresh.
  • Sensitive values that should not persist to disk.

Strengths:

  • Cleared on full page reload.
  • Not persisted to disk by the app.
  • Not shared across browser restarts.
  • Reduces long-term exposure compared with persistent storage.

Risks:

  • Still accessible to injected JavaScript running in the page.
  • Lost on refresh.
  • Not shared across tabs unless explicitly synchronized.
  • Requires session restoration strategy.
  • Can produce confusing UX after reload if the server session still exists.

Memory storage is often a reasonable place for short-lived access tokens when refresh is handled by an HttpOnly cookie or backend session.

Cookies

Cookies are small pieces of data stored by the browser and sent automatically with matching HTTP requests. They are usually set by the server using the Set-Cookie response header.

Example:

Code
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600

Common uses:

  • Server-managed session IDs.
  • Refresh-token cookies.
  • CSRF tokens when JavaScript must read the token.
  • Small user preferences.
  • A/B test bucketing.

Strengths:

  • Can be marked HttpOnly so JavaScript cannot read them.
  • Can be restricted to HTTPS with Secure.
  • Can restrict cross-site sending behavior with SameSite.
  • Automatically sent by the browser to matching requests.
  • Works well with server-side session validation.

Risks:

  • Automatically sent cookies can create CSRF risk.
  • Size limits are much smaller than Web Storage.
  • Cookies are sent on every matching request, adding overhead.
  • Misconfigured Domain and Path can broaden exposure.
  • JavaScript cannot read HttpOnly cookies, so client code needs a different way to know auth state.

Cookies are not automatically secure. Their safety depends heavily on cookie flags, server validation, CSRF defenses, CORS, and HTTPS.

Cookies vs Web Storage

Cookies and Web Storage behave very differently.

localStorage and sessionStorage:

  • Are read and written by JavaScript.
  • Are not automatically sent with HTTP requests.
  • Are scoped by origin.
  • Can store more data than cookies.
  • Are poor choices for secrets because XSS can read them.

Cookies:

  • Are included automatically in matching HTTP requests.
  • Can be set and controlled by the server.
  • Can be hidden from JavaScript with HttpOnly.
  • Have security attributes such as Secure and SameSite.
  • Can create CSRF risk because they are automatically sent.

The trade-off is sharp: Web Storage is convenient for JavaScript-controlled data, while cookies are better for server-controlled session credentials when configured carefully.

XSS Risk

Cross-site scripting changes the storage decision. If an attacker can run JavaScript in the app, that script can read localStorage, read sessionStorage, inspect memory state available to the page, and call APIs as the user.

HttpOnly cookies reduce one important XSS impact: injected JavaScript cannot directly read the cookie value. However, XSS can still perform actions as the user while the cookie is present.

Implications:

  • Do not store long-lived credentials in Web Storage.
  • Use output encoding and avoid dangerous HTML rendering.
  • Use Content Security Policy where appropriate.
  • Treat all client-side storage as untrusted input when reading it.
  • Do not trust values from storage for authorization.

Storage choices reduce risk; they do not replace XSS prevention.

CSRF Risk

Cross-site request forgery matters when authentication credentials are sent automatically by the browser, as with cookies.

If an API relies on cookies for authentication, a malicious site may be able to trigger a request from the user's browser unless defenses are in place.

Common defenses:

  • SameSite=Lax or SameSite=Strict when compatible with product flows.
  • CSRF tokens for unsafe requests.
  • Origin and Referer validation on the server.
  • Avoiding unsafe side effects in GET requests.
  • CORS configured to trusted origins only.

Bearer tokens stored in memory or Web Storage and manually attached to headers are not automatically sent cross-site, so the CSRF risk model is different. But those tokens are more exposed to XSS if JavaScript can read them.

Persistence and User Experience

Storage lifetime affects UX.

localStorage:

  • Best persistence.
  • Survives restart.
  • Good for preferences.
  • Risky for credentials.

sessionStorage:

  • Survives reload in the same tab.
  • Usually clears when tab closes.
  • Good for temporary flow state.

Memory:

  • Clears on refresh.
  • Good for sensitive short-lived data.
  • Requires session restoration.

Cookie:

  • Lifetime controlled by Expires, Max-Age, or session behavior.
  • Can support server-managed login across refreshes.
  • Can be HttpOnly for safer credential storage.

Good UX usually combines storage types. For example, a React app might keep the access token in memory, keep the refresh session in an HttpOnly cookie, and store only theme preference in localStorage.

Cross-Tab Behavior

Storage choices behave differently across tabs.

localStorage is shared across tabs for the same origin and can emit a storage event in other tabs.

Example:

Code
window.addEventListener("storage", (event) => {
  if (event.key === "logout") {
    clearAuthState();
  }
});

function broadcastLogout() {
  localStorage.setItem("logout", String(Date.now()));
}

sessionStorage is tab-specific. Memory state is also tab-specific. Cookies are shared by matching requests across tabs because the browser cookie jar is shared.

For auth, cross-tab consistency matters. If the user logs out in one tab, other tabs should not continue showing privileged UI.

Server Authority

The server must be the authority for authentication and authorization. Client storage can remember hints, but it cannot prove permission.

Unsafe examples:

Code
const isAdmin = localStorage.getItem("role") === "admin";

Better approach:

  • Server validates session or token.
  • Server returns the current user and permissions.
  • UI uses that data to render affordances.
  • Server still enforces every protected operation.

Storage values can be modified by the user. Never use browser storage as an authorization boundary.

Storage for Authentication

Common auth storage patterns:

  • Access token in memory, refresh cookie as HttpOnly, Secure, and SameSite.
  • Server session cookie with backend-managed session state.
  • Bearer access token in memory, refreshed by a backend-for-frontend.
  • Token in Web Storage for simpler SPAs, accepted only with a clear understanding of XSS risk.

A strong interview answer avoids claiming that one pattern is always perfect. It explains the risk trade-offs:

  • Web Storage is easy but exposed to JavaScript.
  • Cookies can protect values from JavaScript but need CSRF defenses.
  • Memory reduces persistence but requires refresh and recovery.
  • Server-side sessions reduce frontend token handling but require backend session management.

Storage for Non-Auth Data

Good localStorage candidates:

  • Theme.
  • Locale.
  • Table density.
  • Dismissed tips.
  • Recently selected non-sensitive IDs.

Good sessionStorage candidates:

  • In-progress wizard step.
  • Temporary return URL.
  • Non-sensitive form draft for one tab.

Good memory candidates:

  • Modal state.
  • Current page filters when URL persistence is not needed.
  • In-flight request state.
  • Sensitive temporary values.

Good cookie candidates:

  • Server session ID.
  • Refresh token managed by server.
  • Small server-needed preference.

For larger structured client caches, IndexedDB is often a better fit than Web Storage, but it has similar XSS and local-device trust concerns.

Logout and Cleanup

Logout should clear all relevant client state.

Typical logout flow:

Code
async function logout() {
  await api.post("/auth/logout");
  queryClient.clear();
  sessionStorage.clear();
  localStorage.removeItem("returnTo");
  setAccessToken(null);
}

If auth uses cookies, the server must expire the cookie. JavaScript cannot delete an HttpOnly cookie directly.

Important cleanup targets:

  • In-memory access token.
  • User profile state.
  • Query cache with sensitive data.
  • sessionStorage flow state.
  • Non-sensitive localStorage auth hints.
  • Cross-tab logout notification.

Do not rely only on hiding UI. Clear cached sensitive data too.

Common Mistakes

Common mistakes include:

  • Storing refresh tokens in localStorage.
  • Assuming sessionStorage is secure because it is temporary.
  • Trusting roles or permissions read from browser storage.
  • Forgetting that all apps on the same origin share Web Storage.
  • Keeping sensitive query cache after logout.
  • Using cookies without HttpOnly, Secure, and a thoughtful SameSite value.
  • Ignoring CSRF when using cookie-based auth.
  • Breaking refresh UX by storing access tokens only in memory without a recovery path.
  • Reading browser storage during server rendering without guards.
  • Failing to synchronize logout across tabs.

Best Practices

Best practices include:

  • Store only non-sensitive preferences in localStorage.
  • Use sessionStorage for temporary, tab-scoped, non-sensitive data.
  • Keep short-lived sensitive values in memory when possible.
  • Prefer HttpOnly, Secure cookies for server-managed session credentials.
  • Add CSRF defenses when cookies authenticate unsafe requests.
  • Keep access tokens short-lived.
  • Treat storage values as untrusted input.
  • Clear sensitive caches on logout.
  • Use different subdomains for unrelated apps that need storage isolation.
  • Guard browser-only storage access during SSR or tests.
  • Choose storage based on lifetime, sensitivity, and request behavior.

Interview Practice

PreviousAxios request and response interceptors for auth headers, global errors, logging, and retry behaviorNext UpCentralized API clients with `fetch`, Axios, RTK Query, or TanStack Query