Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
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