Back to Blog
Lesson 42 of the React Fundamentals: Build Modern UIs from Scratch course
ReactJune 25, 20263 min read

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.

ReactlocalStorageWeb APIsHooksPersistenceFrontend Developmentjavascriptfrontend

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.

JSX
import { 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.

  1. Create a localStorage key called 'movie-search-query'.
  2. Update your useState hook for the search term to initialize from that key.
  3. Add a useEffect hook that saves the search term to localStorage whenever the user types in the input.
  4. 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 localStorage is 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.getItem returns null (because the key doesn't exist yet), JSON.parse(null) will return null. 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 localStorage is 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:

  1. localStorage is the browser's built-in key-value storage.
  2. We must always JSON.stringify and JSON.parse data since storage only accepts strings.
  3. Using a lazy initializer function in useState prevents expensive storage lookups on every render.
  4. useEffect is 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.

Similar Posts