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.
| localStorage | sessionStorage | |
|---|---|---|
| Lifetime | Until removed | Until the tab closes |
| Shared across tabs | Yes | No |
| Good for | Theme, saved draft | One 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.
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.
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.
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.
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.
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
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.
Frequently Asked Questions
What is the difference between localStorage and sessionStorage?
Can localStorage store objects?
Is localStorage safe for tokens?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.