← Back to React Course | Chapter 6: Custom Hooks & Advanced Patterns | Lesson 3 of 10

Building a useLocalStorage Hook

A useLocalStorage hook is like a notebook that remembers what you wrote even after you close the browser tab and come back later.

Why Sync State with localStorage

Normal React state resets every time the page reloads, since it only lives in memory. localStorage persists data in the browser even after the tab is closed. A useLocalStorage hook combines the two, so a value behaves like normal state but also survives a refresh.

Note: Good use cases are theme preference, a saved draft, or 'remember me' style settings — anything you'd want to survive a reload.

Warning: Don't store sensitive data (like tokens or passwords) in localStorage — it's readable by any JavaScript running on the page, including malicious scripts.

Example: Why Sync State with localStorage

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function useLocalStorage(key, initialValue) {
  const [value, setValue] = React.useState(() => {
    const saved = localStorage.getItem(key);
    return saved !== null ? JSON.parse(saved) : initialValue;
  });
  React.useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue];
}
function App() {
  const [name, setName] = useLocalStorage("name", "Guest");
  return <input value={name} onChange={e => setName(e.target.value)} />;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Reading the Saved Value on First Render

useState accepts a function as its initial value (called a lazy initializer), which only runs once on the first render. This is the right place to check localStorage for a previously saved value, since checking it directly in the function body would run on every render.

Note: Always pass a function to useState (() => ...) rather than calling localStorage.getItem() directly as the argument, to avoid re-reading storage on every render.

Warning: If the stored JSON is malformed (e.g. edited manually in devtools), JSON.parse will throw — wrap it in a try/catch to fall back to the initial value.

Example: Reading the Saved Value on First Render

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function useLocalStorage(key, initialValue) {
  const [value] = React.useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved !== null ? JSON.parse(saved) : initialValue;
    } catch { return initialValue; }
  });
  return value;
}
function App() {
  const count = useLocalStorage("count", 0);
  return <p>Loaded count: {count}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Writing Back on Every Change

A useEffect that watches the state value writes it back to localStorage every time it changes, using JSON.stringify since localStorage can only store strings. This keeps the saved copy always in sync with the current React state.

Note: Include both the key and the value in the dependency array so the hook stays correct even if the key itself is dynamic.

Warning: Writing to localStorage on every keystroke of a fast-changing input can be wasteful — consider debouncing the write for large or frequently-changing values.

Example: Writing Back on Every Change

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function useLocalStorage(key, initialValue) {
  const [value, setValue] = React.useState(initialValue);
  React.useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]);
  return [value, setValue];
}
function App() {
  const [notes, setNotes] = useLocalStorage("notes", "");
  return (
    <div>
      <textarea value={notes} onChange={e => setNotes(e.target.value)} />
      <p>Saved: {notes.length} characters</p>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Forgetting that localStorage only stores strings, so objects/arrays must be JSON.stringify'd before saving and JSON.parse'd when reading back.
  2. Not wrapping localStorage access in a try/catch, which can throw in private browsing mode or when storage is full.
  3. Using the same localStorage key for two different pieces of state, causing them to silently overwrite each other.
Chapter Summary
  • useLocalStorage is a custom hook that syncs a piece of React state with the browser's localStorage.
  • It reads the saved value on initial render and writes back to localStorage whenever the state changes.
  • JSON.stringify/JSON.parse are needed because localStorage only stores strings.
  • This pattern is commonly used for theme preferences, form drafts, and small settings that should survive a page refresh.
Browser Support

Available since React 16.8 (Hooks introduction); localStorage itself is supported in all modern browsers (IE8+).

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.