Meeshan.dev
Shadcn RegistryLib

Observable Map

A tiny, reactive Map that plays nice with React's useSyncExternalStore.

Installation

pnpm dlx shadcn add https://meeshan.dev/r/observable-map.json

Why 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

  1. 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.
  2. Batch when you can — If you're updating more than one entry, wrap them in batch() to keep things snappy.
  3. Stick to the wrapper methods — Use set, delete, and clear instead of touching originalMap directly if you want subscribers to be notified automatically.
  4. 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

ParameterTypeDescription
initialIterable<readonly [K, V]>Optional initial entries for the map

Returns

Returns: ObservableMap<K, V>

PropSignatureDescription
get(key: K) => V | undefinedReturns the value for the given key
has(key: K) => booleanReturns true if the key exists
set(key: K, value: V) => voidSets a value and notifies subscribers if changed
delete(key: K) => voidRemoves a key and notifies subscribers if it existed
clear() => voidRemoves all entries and notifies subscribers if map was non-empty
subscribe(listener: () => void) => () => voidSubscribes to changes, returns unsubscribe function
getSnapshot() => V[]Returns cached array of values for useSyncExternalStore
batch(fn: () => void) => voidExecutes function and triggers single notification
originalMapMap<K, V>Direct access to the underlying Map

On this page