Learn to build a robust useLocalStorage hook. Master generic storage, JSON serialization, and cross-tab synchronization to keep your React app state persistent.
Previously in this course, we explored Introduction to Custom Hooks to encapsulate and reuse logic across your application. While those lessons focused on abstracting component behavior, today we’re tackling a critical real-world requirement: data persistence.
By default, React state lives in memory. Refresh the page, and it’s gone. To build a production-grade dashboard, we need to bridge the gap between volatile component state and the browser's persistent storage. We’ll build a useLocalStorage hook that handles serialization, provides a clean API, and ensures our UI stays consistent even if the user opens multiple tabs.
The goal is to create a hook that mimics the useState API but automatically syncs the value to localStorage. We need to handle three core challenges:
localStorage only stores strings; we need to store objects and arrays.localStorage on every single render.We’ll start by creating useLocalStorage.js. This hook will take a key and an initialValue.
JAVASCRIPTimport { useState, useEffect } from CE9178">'react'; export function useLocalStorage(key, initialValue) { // 1. Get initial value from storage or fallback to initialValue const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); // 2. Update storage whenever state changes useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(storedValue)); } catch (error) { console.error(error); } }, [key, storedValue]); return [storedValue, setStoredValue]; }
The basic implementation above works for a single tab, but it fails if the user has your dashboard open in two different windows. When localStorage changes in one tab, the other tabs don't automatically trigger a re-render.
We can solve this by listening to the storage event, which fires globally across the same origin.
JAVASCRIPTuseEffect(() => { const handleStorageChange = (e) => { if (e.key === key && e.newValue !== null) { setStoredValue(JSON.parse(e.newValue)); } }; window.addEventListener(CE9178">'storage', handleStorageChange); return () => window.removeEventListener(CE9178">'storage', handleStorageChange); }, [key]);
In our running project, let's use this hook to persist the user's dashboard theme preference.
JSXimport { useLocalStorage } from CE9178">'./hooks/useLocalStorage'; function DashboardSettings() { const [theme, setTheme] = useLocalStorage(CE9178">'dashboard-theme', CE9178">'light'); return ( <div> <p>Current theme: {theme}</p> <button onClick={() => setTheme(theme === CE9178">'light' ? CE9178">'dark' : CE9178">'light')}> Toggle Theme </button> </div> ); }
By using useLocalStorage here, the theme state survives page refreshes and stays in sync if the user toggles it in a different dashboard view. This is a massive improvement over standard useState for user preferences.
useLocalStorage hook file as shown above.null or an invalid JSON string in the browser DevTools Application tab and ensure your hook catches the error gracefully.JSON.parse in a try/catch block. If a user manually edits their localStorage to an invalid value, your entire app could crash on mount.localStorage for high-frequency state updates (like tracking mouse coordinates). It involves synchronous disk I/O, which can cause jank. Reserve it for configuration or form data.window will be undefined on the server. Always check if (typeof window !== 'undefined') or only access storage inside useEffect.We’ve moved beyond simple state management by bridging memory with persistent browser storage. We learned how to:
storage event.This pattern is a foundational piece of our dashboard project, ensuring that user preferences and cached UI states are preserved across sessions.
Up next: We'll dive into Complex State with useReducer, where we'll learn how to handle more intricate state transitions that go beyond simple key-value pairs.
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.
Read moreLearn how to use the Context API and useContext to share data across your React application, effectively eliminating prop drilling for cleaner code.
Building a useLocalStorage Hook
Structuring State for Performance
Handling Authentication State
Integrating Reducers with Auth State
Introduction to React Router
Dynamic Routing with URL Parameters
Nested Routes and Layouts
Protected Routes for Authenticated Views
Programmatic Navigation
Building the Dashboard Navigation Structure
Asynchronous Data Lifecycle
Caching Strategies with React Query
Mutations and Data Updates
Synchronizing Client and Server State
Integrating Live Data into the Dashboard
Error Handling and Loading UI
Controlled vs Uncontrolled Components
Real-time Form Validation
Schema-based Validation with Zod
Handling Multi-step Forms
Optimizing Form Submissions
Performance Profiling with React DevTools
Refactoring for Scalability
Finalizing Dashboard Data Flow
Deploying the Application
Advanced Hook Composition
Implementing Middleware for State
Advanced Context Patterns
Router Loaders and Data Prefetching
Complex Route Guards
Handling Large Datasets in UI
Testing Hooks and Components
Managing Global Modals
Implementing Keyboard Shortcuts
Optimizing Asset Loading
Internationalization Basics
Managing WebSocket Connections