Observable Map
A tiny, reactive Map that plays nice with React's useSyncExternalStore.
Installation
pnpm dlx shadcn add https://meeshan.dev/r/observable-map.jsonWhy do you need this?
Handling complex state in React, especially when you're dealing with big collections of data, is a bit of a pain. You want your app to be fast and only re-render when it absolutely has to, but getting there usually involves a lot of head-scratching.
If you use useState for everything, you'll likely end up with either a mess of boilerplate or a slow app because of unnecessary re-renders. ObservableMap gives you a simple way to manage key-value pairs that tells React exactly when to update. It's built to work perfectly with useSyncExternalStore so you get optimal performance without the headache.
With ObservableMap, you can:
- Grab data from high up in your component tree without making everything re-render.
- Build reactive data structures that update the UI automatically — no heavy state management libraries required.
- Keep your code clean with a straightforward API.
- Take full advantage of React's concurrent features.
Usage
Here's how you can set it up. In this example, I'm using a ScopeProvider to share the map, but you could just as easily use standard React Context.
import React from 'react';
import { observableMap, type ObservableMap } from '~/lib/observable-map';
import { ScopeProvider, useScopeCtx } from '~/lib/scope-provider';
type Store = ObservableMap<string, boolean>;
// This component won't re-render when selections change
function App() {
const store = React.useRef<Store>(observableMap()).current;
return (
<ScopeProvider value={store}>
<SelectionList />
<SelectionActions />
</ScopeProvider>
);
}
// This one re-renders only when the data it's watching changes
function SelectionList() {
const store = useScopeCtx<Store>();
const selections = React.useSyncExternalStore(
store.subscribe,
store.getSnapshot,
);
return <p>Selected: {selections.length}</p>;
}
// This one never re-renders because it just calls methods on the store
function SelectionActions() {
const store = useScopeCtx<Store>();
return (
<div>
<button onClick={() => store.set('item-1', true)}>Select Item 1</button>
<button
onClick={() => {
store.delete('item-1');
}}
>
Deselect Item 1
</button>
</div>
);
}Batching Updates
If you need to update a bunch of items at once, use batch. This groups your changes so React only triggers one notification (and one re-render) instead of one for every single change.
// This triggers 3 notifications
selectionMap.set('1', true);
selectionMap.set('2', true);
selectionMap.set('3', true);
// This triggers only 1 notification
selectionMap.batch(() => {
selectionMap.originalMap.set('1', true);
selectionMap.originalMap.set('2', true);
selectionMap.originalMap.set('3', true);
});Getting to the Original Map
Sometimes you just need the raw Map. You can access it directly whenever you need to iterate or check the size.
const store = useScopeCtx<Store>();
// Loop through everything
for (const [key, value] of store.originalMap) {
console.log(key, value);
}
// Check the size
console.log(store.originalMap.size);A few tips
- Keep it stable — Make sure you define your map outside of your components or wrap it in
useRef. You don't want a new map being created every time your component renders. - Batch when you can — If you're updating more than one entry, wrap them in
batch()to keep things snappy. - Stick to the wrapper methods — Use
set,delete, andclearinstead of touchingoriginalMapdirectly if you want subscribers to be notified automatically. - Context is your friend — For app-wide state, wrap your map in a React Context. It makes accessing your data anywhere in the app a breeze. Check out the Scope Provider if you want a clean way to do this.
API Reference
Parameters
| Parameter | Type | Description |
|---|---|---|
initial | Iterable<readonly [K, V]> | Optional initial entries for the map |
Returns
Returns: ObservableMap<K, V>
| Prop | Signature | Description |
|---|---|---|
get | (key: K) => V | undefined | Returns the value for the given key |
has | (key: K) => boolean | Returns true if the key exists |
set | (key: K, value: V) => void | Sets a value and notifies subscribers if changed |
delete | (key: K) => void | Removes a key and notifies subscribers if it existed |
clear | () => void | Removes all entries and notifies subscribers if map was non-empty |
subscribe | (listener: () => void) => () => void | Subscribes to changes, returns unsubscribe function |
getSnapshot | () => V[] | Returns cached array of values for useSyncExternalStore |
batch | (fn: () => void) => void | Executes function and triggers single notification |
originalMap | Map<K, V> | Direct access to the underlying Map |