# 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 (
{todos.map((todo, index) => (
{todo}
))}
)
}
```
## 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 (
Count: {count}
)
}
```
## 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
Items: {data.length}
}
```
## 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 (
Theme: {preferences.theme}
)
}
```
## 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 (
)
}
```
## 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 (
This tab's count: {count}
)
}
```
## 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 (
Current theme: {theme}
)
}
```
## 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('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 (
)
}
```
## 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('current-user', {
defaultValue: null
})
// Type-safe state with undefined
const [preferences, setPreferences] = useLocalStorageState('prefs', {
defaultValue: undefined
})
const login = (userData: User) => {
setUser(userData)
}
const logout = () => {
setUser(null)
}
return (
{user ? (
<>
Welcome, {user.name}!
>
) : (
)}
)
}
```
## 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
)
}
export default function App() {
// Both components share the same 'shared-count' key
// and stay synchronized automatically
return (
)
}
```
## 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.