Persist React State Like a Pro: Building a Reusable useLocalState Hook
In React, state is ephemeral by default. Refresh the page, and everything you’ve carefully built — a form, a theme toggle, a shopping cart — resets to square one. That works fine for some cases, but often users expect their choices to stick around.
Fortunately, the browser provides localStorage, a simple key-value store that persists data across sessions, even after the tab is closed or the page is reloaded. By combining localStorage with React hooks, we can build a custom solution that handles persistence automatically.
In this post, we’ll create a reusable useLocalState hook. It’s tiny, it’s safe, and it feels exactly like useState. Let’s get started.
Why not just use useState?
React’s built-in useState is perfect for temporary state that resets on reload:
const [count, setCount] = useState(0)But after refreshing the browser, count is back to 0. If you want state that survives navigation, reloads, or even closing and reopening the browser, you need something more permanent. That’s where localStorage comes in.
Step 1: Setting up the Hook
Here’s the complete custom hook:
import { useState, useEffect } from "react"
function useLocalState(key, defaultValue) {
const [state, setState] = useState(() => {
try {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : defaultValue
} catch {
return defaultValue
}
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state))
}, [key, state])
return [state, setState]
}
export default useLocalStateHow it Works
- Initialization with lazy state — The
useStateinitializer runs only once. It checks if a value exists in localStorage under the given key. If found, it parses and returns it; otherwise, it falls back to the provided default. - Sync with useEffect — Every time the state changes, the effect saves the new value to localStorage as a JSON string. This keeps storage in sync without extra work.
- Familiar return value — The hook returns
[state, setState], exactly likeuseState. You can use it anywhere without changing your component logic.
Handling Errors Gracefully
The try-catch block inside the lazy initializer ensures that corrupted or unparseable data doesn’t crash your app. If something goes wrong (e.g., localStorage is full or the stored data is invalid), it simply falls back to the default value. This is a small but important safety net.
Using the Hook in a Component
Here’s a simple counter example:
import React from "react"
import useLocalState from "./useLocalState"
function Counter() {
const [count, setCount] = useLocalState("count", 0)
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(c => c + 1)}>Increase</button>
<button onClick={() => setCount(c => c - 1)}>Decrease</button>
</div>
)
}
export default CounterNow, refresh the page as many times as you like — the counter stays exactly where you left it. No more resetting.
Real-World Use Cases
The useLocalState pattern works well whenever you need lightweight persistence without a backend:
- Dark mode toggle — Save the user’s theme preference so it persists across sessions.
- Multi-step forms — Keep form inputs filled even if the user accidentally reloads.
- Shopping cart state — Preserve cart items until the user checks out (useful for single-page apps).
- Content drafts — Autosave unsent comments, notes, or blog posts.
When to Use Something Else
localStorage is synchronous and limited to about 5–10 MB per origin. For large datasets, sensitive data, or cross-tab synchronization, consider alternatives like IndexedDB, sessionStorage, or a state management library with persistence middleware.
But for most UI preferences and small bits of user data, this hook is simple, effective, and ready to use in minutes.
Comments
Related posts
Deploy Like a Developer: Zero-Config Deployment from Your Terminal with Vercel CLI
After perfecting your project, a fast zero-config way to deploy it online is using Vercel: sign up, install the Vercel CLI globally, authenticate via `vercel login`, then deploy to production with `vercel --prod`.
Jun 13, 2026 · 3 min read
The Hidden Power of useEffect's Return Function
React's useEffect cleanup function is essential for preventing memory leaks and unexpected behavior by properly disposing of timers, event listeners, and subscriptions when components unmount or dependencies change.
Jun 13, 2026 · 3 min read
Implement a Manual Dark Mode Toggle in Your Tailwind CSS Site with a Custom React Hook
To give your website visitors control over their viewing experience, you can easily implement a manual dark mode toggle in Tailwind CSS by switching to a class-based approach and using a custom React hook that reads from localStorage. This lightweight solution lets users toggle between light and dark themes with a single button click while remembering their preference across sessions.
Jun 13, 2026 · 3 min read