Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
Use Local Storage State
https://github.com/astoilkov/use-local-storage-state
Admin
A React hook that persists component state to localStorage with SSR support, automatic sync across
...
Tokens:
4,013
Snippets:
32
Trust Score:
9.7
Update:
1 week ago
Context
Skills
Chat
Benchmark
93.2
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# use-local-storage-state `use-local-storage-state` is a lightweight React hook that provides persistent state management using the browser's localStorage API. It offers a drop-in replacement for React's `useState` with automatic persistence, SSR support, and synchronization across browser tabs. The library handles edge cases gracefully including localStorage quota errors, private browsing mode restrictions, and invalid stored data. The hook weighs just 689 bytes (brotli compressed) and is production-ready with support for React 18+ concurrent rendering and React 19. It provides an in-memory fallback when localStorage is unavailable, cross-tab synchronization via the Window `storage` event, and TypeScript support out of the box. The API design mirrors `useState` to minimize learning curve while extending it with additional utilities like `removeItem` and `isPersistent` for complete control over persisted state. ## Basic Usage The `useLocalStorageState` hook works like `useState` but persists data to localStorage. It accepts a key string and an options object with a default value. ```typescript import useLocalStorageState from 'use-local-storage-state' export default function Todos() { const [todos, setTodos] = useLocalStorageState('todos', { defaultValue: ['buy avocado', 'do 50 push-ups'] }) const addTodo = (newTodo: string) => { setTodos([...todos, newTodo]) } const clearTodos = () => { setTodos([]) } return ( <div> <ul> {todos.map((todo, index) => ( <li key={index}>{todo}</li> ))} </ul> <button onClick={() => addTodo('new task')}>Add</button> <button onClick={clearTodos}>Clear</button> </div> ) } ``` ## Updating State with Callback Function Like `useState`, the setter function accepts either a new value or a callback function that receives the current value and returns the new value. This is useful for updates based on the previous state. ```typescript import useLocalStorageState from 'use-local-storage-state' export default function Counter() { const [count, setCount] = useLocalStorageState('counter', { defaultValue: 0 }) const increment = () => { setCount((prevCount) => prevCount + 1) } const decrement = () => { setCount((prevCount) => prevCount - 1) } const reset = () => { setCount(0) } return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>Reset</button> </div> ) } ``` ## Lazy Default Value Initialization The `defaultValue` option accepts a lazy initializer function (like `useState`). This is useful when computing the default value is expensive, as it only runs once on initial mount. ```typescript import useLocalStorageState from 'use-local-storage-state' export default function ExpensiveComponent() { const [data, setData] = useLocalStorageState('expensive-data', { defaultValue: () => { // This expensive computation only runs if no stored value exists console.log('Computing expensive default...') return Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.random() })) } }) return <div>Items: {data.length}</div> } ``` ## Using removeItem to Reset State The hook returns a third value containing `removeItem()` and `isPersistent`. The `removeItem()` function clears the localStorage entry and resets the state to its default value. ```typescript import useLocalStorageState from 'use-local-storage-state' export default function UserPreferences() { const [preferences, setPreferences, { removeItem }] = useLocalStorageState('user-prefs', { defaultValue: { theme: 'light', fontSize: 16, notifications: true } }) const updateTheme = (theme: string) => { setPreferences({ ...preferences, theme }) } const resetToDefaults = () => { removeItem() // Clears localStorage and resets to defaultValue } return ( <div> <p>Theme: {preferences.theme}</p> <button onClick={() => updateTheme('dark')}>Dark Mode</button> <button onClick={() => updateTheme('light')}>Light Mode</button> <button onClick={resetToDefaults}>Reset All Settings</button> </div> ) } ``` ## Checking Persistence Status with isPersistent The `isPersistent` property indicates whether data is being stored in localStorage or only in memory (fallback). This is useful for notifying users when their data won't persist (e.g., in private browsing mode or when storage quota is exceeded). ```typescript import useLocalStorageState from 'use-local-storage-state' export default function PersistentForm() { const [formData, setFormData, { isPersistent }] = useLocalStorageState('draft-form', { defaultValue: { name: '', email: '', message: '' } }) const updateField = (field: string, value: string) => { setFormData({ ...formData, [field]: value }) } return ( <form> {!isPersistent && ( <div className="warning"> ⚠️ Your changes won't be saved between sessions. Please enable cookies/localStorage. </div> )} <input value={formData.name} onChange={(e) => updateField('name', e.target.value)} placeholder="Name" /> <input value={formData.email} onChange={(e) => updateField('email', e.target.value)} placeholder="Email" /> <textarea value={formData.message} onChange={(e) => updateField('message', e.target.value)} placeholder="Message" /> </form> ) } ``` ## Disabling Cross-Tab Synchronization By default, state changes sync across browser tabs via the Window `storage` event. Set `storageSync: false` to disable this behavior when cross-tab synchronization is not needed. ```typescript import useLocalStorageState from 'use-local-storage-state' export default function IsolatedCounter() { // Each tab maintains its own state, no sync between tabs const [count, setCount] = useLocalStorageState('isolated-counter', { defaultValue: 0, storageSync: false }) return ( <div> <p>This tab's count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ) } ``` ## Server-Side Rendering with defaultServerValue For SSR frameworks like Next.js, use `defaultServerValue` to provide a different initial value during server rendering and hydration. This prevents hydration mismatches when the localStorage value differs from the server-rendered value. ```typescript import useLocalStorageState from 'use-local-storage-state' export default function SSRComponent() { const [theme, setTheme] = useLocalStorageState('theme', { defaultValue: 'light', defaultServerValue: 'light' // Always render 'light' on server }) return ( <div className={`app ${theme}`}> <p>Current theme: {theme}</p> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> </div> ) } ``` ## Custom Serializer with superjson The default JSON serializer cannot handle special types like `Date`, `RegExp`, or `BigInt`. Use a custom serializer like `superjson` to preserve these types when storing and retrieving data. ```typescript import useLocalStorageState from 'use-local-storage-state' import superjson from 'superjson' interface Task { id: number title: string createdAt: Date dueDate: Date | null } export default function TaskManager() { const [tasks, setTasks] = useLocalStorageState<Task[]>('tasks', { defaultValue: [], serializer: superjson // Preserves Date objects correctly }) const addTask = (title: string) => { const newTask: Task = { id: Date.now(), title, createdAt: new Date(), // Date object will be preserved dueDate: null } setTasks([...tasks, newTask]) } return ( <div> {tasks.map((task) => ( <div key={task.id}> <span>{task.title}</span> <small>Created: {task.createdAt.toLocaleDateString()}</small> </div> ))} <button onClick={() => addTask('New Task')}>Add Task</button> </div> ) } ``` ## TypeScript Generic Types The hook supports TypeScript generics for type-safe state management. You can explicitly define the type when the state can be `undefined` or `null`, or when using complex types. ```typescript import useLocalStorageState from 'use-local-storage-state' interface User { id: number name: string email: string } export default function UserProfile() { // Explicit type for nullable state const [user, setUser] = useLocalStorageState<User | null>('current-user', { defaultValue: null }) // Type-safe state with undefined const [preferences, setPreferences] = useLocalStorageState<string[] | undefined>('prefs', { defaultValue: undefined }) const login = (userData: User) => { setUser(userData) } const logout = () => { setUser(null) } return ( <div> {user ? ( <> <p>Welcome, {user.name}!</p> <button onClick={logout}>Logout</button> </> ) : ( <button onClick={() => login({ id: 1, name: 'John', email: 'john@example.com' })}> Login </button> )} </div> ) } ``` ## Handling Multiple Components with Same Key Multiple components using the same localStorage key will automatically stay synchronized. When one component updates the value, all other components with the same key will receive the updated value immediately. ```typescript import useLocalStorageState from 'use-local-storage-state' function DisplayComponent() { const [count] = useLocalStorageState('shared-count', { defaultValue: 0 }) return <div>Current Count: {count}</div> } function ControlComponent() { const [count, setCount] = useLocalStorageState('shared-count', { defaultValue: 0 }) return ( <div> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(count - 1)}>Decrement</button> </div> ) } export default function App() { // Both components share the same 'shared-count' key // and stay synchronized automatically return ( <div> <DisplayComponent /> <DisplayComponent /> <ControlComponent /> </div> ) } ``` ## Summary `use-local-storage-state` is ideal for persisting user preferences (themes, language settings, UI configurations), form drafts, shopping cart contents, authentication tokens, and any client-side state that should survive page refreshes. The hook seamlessly handles edge cases like private browsing mode, storage quota limits, and corrupted localStorage data, making it suitable for production applications without additional error handling code. Integration is straightforward—simply replace `useState` with `useLocalStorageState` and provide a unique key. For SSR frameworks, add `defaultServerValue` to prevent hydration mismatches. For applications requiring complex data types like dates or BigInts, integrate `superjson` or a similar serializer. The cross-tab synchronization feature makes it excellent for applications where users may have multiple tabs open, ensuring consistent state across all instances.