Skip to main content

React

@oimdb/react provides hooks and a context provider. All hooks use useSyncExternalStore and re-render only when the specific data they watch actually changes.

npm install @oimdb/react @oimdb/core

Setup

Create your store outside React, then pass collections through a Provider:

// store.ts
import { createOIMCollectionKit, OIMEventQueue, OIMEventQueueSchedulerFactory } from '@oimdb/core';

const queue = new OIMEventQueue({
scheduler: OIMEventQueueSchedulerFactory.createMicrotask(),
});

const users = createOIMCollectionKit<User, string>(queue, {
selectPk: (u) => u.id,
});
const tasks = createOIMCollectionKit<Task, string>(queue, {
selectPk: (t) => t.id,
});

export const byTeam = users.indexFactory.derivedSetIndex((u) => u.teamId);
export const byStatus = tasks.indexFactory.derivedSetIndex((t) => t.status);

export const collections = {
users: users.collection,
tasks: tasks.collection,
};
// App.tsx
import { OIMCollectionsProvider } from '@oimdb/react';
import { collections } from './store';

export function App() {
return (
<OIMCollectionsProvider collections={collections}>
<TeamList teamId="team1" />
</OIMCollectionsProvider>
);
}

Hooks

Access collections via useOIMCollectionsContext, then pass them to hooks:

import { useOIMCollectionsContext, useSelectEntitiesByIndexKeySetBased, useSelectEntityByPk } from '@oimdb/react';
import { byTeam } from './store';

type AppCollections = typeof collections;

function TeamList({ teamId }: { teamId: string }) {
const { users } = useOIMCollectionsContext<AppCollections>();
const members = useSelectEntitiesByIndexKeySetBased(users, byTeam, teamId);

return <ul>{members?.map(u => u && <li key={u.id}>{u.name}</li>)}</ul>;
}

function UserCard({ userId }: { userId: string }) {
const { users } = useOIMCollectionsContext<AppCollections>();
const user = useSelectEntityByPk(users, userId);

return <span>{user?.name ?? '—'}</span>;
}

Collection hooks

HookReturns
useSelectEntityByPk(collection, pk)TEntity | undefined
useSelectEntitiesByPks(collection, pks)(TEntity | undefined)[]
useSelectEntitiesByIndexKeySetBased(collection, index, key)(TEntity | undefined)[]
useSelectEntitiesByIndexKeysSetBased(collection, index, keys)(TEntity | undefined)[]
useSelectEntitiesByIndexKeyArrayBased(collection, index, key)(TEntity | undefined)[]
useSelectEntitiesByIndexKeysArrayBased(collection, index, keys)(TEntity | undefined)[]

Index-only hooks (PKs without entities)

HookReturns
useSelectPksByIndexKeySetBased(index, key)Set<TPk>
useSelectPksByIndexKeysSetBased(index, keys)TPk[] (flattened)
useSelectPksByIndexKeyArrayBased(index, key)TPk[]
useSelectPksByIndexKeysArrayBased(index, keys)TPk[] (flattened)

Object hooks

For OIMReactiveObject (settings, flags, single values):

HookReturns
useSelectValueByObjectKey(obj, key)TValue | undefined
useSelectValuesByObjectKeys(obj, keys)(TValue | undefined)[]

Signal hooks + mutable collections (advanced)

The default hooks use useSyncExternalStore, which detects change by reference (Object.is). That requires the store to produce a new entity object per update — the default immutable merge ({ ...prev, ...draft }), whose copy is the biggest per-update data-layer cost.

For update-heavy workloads you can run a collection in mutable / in-place mode and bind it with the lighter signal hooks:

import { OIMReactiveCollection, createInPlaceEntityUpdater } from '@oimdb/core';
import { useSelectEntityByPkSignal, useSelectPksByIndexKeyArrayBasedSignal } from '@oimdb/react';

// Mutate entities in place — no per-update object allocation.
const cards = new OIMReactiveCollection(queue, {
selectPk: c => c.id,
updateEntity: createInPlaceEntityUpdater(),
});

// Signal hooks re-render on the keyed notification (no Object.is), so they see
// in-place mutations that the default uSES hooks would miss.
const card = useSelectEntityByPkSignal(cards, id);
const ids = useSelectPksByIndexKeyArrayBasedSignal(cardsByDeck, deckId);

Available signal hooks (same signatures as their default counterparts):

HookReturns
useSelectEntityByPkSignal(collection, pk)TEntity | undefined
useSelectPksByIndexKeyArrayBasedSignal(index, key)readonly TPk[]
useSelectPksByIndexKeySetBasedSignal(index, key)ReadonlySet<TPk>

These three cover the fine-grained pattern: a parent reads pks by index key, each row reads its own entity by pk. (Entity-by-pk for the row + pks-by-index for the list — no signal variants exist for the *Keys plural or entities-by-index hooks; compose from these.)

This drops both the merge copy and the uSES overhead — the per-update work MobX also avoids. It pays off where the data layer is the bottleneck (very fine-grained renderers, large update-heavy lists); in plain React the per-component commit usually dominates, so the win is small.

Use only when every reader is subscription-driven, and mind the trade-offs:

Default hooks*Signal hooks + in-place
Change detectionreference (Object.is)keyed notification
Per-update allocationnew entity objectnone (in place)
Concurrent Modetearing-safenot tearing-safe
Reference identitystable per changestable across changes — breaks React.memo on entities, prev/next diffing, time-travel, and the immutable-only consumers (e.g. the default uSES hooks on the same collection)

Select each entity where you render it (by pk); don't pass mutable entities into React.memo children expecting them to re-render.

Multiple contexts

If you have multiple independent app sections, create separate typed contexts instead of sharing the global one:

import { createOIMCollectionsContext, OIMCollectionsProvider, useOIMCollectionsContext } from '@oimdb/react';

const AdminContext = createOIMCollectionsContext<{ users: typeof users.collection }>();

function AdminPanel() {
return (
<OIMCollectionsProvider collections={{ users: users.collection }} context={AdminContext}>
<AdminContent />
</OIMCollectionsProvider>
);
}

function AdminContent() {
const { users } = useOIMCollectionsContext(AdminContext);
// ...
}