useSuspenseLocalStorageState
About
⚠️ Experimental Hook: This hook may be removed or significantly changed in any release without notice.
A Suspense-enabled hook for localStorage state management with cross-tab synchronization. This hook suspends during initialization and must be wrapped in a Suspense boundary. It provides full TypeScript generic support, proper JSON serialization/deserialization, error handling, and automatic synchronization across browser tabs.
Examples
Basic Usage with Numbers
import React, { Suspense } from "react";
import { useSuspenseLocalStorageState } from "rooks/experimental";
function Counter() {
const [count, { setItem, deleteItem }] = useSuspenseLocalStorageState(
'counter',
(currentValue) => currentValue ? parseInt(currentValue, 10) : 0
);
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => setItem(count + 1)}>
Increment
</button>
<button onClick={() => setItem(count - 1)}>
Decrement
</button>
<button onClick={deleteItem}>
Reset
</button>
</div>
);
}
export default function App() {
return (
<Suspense fallback={<div>Loading counter...</div>}>
<Counter />
</Suspense>
);
}Working with Objects and TypeScript
import React, { Suspense } from "react";
import { useSuspenseLocalStorageState } from "rooks/experimental";
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
const [user, { setItem, getItem, deleteItem }] = useSuspenseLocalStorageState(
'user-profile',
(currentValue): User => {
if (currentValue) {
try {
return JSON.parse(currentValue);
} catch {
return { id: 0, name: 'Guest', email: '' };
}
}
return { id: 0, name: 'Guest', email: '' };
}
);
const updateUser = () => {
setItem({
id: 123,
name: 'John Doe',
email: 'john@example.com'
});
};
const getCurrentUser = () => {
const current = getItem();
alert(`Current user: ${current.name} (${current.email})`);
};
return (
<div>
<h2>User Profile</h2>
<p>ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<button onClick={updateUser}>
Update User
</button>
<button onClick={getCurrentUser}>
Show Current User
</button>
<button onClick={deleteItem}>
Clear Profile
</button>
</div>
);
}
export default function App() {
return (
<Suspense fallback={<div>Loading user profile...</div>}>
<UserProfile />
</Suspense>
);
}Cross-tab Synchronization Demo
import React, { Suspense } from "react";
import { useSuspenseLocalStorageState } from "rooks/experimental";
function SharedCounter() {
const [count, { setItem, deleteItem }] = useSuspenseLocalStorageState(
'shared-counter',
(currentValue) => currentValue ? parseInt(currentValue, 10) : 0
);
return (
<div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h3>Shared Counter</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>
Count: {count}
</p>
<div style={{ gap: '10px', display: 'flex' }}>
<button onClick={() => setItem(count + 1)}>
+1
</button>
<button onClick={() => setItem(count + 10)}>
+10
</button>
<button onClick={deleteItem}>
Reset
</button>
</div>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
Open this page in multiple tabs to see real-time synchronization!
</p>
</div>
);
}
export default function App() {
return (
<Suspense fallback={<div>Initializing shared state...</div>}>
<SharedCounter />
</Suspense>
);
}Complex State with Arrays
import React, { Suspense } from "react";
import { useSuspenseLocalStorageState } from "rooks/experimental";
function TodoList() {
const [todos, { setItem, deleteItem }] = useSuspenseLocalStorageState(
'todo-list',
(currentValue) => {
if (currentValue) {
try {
return JSON.parse(currentValue);
} catch {
return [];
}
}
return [];
}
);
const addTodo = () => {
const text = prompt('Enter todo:');
if (text) {
setItem([
...todos,
{
id: Date.now(),
text,
completed: false
}
]);
}
};
const toggleTodo = (id) => {
setItem(
todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
const deleteTodo = (id) => {
setItem(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h2>Todo List ({todos.length} items)</h2>
<button onClick={addTodo} style={{ marginBottom: '20px' }}>
Add Todo
</button>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
style={{
padding: '10px',
margin: '5px 0',
border: '1px solid #ddd',
borderRadius: '4px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<span
onClick={() => toggleTodo(todo.id)}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer',
flex: 1
}}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
{todos.length > 0 && (
<button onClick={deleteItem} style={{ marginTop: '20px' }}>
Clear All Todos
</button>
)}
</div>
);
}
export default function App() {
return (
<Suspense fallback={<div>Loading todos...</div>}>
<TodoList />
</Suspense>
);
}With Error Boundary for Production Use
import React, { Suspense } from "react";
import { useSuspenseLocalStorageState } from "rooks/experimental";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<p>Failed to initialize localStorage state.</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
function Settings() {
const [settings, { setItem, deleteItem }] = useSuspenseLocalStorageState(
'app-settings',
(currentValue) => {
const defaults = {
theme: 'light',
notifications: true,
autoSave: false
};
if (currentValue) {
try {
return { ...defaults, ...JSON.parse(currentValue) };
} catch {
return defaults;
}
}
return defaults;
}
);
const updateSetting = (key, value) => {
setItem({
...settings,
[key]: value
});
};
return (
<div>
<h2>Application Settings</h2>
<div style={{ display: 'grid', gap: '15px', maxWidth: '300px' }}>
<label>
Theme:
<select
value={settings.theme}
onChange={(e) => updateSetting('theme', e.target.value)}
style={{ marginLeft: '10px' }}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) => updateSetting('notifications', e.target.checked)}
/>
Enable Notifications
</label>
<label>
<input
type="checkbox"
checked={settings.autoSave}
onChange={(e) => updateSetting('autoSave', e.target.checked)}
/>
Auto Save
</label>
</div>
<button onClick={deleteItem} style={{ marginTop: '20px' }}>
Reset to Defaults
</button>
</div>
);
}
export default function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading settings...</div>}>
<Settings />
</Suspense>
</ErrorBoundary>
);
}Cross-tab Synchronization
The hook automatically synchronizes state across browser tabs and windows through:
- Storage Events: Listens for
storageevents fired when localStorage is modified in other tabs - Custom Events: Uses custom document events to synchronize state changes within the same tab across multiple hook instances
- Real-time Updates: Components automatically re-render when localStorage changes occur from any source
This means if you have the same hook with the same key in multiple tabs or multiple components, they will all stay in sync automatically.
Arguments
| Parameter | Type | Description |
|---|---|---|
key | string | The localStorage key to use for storing the data |
initializer | (currentValue: string | null) => T | Function that transforms the raw localStorage value (string or null) into type T |
Initializer Function
The initializer function receives the raw string value from localStorage (or null if not present) and should return the properly typed initial state. This allows for:
- Type Conversion: Convert strings to numbers, parse JSON objects, etc.
- Default Values: Provide fallbacks when localStorage is empty
- Data Validation: Validate and sanitize stored data
- Migration: Handle changes in data structure over time
Return Value
Returns a tuple [value, controls] where:
| Property | Type | Description |
|---|---|---|
value | T | The current state value |
controls | object | Object containing control methods |
Control Methods
The controls object provides the following methods:
| Method | Type | Description |
|---|---|---|
getItem | () => T | Returns the current state value |
setItem | (value: T) => void | Updates the state and localStorage with the new value |
deleteItem | () => void | Removes the item from localStorage and resets to initial value |
Type Safety
The hook provides full TypeScript generic support:
// Number type
const [count, controls] = useSuspenseLocalStorageState<number>(
'count',
(value) => value ? parseInt(value, 10) : 0
);
// controls.setItem expects number
controls.setItem(42); // ✅ Valid
controls.setItem('42'); // ❌ Type error
// Object type
interface User {
name: string;
age: number;
}
const [user, userControls] = useSuspenseLocalStorageState<User>(
'user',
(value) => value ? JSON.parse(value) : { name: '', age: 0 }
);
// userControls.setItem expects User object
userControls.setItem({ name: 'John', age: 30 }); // ✅ Valid
userControls.setItem({ name: 'John' }); // ❌ Type error (missing age)Error Handling
The hook handles various error scenarios gracefully:
- localStorage Unavailable: Works in SSR environments or when localStorage is disabled
- Storage Quota Exceeded: Continues to work even if localStorage writes fail
- JSON Parse Errors: Lets the initializer handle malformed data
- Initializer Errors: Throws errors to be caught by error boundaries
Always wrap in error boundaries for production use.
Browser Support
- Modern Browsers: ✅ Full support
- Server-Side Rendering: ✅ Works (uses initializer default when localStorage unavailable)
- Storage Disabled: ✅ Graceful fallback (state works, but won't persist)
Import
import { useSuspenseLocalStorageState } from "rooks/experimental";