How to Persist React State Safely

Persist React state to the browser with localStorage and sessionStorage. Load saved values on mount, and handle JSON and SSR safely.

6 min read

To persist React state means saving it somewhere that survives a reload, usually the browser storage. The safe pattern is to read the saved value once when state initializes and write it back when it changes. It also matters when not to persist, because a saved copy of stale data is worse than no copy at all.

Choose the right storage

localStorage keeps data across sessions and tabs. sessionStorage keeps data for one tab and clears when the tab closes.

localStoragesessionStorage
LifetimeUntil removedUntil the tab closes
Shared across tabsYesNo
Good forTheme, saved draftOne session form

Both store strings only, so objects must be serialized with JSON before saving. For most cases localStorage is the default; reach for sessionStorage when the data should disappear with the tab.

Read the saved value

Initialize state lazily so the browser is read once, on the first render.

App.jsxApp.jsx
const [name, setName] = useState(() => {
  return localStorage.getItem("name") ?? "";
});

The function form of useState runs only on the first render. On later renders React ignores it, so the browser is not read on every keystroke. The nullish operator provides a default when the key is missing, so the first visit starts with an empty value.

Write changes back

Update the saved value in the event handler, right where the state changes.

App.jsxApp.jsx
function handleNameChange(event) {
  const next = event.target.value;
  setName(next);
  localStorage.setItem("name", next);
}

Saving in the handler keeps the write close to the change. For values that change from several places, a custom hook that pairs the state and the write is easier, as build a useLocalStorage hook in React shows. If the same value changes from many handlers, centralize the write in one place so a change is never saved twice.

Store objects as JSON

Objects must go through JSON.stringify before saving and JSON.parse after loading.

App.jsxApp.jsx
const [filters, setFilters] = useState(() => {
  const saved = localStorage.getItem("filters");
  return saved ? JSON.parse(saved) : {};
});
 
function updateFilters(next) {
  setFilters(next);
  localStorage.setItem("filters", JSON.stringify(next));
}

JSON.parse can throw if the stored value was corrupted or written by old code. Wrap it in a try block, or store a version number with the data, so a bad value falls back to a default instead of crashing the app. Start with a try block, and add a version only when you actually change the shape.

Avoid storing secrets

localStorage is readable by any script on the page, so it is the wrong place for tokens, passwords, or session data. Keep authentication in HttpOnly cookies, which JavaScript cannot read. The same caution applies to any value you would not want pasted into the developer console.

Sync across tabs

When another tab writes to localStorage, the browser fires a storage event. Listening to it keeps two open tabs in step.

App.jsxApp.jsx
import { useEffect } from "react";
 
useEffect(() => {
  function handleStorage(event) {
    if (event.key === "name") setName(event.newValue);
  }
  window.addEventListener("storage", handleStorage);
  return () => window.removeEventListener("storage", handleStorage);
}, []);

The event fires only in other tabs, not the tab that made the write. The cleanup removes the listener when the component unmounts.

When persistence helps

Persist values the user would expect to survive a reload: a theme, a saved draft, or a collapsed sidebar. Do not persist values that must stay fresh, like a feed or a price, because a saved copy only goes stale. For those, server state with a query library is the better home.

Handle server rendering

localStorage does not exist on the server, so code that reads it during render can crash a server rendered app. A lazy useState initializer still runs on the server in frameworks with SSR, so guard the read instead of calling localStorage directly.

App.jsxApp.jsx
const [name, setName] = useState(() => {
  if (typeof window === "undefined") return "";
  return localStorage.getItem("name") ?? "";
});

The guard returns the same default on the server and on the first client render. If the saved value can differ from that default, load it after mount so hydration does not warn about mismatched content.

A related pattern for browser APIs is in how to synchronize React with browser APIs. For larger stores, libraries add persistence middleware, as how to use Zustand explains.

Rune AI

Rune AI

Key Insights

  • Read the saved value with a lazy useState initializer.
  • Write back in the event handler where state changes.
  • Serialize objects with JSON.stringify and JSON.parse.
  • Keep secrets in HttpOnly cookies, not localStorage.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between localStorage and sessionStorage?

localStorage keeps data until removed. sessionStorage keeps data for one tab and clears when the tab closes.

Can localStorage store objects?

No, it stores strings only. Convert objects with JSON.stringify before saving and JSON.parse after loading.

Is localStorage safe for tokens?

No. Any script on the page can read it. Keep secrets in HttpOnly cookies, which JavaScript cannot access.

Conclusion

Persisting React state is safe when the browser is read once on mount and written on each change. Serialize objects as JSON and never store secrets in browser storage.