Working with LocalStorage: Persisting React State Across Reloads
Learn to use localStorage to persist your React app's state across browser refreshes. Master the art of syncing local data with your component state.
Previously in this course, we covered handling browser history. While that lesson focused on navigation, today we are tackling the "memory" of your application: how to keep data alive even after a user refreshes the page.
React state is volatile. Every time a user refreshes their browser, your application re-initializes, and your useState values reset to their defaults. If you want to remember a user's theme preference, a draft they were typing, or a list of items they interacted with, you need a way to store that data outside of React's memory. The browser's localStorage API is the standard tool for this persistence.
Understanding the localStorage API
localStorage is a simple key-value store built into every modern browser. It is synchronous, meaning it blocks the main thread while reading or writing, so we must use it carefully. Because it only stores strings, we have to serialize our data (using JSON.stringify) when saving it and deserialize it (using JSON.parse) when reading it back.
The primary methods you need are:
localStorage.setItem('key', 'value'): Stores data.localStorage.getItem('key'): Retrieves data.localStorage.removeItem('key'): Deletes a specific item.
Syncing State with localStorage
To make localStorage work with React, we need to bridge the gap between the browser's storage and our component's state. We do this by initializing state from storage and using useEffect to save changes whenever that state updates.
Consider a "Dark Mode" toggle for our movie browser. We want the user's choice to persist even if they close and reopen the tab.
JSXimport { useState, useEffect } from CE9178">'react'; function ThemeToggle() { // 1. Initialize state from localStorage const [isDarkMode, setIsDarkMode] = useState(() => { const savedTheme = localStorage.getItem(CE9178">'isDarkMode'); return savedTheme ? JSON.parse(savedTheme) : false; }); // 2. Sync state changes to localStorage useEffect(() => { localStorage.setItem(CE9178">'isDarkMode', JSON.stringify(isDarkMode)); }, [isDarkMode]); return ( <button onClick={() => setIsDarkMode(!isDarkMode)}> Switch to {isDarkMode ? CE9178">'Light' : CE9178">'Dark'} Mode </button> ); }
In this example, notice the "lazy initializer" function inside useState. By passing a function instead of a raw value, we ensure that localStorage.getItem only runs once during the initial mount, rather than on every single re-render. This is a crucial performance optimization for managing state.
Hands-on Exercise: Persisting User Preferences
Let's apply this to our movie project. In your existing movie browser, find the component that handles the search input.
- Create a
localStoragekey called'movie-search-query'. - Update your
useStatehook for the search term to initialize from that key. - Add a
useEffecthook that saves the search term tolocalStoragewhenever the user types in the input. - Verify the persistence by typing a search term, refreshing the page, and confirming the input still contains your text.
Common Pitfalls
Even for experienced engineers, localStorage has a few traps:
- Blocking the Main Thread: Since
localStorageis synchronous, don't use it for massive amounts of data (like a 50MB JSON blob). It will make your app feel stuttery. - The "Undefined" Trap: If
localStorage.getItemreturnsnull(because the key doesn't exist yet),JSON.parse(null)will returnnull. Always provide a fallback default value (e.g.,savedTheme ? JSON.parse(savedTheme) : false). - Security: Never store sensitive information like passwords or auth tokens in
localStorage. It is accessible by any script running on your page, making it vulnerable to Cross-Site Scripting (XSS) attacks. - React Reconciliation: Remember that updating state triggers React reconciliation, but
localStorageis completely external to this process. Always keep your React state as the "source of truth."
Recap
Persistence is the final piece of the puzzle for a functional, user-friendly UI. We’ve learned that:
localStorageis the browser's built-in key-value storage.- We must always
JSON.stringifyandJSON.parsedata since storage only accepts strings. - Using a lazy initializer function in
useStateprevents expensive storage lookups on every render. useEffectis the perfect place to synchronize our React state with the browser's persistent storage.
By mastering this, you’ve ensured your users don't lose their progress when they navigate away or refresh. You're building apps that feel robust and thoughtful.
Up next: We will build a Favorites List, where we will use these persistence concepts to manage arrays of data.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.