The useLocalStorageState hook provides persistent state management using localStorage with cross-tab synchronization, server-side rendering support, and a useState-like API. It's designed for user preferences, demo state, and any data that should persist across browser sessions.
setState(prev => prev + 1)const [value, setValue] = useLocalStorageState(key, initializer);
| Parameter | Type | Description |
|---|---|---|
key | string | null | localStorage key. If null, persistence is disabled |
initializer | string | null | (() => string | null) | Initial value or function returning initial value |
| Property | Type | Description |
|---|---|---|
value | string | null | Current value from localStorage or initial value |
setValue | React.Dispatch<React.SetStateAction<string | null>> | Function to update the value |
import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
function ThemeToggle() {
const [theme, setTheme] = useLocalStorageState('theme', () => 'light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return <button onClick={toggleTheme}>Current theme: {theme} (Click to toggle)</button>;
}
import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
function Counter() {
const [count, setCount] = useLocalStorageState('counter', () => '0');
const increment = () => {
setCount((prev) => String(Number(prev || '0') + 1));
};
const decrement = () => {
setCount((prev) => String(Number(prev || '0') - 1));
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
function UserSettings({ enablePersistence }: { enablePersistence: boolean }) {
// When enablePersistence is false, key is null and state isn't persisted
const [settings, setSettings] = useLocalStorageState(
enablePersistence ? 'user-settings' : null,
() => 'default-settings',
);
return (
<div>
<p>Settings: {settings}</p>
<p>Persistence: {enablePersistence ? 'enabled' : 'disabled'}</p>
<button onClick={() => setSettings('custom-settings')}>Update Settings</button>
</div>
);
}
import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
function CodeEditor({ demoId }: { demoId: string }) {
const [code, setCode] = useLocalStorageState(
`demo-code-${demoId}`,
() => `// Default code for ${demoId}\nconsole.log('Hello World');`,
);
return (
<div>
<textarea
value={code || ''}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter your code here..."
rows={10}
cols={50}
/>
<div>
<button onClick={() => setCode(null)}>Reset to Default</button>
<button onClick={() => setCode('')}>Clear</button>
</div>
</div>
);
}
import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
function PreferencesPanel() {
const [language, setLanguage] = useLocalStorageState('ui-language', () => 'en');
const [fontSize, setFontSize] = useLocalStorageState('font-size', () => 'medium');
const [autoSave, setAutoSave] = useLocalStorageState('auto-save', () => 'true');
return (
<div>
<h3>User Preferences</h3>
<label>
Language:
<select value={language || 'en'} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
</label>
<label>
Font Size:
<select value={fontSize || 'medium'} onChange={(e) => setFontSize(e.target.value)}>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</label>
<label>
<input
type="checkbox"
checked={autoSave === 'true'}
onChange={(e) => setAutoSave(e.target.checked ? 'true' : 'false')}
/>
Auto-save
</label>
</div>
);
}
The hook handles SSR by:
[null, () => {}] - no localStorage accessuseSyncExternalStore with server snapshot returning nullThis prevents hydration mismatches while providing immediate localStorage access after hydration.
// Internal event system for same-tab updates
const currentTabChangeListeners = new Map<string, Set<() => void>>();
// Listens to both:
// 1. `storage` events (for other tabs)
// 2. Custom events (for current tab)
function subscribe(area: Storage, key: string | null, callback: () => void) {
const storageHandler = (event: StorageEvent) => {
if (event.storageArea === area && event.key === key) {
callback(); // Other tabs changed this key
}
};
window.addEventListener('storage', storageHandler);
onCurrentTabStorageChange(key, callback); // Same tab changes
// ...
}
Setting Values:
// Supports both direct values and function updates
setValue('new-value');
setValue((prev) => `${prev}-updated`);
// null removes the item and falls back to initial value
setValue(null);
Storage Operations:
setValue(value) → localStorage.setItem(key, value)setValue(null) → localStorage.removeItem(key) + fallback to initialWhen key is null:
useStateconst [value, setValue] = useLocalStorageState(shouldPersist ? 'my-key' : null, () => 'default');
The hook gracefully handles:
All errors are caught and ignored, falling back to non-persistent behavior.
// Hook signature
function useLocalStorageState(
key: string | null,
initializer?: string | null | (() => string | null),
): [string | null, React.Dispatch<React.SetStateAction<string | null>>];
// Example usage
const [value, setValue] = useLocalStorageState('key', () => 'initial');
// ^string | null ^React.Dispatch<React.SetStateAction<string | null>>
usePreference - Higher-level preference management