A zero-dependency React hook for sharing state across components with optional localStorage persistence and cross-tab sync
npm install @stackoverprof/use-shared-state


A lightweight React hook for sharing state across components with optional localStorage persistence and cross-tab synchronization.
See real-time state sharing, persistence, and cross-tab synchronization in action!
- ๐ Simple API - Drop-in replacement for useState with cross-component sharing
- ๐พ Optional Persistence - Use @ prefix for localStorage persistence
- ๐ Cross-tab Sync - Automatic synchronization across browser tabs
- โก High Performance - Optimized with minimal overhead using Map storage
- ๐ก๏ธ Type Safe - Full TypeScript support with generics
- ๐ฏ Lite SWR - Built with custom lightweight SWR implementation (~100 lines)
- ๐งช Zero Dependencies - No external dependencies except React
``bash`Install the library
npm install @stackoverprof/use-shared-state
> Note: React >=16.8.0 is required (peer dependency)
`tsx
import useSharedState from "@stackoverprof/use-shared-state";
// Basic shared state (memory only)
const [count, setCount] = useSharedState("counter", 0);
// Persistent shared state (localStorage + cross-tab sync)
const [user, setUser] = useSharedState("@user", { name: "John" });
// โณ Saved in localStorage as "shared@user"
`
Returns a tuple [state, setState] similar to React's useState.
#### Parameters
- key - Unique identifier for the shared state@
- Regular keys: Memory-only storage
- Keys with prefix: Persistent localStorage + cross-tab syncinitialValue
- - Default value when state is undefined
#### Returns
- state - Current state value (T | undefined)setState
- - Function to update state, supports value or updater function
- Memory-only keys: ~0.1ms overhead
- Persistent keys: ~2-3ms overhead (includes localStorage operations)
- Cross-tab sync: Automatic with StorageEvent API
- Memory usage: Efficient Map-based storage with automatic cleanup
- Re-renders: Only components using the changed state key re-render
Important: Only components that actively use a shared state key will re-render when that state changes.
โ
Precise targeting: Only components using the changed key re-render
โ
Parent isolation: Parent won't re-render unless it uses shared state
โ
Sibling isolation: Unrelated siblings won't re-render
โ
Performance: Better than Context (which can cause cascade re-renders)
| Feature | use-shared-state | Redux | Context | localStorage |
| -------------------- | ---------------- | ------ | ------- | ------------ |
| Setup complexity | Minimal | High | Medium | Manual |
| TypeScript support | Full | Good | Good | Manual |
| Cross-component sync | โ
| โ
| โ
| โ |
| Persistence | Optional | Manual | โ | Manual |
| Cross-tab sync | โ
| Manual | โ | Manual |
| Performance | High | Medium | Low\* | High |
| Bundle size | Small | Large | None | None |
\*Context can cause unnecessary re-renders
1. Use regular keys for temporary state
`tsx`
const [loading, setLoading] = useSharedState("loading", false);
2. Use @ prefix for data that should persist
`tsx`
const [settings, setSettings] = useSharedState("@user-settings", {});
3. Provide default values for better TypeScript inference
`tsx`
const [items, setItems] = useSharedState
4. Use updater functions for complex state changes
`tsx`
setCart((prev) => ({ ...prev, total: calculateTotal(prev.items) }));
- โ
Lite SWR reference counting - Cleans up when ALL components using a key unmount
- โ
Event listeners removed - Cross-tab sync listeners auto-cleanup
- โ
Memory efficient - Map-based storage with garbage collection
| Type | Lite SWR Cleanup | localStorage Cleanup |
| -------------- | --------------------- | ---------------------------- |
| "user-data" | โ
Auto (memory only) | โ N/A |"@user-data"
| | โ
Memory cache only | โ Stays until manual delete |
`tsx
import { sharedStateUtils } from "@stackoverprof/use-shared-state";
// Clear specific keys
sharedStateUtils.delete("temp-data"); // Memory only
sharedStateUtils.delete("@user-session"); // Memory + localStorage
// Clear all (with/without persistent)
sharedStateUtils.clear(false); // Memory only
sharedStateUtils.clear(true); // Memory + localStorage
// Route cleanup
useEffect(
() => () => {
sharedStateUtils.delete("dashboard-filters");
},
[]
);
`
The library provides debugging utilities via sharedStateUtils:
`tsx
import { sharedStateUtils } from "@stackoverprof/use-shared-state";
// Get all current keys
console.log(sharedStateUtils.getKeys());
// Get current state size
console.log(sharedStateUtils.getSize());
// Clear all state (optionally including persistent)
sharedStateUtils.clear(true);
// Delete specific key
sharedStateUtils.delete("some-key");
// Get all persistent keys
console.log(sharedStateUtils.getPersistentKeys());
`
- React >= 16.8.0
`tsx
import useSharedState from "@stackoverprof/use-shared-state";
function Counter() {
const [count, setCount] = useSharedState("counter", 0);
return (
Count: {count}
$3
`tsx
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}function ProductList() {
const [cartItems, setCartItems] = useSharedState(
"@cart-items",
[]
);
const addToCart = (product: CartItem) => {
setCartItems((prev) => {
const existing = prev?.find((item) => item.id === product.id);
if (existing) {
return (
prev?.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
) || []
);
}
return [...(prev || []), { ...product, quantity: 1 }];
});
};
return
{/ Product list /};
}function CartSummary() {
const [cartItems] = useSharedState("@cart-items", []);
const total =
cartItems?.reduce((sum, item) => sum + item.price * item.quantity, 0) ||
0;
return (
Cart ({cartItems?.length || 0} items)
Total: ${total.toFixed(2)}
);
}
`$3
`tsx
interface FormData {
name: string;
email: string;
preferences: string[];
}function Step1() {
const [formData, setFormData] = useSharedState("@form-data", {
name: "",
email: "",
preferences: [],
});
return (
value={formData?.name || ""}
onChange={(e) =>
setFormData((prev) => ({
...prev!,
name: e.target.value,
}))
}
placeholder="Name"
/>
);
}function Step2() {
const [formData, setFormData] = useSharedState("@form-data");
return (
Hello, {formData?.name}!
value={formData?.email || ""}
onChange={(e) =>
setFormData((prev) => ({
...prev!,
email: e.target.value,
}))
}
placeholder="Email"
/>
);
}
``MIT
Contributions are welcome! Please feel free to submit a Pull Request.