@oimdb/react 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +266 -133
- package/dist/index.cjs +340 -727
- package/dist/index.d.cts +53 -46
- package/dist/index.d.ts +53 -46
- package/dist/index.js +326 -699
- package/llms.txt +4156 -0
- package/package.json +46 -36
package/README.md
CHANGED
|
@@ -1,189 +1,322 @@
|
|
|
1
1
|
# @oimdb/react
|
|
2
2
|
|
|
3
|
-
React
|
|
3
|
+
React bindings for `@oimdb/core`. There is one binding hook and a handful of wrappers around it:
|
|
4
|
+
two that save you from writing `useMemo` at every call site, three that resolve a key and own
|
|
5
|
+
that lifetime for you, and two that own a computed store slot for the component. One more hook
|
|
6
|
+
owns a lifetime and takes no subscription at all, for the derivations a component reads.
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## For AI Assistants
|
|
8
|
-
|
|
9
|
-
If you are an AI assistant, read the full API reference before writing any OIMDB code:
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
node_modules/@oimdb/core/llms.txt
|
|
8
|
+
```bash
|
|
9
|
+
npm install @oimdb/react @oimdb/core
|
|
13
10
|
```
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
## Why one binding
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
Subscribing in this engine is one call: `OIMEffectService.INSTANCE.subscribeEffect(db, sourceSlots, sourceComputeds, body)`.
|
|
15
|
+
It carries no value; what changed is read off the slots afterwards. So a component needs
|
|
16
|
+
exactly three things: the roots a write can reach, the derivations to ask before reading,
|
|
17
|
+
and the derive function. Together they make up `TOIMBinding`, and `useOIMValue` is the hook that applies it through
|
|
18
|
+
`useSyncExternalStore`.
|
|
18
19
|
|
|
19
|
-
```
|
|
20
|
-
|
|
20
|
+
```ts signature
|
|
21
|
+
type TOIMBinding<TValue> = {
|
|
22
|
+
readonly sourceSlots?: readonly TOIMSlot[];
|
|
23
|
+
readonly sourceComputeds?: readonly TOIMComputedSlot[];
|
|
24
|
+
readonly derive: () => TValue;
|
|
25
|
+
};
|
|
21
26
|
```
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
Both source keys are optional; a missing one means none.
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
## A complete example
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
OIMReactiveCollectionIndexManualSetBased,
|
|
32
|
-
} from '@oimdb/core';
|
|
32
|
+
The store is created once, outside React, and shared by every example on this page.
|
|
33
|
+
A service is built once per type, at module scope, and holds no data; `createStoreSlot(db)` makes one
|
|
34
|
+
instance, a store slot, and every store door of the service takes that store slot first.
|
|
35
|
+
Whatever a read mints is swept at the end of the frame unless something took a real reference to it.
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
teamId: string;
|
|
37
|
-
name: string;
|
|
38
|
-
};
|
|
37
|
+
```ts setup
|
|
38
|
+
import { OIMDB, createOIMBucketService, createOIMEntityService, type TOIMEntitySlot } from '@oimdb/core';
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
type TCard = { id: string; deckId: string; title: string; rank: number };
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
selectPk: (user) => user.id,
|
|
44
|
-
});
|
|
42
|
+
export const db = new OIMDB();
|
|
45
43
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
string,
|
|
49
|
-
string,
|
|
50
|
-
User
|
|
51
|
-
>(queue, { collection: users }),
|
|
52
|
-
};
|
|
53
|
-
```
|
|
44
|
+
export const cardService = createOIMEntityService<TCard>(db, { keyOf: (card) => card.id });
|
|
45
|
+
export const cards = cardService.createStoreSlot(db);
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
// A bucket keeps the order the caller writes; a sorted list is
|
|
48
|
+
// `createOIMComputedServiceSorted` over an index.
|
|
49
|
+
export const byDeckService = createOIMBucketService<TOIMEntitySlot<TCard>>(db);
|
|
50
|
+
export const byDeck = byDeckService.createStoreSlot(db);
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
// A BUCKET'S SLOTS ARE YOURS. A write moves the card's value and files nothing, so the
|
|
53
|
+
// code that writes a deck also says who is in it. `byDeckService.holdSlotByKey` is the door onto a
|
|
54
|
+
// bucket by key: it mints one if there is none and hands back the bucket itself,
|
|
55
|
+
// counted. `owner.unholdSlot` gives that count back, once the bucket's own slots keep it.
|
|
56
|
+
export function setDeck(deckId: string, cardsInDeck: readonly TCard[]): void {
|
|
57
|
+
cardService.slotContentService.upsertEntities(cards, cardsInDeck);
|
|
58
|
+
const bucket = byDeckService.holdSlotByKey(byDeck, deckId);
|
|
59
|
+
byDeckService.slotContentService.writeBucketMemberSlots(
|
|
60
|
+
bucket, cardsInDeck.map((card) => cardService.getSlotByKey(cards, card.id)),
|
|
61
|
+
);
|
|
62
|
+
bucket.owner.unholdSlot(bucket);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
```tsx
|
|
67
|
+
import { OIMDBProvider, useOIMEntity, useOIMBucketSlotsByKey } from '@oimdb/react';
|
|
61
68
|
|
|
62
69
|
function App() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
return (
|
|
71
|
+
<OIMDBProvider db={db}>
|
|
72
|
+
<Card pk="c1" />
|
|
73
|
+
<DeckList deckId="d1" />
|
|
74
|
+
</OIMDBProvider>
|
|
75
|
+
);
|
|
68
76
|
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Keep indexes in your module, app schema, props, or a separate context:
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
+
// One entity, by key.
|
|
79
|
+
function Card({ pk }: { pk: string }) {
|
|
80
|
+
const card = useOIMEntity(cardService, cards, pk);
|
|
81
|
+
return <span>{card?.title}</span>;
|
|
82
|
+
}
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
// One group, by key. The hook resolves it, holds it while this component is
|
|
85
|
+
// mounted, and gives the hold back when it unmounts or when `deckId` moves —
|
|
86
|
+
// there is no release to remember here.
|
|
87
|
+
function DeckList({ deckId }: { deckId: string }) {
|
|
88
|
+
const deckCards = useOIMBucketSlotsByKey(byDeckService, byDeck, deckId);
|
|
89
|
+
return <ul>{deckCards.map((s) => <li key={String(s.key)}>{s.entity?.title}</li>)}</ul>;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
## The provider
|
|
94
|
+
|
|
95
|
+
**`<OIMDBProvider db={db}>`** puts the database in context (`OIMDBContext`), and every hook except
|
|
96
|
+
`useOIMSlotsByKeys` reads it through `useOIMDB()`. The effect service is stateless
|
|
97
|
+
(`OIMEffectService.INSTANCE`) and takes the database as its channel map, so there is nothing else
|
|
98
|
+
to provide. A slot filed on another `OIMDB` than the provider's would never wake the component, so
|
|
99
|
+
the hooks that resolve or take a slot check that its store slot runs on `db.defaultChannelSlot` and
|
|
100
|
+
throw when it does not.
|
|
101
|
+
|
|
102
|
+
## The hooks
|
|
103
|
+
|
|
104
|
+
Every hook that reads a collection takes the collection service first and the store slot second.
|
|
105
|
+
The parameter is typed as a `Pick` of `OIMSlotCollectionService` with only the members the hook calls.
|
|
106
|
+
|
|
107
|
+
- **`useOIMValue(binding)`**: the binding. It subscribes one effect over the binding's roots and
|
|
108
|
+
derivations (the effect holds each of them for as long as it lives), settles the derivations before
|
|
109
|
+
every read, and re-renders when the effect fires. **The binding must keep its identity across renders.** See
|
|
110
|
+
[the requirement](#the-binding-you-hand-useoimvalue-must-keep-its-identity); breaking it
|
|
111
|
+
causes a hang, not a slow path.
|
|
112
|
+
- **`useOIMComputedValue(computedSlot)`**: the value of ONE computed slot, re-rendering
|
|
113
|
+
when it changes. It is `useOIMValue` over `sourceComputeds: [computedSlot]`, with the
|
|
114
|
+
value read off `computedSlot.value` after `useOIMValue` settles it.
|
|
115
|
+
- **`useOIMEntity(entityCollectionService, entityStoreSlot, key)`**: one entity, by key. The key is resolved
|
|
116
|
+
to an identity ON COMMIT, not on render, through `holdSlotByKey(entityStoreSlot, key)`: a render is not a
|
|
117
|
+
lifetime, and an identity a discarded render minted would have no hold to give back. The render path reads
|
|
118
|
+
`getSlotByKey(entityStoreSlot, key)`, so the first paint shows current state. That read mints when the key
|
|
119
|
+
has none, but the mint goes to the database's unused-slot collector, so a render that never commits leaves
|
|
120
|
+
nothing standing past the next `db.flush()`.
|
|
121
|
+
- **`useOIMField(entityCollectionService, slot, name)`**: one field of one identity, through an
|
|
122
|
+
effect on `entityCollectionService.getColumn(slot, name)`. `name` is a column of that slot's entity, so a typo is a
|
|
123
|
+
compile error and the value comes back with the type that column holds. It re-renders when
|
|
124
|
+
that field changes and for no other reason. `slot` is an ENTITY slot: the subscription holds the identity it
|
|
125
|
+
did not resolve.
|
|
126
|
+
- **`useOIMBucketSlots(bucketCollectionService, bucketSlot)`**: a bucket's slots as a readonly array, in that
|
|
127
|
+
bucket's order, read through `bucketCollectionService.slotContentService.getBucketMemberSlots`. The array is
|
|
128
|
+
copied only when the bucket slot's change stamp (`changedAtRevision`, `changedAtEpoch`) moved. That stamp is
|
|
129
|
+
reliable here because the caller holds the bucket: a write to a slot nothing holds is not stamped.
|
|
130
|
+
- **`useOIMBucketSlotsByKey(bucketCollectionService, bucketStoreSlot, key)`**: one bucket by key, as an array of
|
|
131
|
+
entity slots. It resolves the bucket on commit, holds it for as long as the component is mounted, and gives the
|
|
132
|
+
hold back on unmount or when the key changes. It copies the members on every render: the bucket it reads during a
|
|
133
|
+
render may be held by nothing yet, and an unheld bucket's stamp does not move. **The key may be built inline**,
|
|
134
|
+
including the array that keys an `OIMSlotStoreServiceTreeDriven`; the hook settles it by asking the store which
|
|
135
|
+
bucket each key names.
|
|
136
|
+
- **`useOIMSlotsByKeys(slotCollectionService, storeSlot, keys)`**: several identities of one store slot,
|
|
137
|
+
resolved by key and held for as long as the component is mounted. It returns the slots and takes no subscription,
|
|
138
|
+
so use it when a component reads **derivations**: a computed slot exists only while something holds it, and a bare
|
|
139
|
+
read mints one that the next `flush()` takes straight back. See [Holds per component](#holds-per-component).
|
|
140
|
+
- **`useOIMComputedValueByKey(computedCollectionService, computedStoreSlot, key)`**: the value of one key of a
|
|
141
|
+
computed store slot. Pass `computedService.computedCollectionService`. It has the same body as `useOIMEntity`:
|
|
142
|
+
it resolves with `holdSlotByKey` on commit, subscribes over that computed slot, gives the resolve-hold straight
|
|
143
|
+
back, and reads the value through `computedSlot.computedValueService.getValueForSlot`.
|
|
144
|
+
- **`useOIMComputedCollection(computedService, derive, createSources, deps)`**: a keyed computed store slot the
|
|
145
|
+
component owns. The SERVICE is yours, built once at module scope with `createOIMComputedService(options)`; the hook
|
|
146
|
+
only calls `computedService.createComputedStoreSlot(db, derive, createSources)` in a `useMemo` keyed on `deps`, and
|
|
147
|
+
returns that store slot. `derive` and `createSources` may be `undefined` when the service already has them.
|
|
148
|
+
Read it by key with `useOIMComputedValueByKey(computedService.computedCollectionService, computedStoreSlot, key)`.
|
|
149
|
+
- **`useOIMSingleComputedValue(computedService, derive, createSources, deps)`**: a single computed the component
|
|
150
|
+
owns, read under the key `undefined`. The service is a void-keyed `createOIMComputedService` with
|
|
151
|
+
`computedSlotStoreService: OIMSlotStoreServiceSingleSlotDriven.INSTANCE`, built once at module scope. Per mount
|
|
152
|
+
the hook builds a store slot through `useOIMComputedCollection` and reads it through `useOIMComputedValueByKey`,
|
|
153
|
+
so a flush between render and subscribe, which releases the slot the render minted, is safe.
|
|
154
|
+
- **`useOIMSingleEntity(singleSlotService, singleSlotStoreSlot)`** and
|
|
155
|
+
**`useOIMSingleBucketSlots(singleSlotService, singleSlotStoreSlot)`**: the single entity and the single bucket.
|
|
156
|
+
See [Singles](#singles).
|
|
157
|
+
- **`useOIMDB()`**: the `OIMDB` from `OIMDBContext`. It throws if used outside `<OIMDBProvider>`.
|
|
83
158
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
159
|
+
```tsx
|
|
160
|
+
import { OIMSlotStoreServiceSingleSlotDriven, createOIMComputedService } from '@oimdb/core';
|
|
161
|
+
import type { TOIMComputedSlot, TOIMSingleSlotStore } from '@oimdb/core';
|
|
162
|
+
import { useOIMSingleComputedValue } from '@oimdb/react';
|
|
163
|
+
|
|
164
|
+
// Module scope: one service per kind of computed, shared by every component.
|
|
165
|
+
const totalRankService = createOIMComputedService<void, number, TOIMSingleSlotStore<TOIMComputedSlot<number, void>>>({
|
|
166
|
+
computedSlotStoreService:
|
|
167
|
+
OIMSlotStoreServiceSingleSlotDriven.INSTANCE as OIMSlotStoreServiceSingleSlotDriven<TOIMComputedSlot<number, void>>,
|
|
168
|
+
});
|
|
89
169
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
170
|
+
function TotalRank({ ids }: { ids: readonly string[] }) {
|
|
171
|
+
// Per mount: one store slot with its own derive and sources.
|
|
172
|
+
const total = useOIMSingleComputedValue(
|
|
173
|
+
totalRankService,
|
|
174
|
+
() => ids.reduce((sum, id) => sum + (cardService.getSlotByKey(cards, id).entity?.rank ?? 0), 0),
|
|
175
|
+
() => ({ sourceSlots: ids.map((id) => cardService.getSlotByKey(cards, id)) }),
|
|
176
|
+
[ids.join()],
|
|
177
|
+
);
|
|
178
|
+
return <span>{total}</span>;
|
|
97
179
|
}
|
|
98
180
|
```
|
|
99
181
|
|
|
100
|
-
##
|
|
182
|
+
## Singles
|
|
101
183
|
|
|
102
|
-
|
|
184
|
+
A single is a store slot that holds ONE identity and has no key: `createOIMSingleEntityService(db)` and
|
|
185
|
+
`createOIMSingleBucketService(db)` hand back an `OIMSingleSlotService`, read through `getSingleSlot(storeSlot)` and
|
|
186
|
+
`holdSingleSlot(storeSlot)`. The two single hooks take that service (typed as a `Pick` of `OIMSingleSlotService`)
|
|
187
|
+
and its store slot, and have the by-key hooks' mechanics without the key: the render reads `getSingleSlot`, whose
|
|
188
|
+
mint the next `flush()` takes back if no commit follows; the commit resolves with `holdSingleSlot`, subscribes over
|
|
189
|
+
that slot and gives the resolve-hold straight back, so a single released while nothing watched it is minted afresh.
|
|
103
190
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
### Set-Based Indexes
|
|
108
|
-
|
|
109
|
-
- `useSelectPksByIndexKeySetBased(index, key)` watches PKs for one index key.
|
|
110
|
-
- `useSelectPksByIndexKeysSetBased(index, keys)` watches PKs for several index keys.
|
|
111
|
-
- `useSelectEntitiesByIndexKeySetBased(collection, index, key)` watches entities referenced by one index key.
|
|
112
|
-
- `useSelectEntitiesByIndexKeysSetBased(collection, index, keys)` watches entities referenced by several index keys.
|
|
191
|
+
```tsx
|
|
192
|
+
import { createOIMSingleBucketService, createOIMSingleEntityService } from '@oimdb/core';
|
|
193
|
+
import { useOIMSingleBucketSlots, useOIMSingleEntity } from '@oimdb/react';
|
|
113
194
|
|
|
114
|
-
|
|
195
|
+
type TSettings = { theme: string };
|
|
115
196
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- `useSelectEntitiesByIndexKeyArrayBased(collection, index, key)` watches entities referenced by one index key.
|
|
119
|
-
- `useSelectEntitiesByIndexKeysArrayBased(collection, index, keys)` watches entities referenced by several index keys.
|
|
197
|
+
const settingsService = createOIMSingleEntityService<TSettings>(db);
|
|
198
|
+
const settings = settingsService.createStoreSlot(db);
|
|
120
199
|
|
|
121
|
-
|
|
200
|
+
const pickedService = createOIMSingleBucketService<TOIMEntitySlot<TCard>>(db);
|
|
201
|
+
const picked = pickedService.createStoreSlot(db);
|
|
122
202
|
|
|
123
|
-
|
|
203
|
+
function Toolbar() {
|
|
204
|
+
const theme = useOIMSingleEntity(settingsService, settings)?.theme;
|
|
205
|
+
const pickedCards = useOIMSingleBucketSlots(pickedService, picked);
|
|
206
|
+
return <span>{theme}: {pickedCards.length} picked</span>;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
124
209
|
|
|
125
|
-
|
|
126
|
-
|
|
210
|
+
`useOIMSingleBucketSlots` copies the members on every render, for the same reason as `useOIMBucketSlotsByKey`.
|
|
211
|
+
A single computed is `useOIMSingleComputedValue`.
|
|
127
212
|
|
|
128
|
-
|
|
213
|
+
## Binding a derivation
|
|
129
214
|
|
|
130
|
-
|
|
131
|
-
|
|
215
|
+
`createOIMComputedService` and `createOIMComputedServiceBucketMemo` both hand back a service of computed slots,
|
|
216
|
+
whose store slot holds them. You can either hold the key where the lifetime is (a module, your own store) and bind
|
|
217
|
+
it with `useOIMValue`, or let the component own the lifetime with `useOIMSlotsByKeys`.
|
|
132
218
|
|
|
133
|
-
|
|
219
|
+
`useOIMValue` produces the binding's derivations before every read, because a derivation is
|
|
220
|
+
asked and never woken. Only a computed slot's `computedValueService.getValueForSlot` produces its value, so a
|
|
221
|
+
`derive` that reaches a derivation's result any other way would show whatever the last produce left there.
|
|
134
222
|
|
|
135
|
-
|
|
223
|
+
**No wake is owed for what happened before the subscription.** An effect is stamped with the channel clock at
|
|
224
|
+
birth (`ranAtRevision` / `ranAtEpoch`), so a frame runs the body only when a source moved *after* the subscription.
|
|
225
|
+
Every hook here subscribes with `subscribeEffect`; `OIMEffectService.INSTANCE.createEffectImmediate` is the door
|
|
226
|
+
that delivers a value at once.
|
|
136
227
|
|
|
137
|
-
|
|
228
|
+
## Holds per component
|
|
138
229
|
|
|
139
|
-
|
|
230
|
+
`useOIMSlotsByKeys` holds every identity its keys name for as long as the component is mounted: on commit it
|
|
231
|
+
resolves each key through `holdSlotByKey(storeSlot, key)` and keeps that hold, and on unmount (or when the key list
|
|
232
|
+
moves) it gives back exactly the holds that commit took.
|
|
140
233
|
|
|
141
234
|
```tsx
|
|
142
|
-
import {
|
|
143
|
-
import {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Mutate entities in place — no per-update object allocation.
|
|
150
|
-
const cards = new OIMReactiveCollection<Card, string>(queue, {
|
|
151
|
-
selectPk: (c) => c.id,
|
|
152
|
-
updateEntity: createInPlaceEntityUpdater(),
|
|
235
|
+
import { createOIMComputedService, oimSameValueZeroGuard } from '@oimdb/core';
|
|
236
|
+
import { useOIMSlotsByKeys } from '@oimdb/react';
|
|
237
|
+
|
|
238
|
+
const rankService = createOIMComputedService<string, number>({
|
|
239
|
+
createSources: (id) => ({ sourceSlots: [cardService.getSlotByKey(cards, id)] }),
|
|
240
|
+
derive: (computedSlot) => cardService.getSlotByKey(cards, computedSlot.key).entity?.rank ?? -1,
|
|
241
|
+
sameValueGuard: oimSameValueZeroGuard,
|
|
153
242
|
});
|
|
243
|
+
const ranks = rankService.createComputedStoreSlot(db);
|
|
154
244
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
245
|
+
function Hand({ ids }: { ids: readonly string[] }) {
|
|
246
|
+
const slots = useOIMSlotsByKeys(rankService.computedCollectionService, ranks, ids);
|
|
247
|
+
return <ul>{slots.map((s) => <li key={String(s.key)}>{s.computedValueService.getValueForSlot(s)}</li>)}</ul>;
|
|
248
|
+
}
|
|
159
249
|
```
|
|
160
250
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
251
|
+
- **It resolves twice, on purpose.** The render goes through `getSlotByKey(storeSlot, key)`, whose mint the
|
|
252
|
+
database's unused-slot collector takes back at the next `flush()`, so a render that React throws away leaks
|
|
253
|
+
nothing. The commit then resolves the same keys through `holdSlotByKey(storeSlot, key)`. If a `flush()` landed
|
|
254
|
+
between the two and the commit therefore resolved a *different* identity, the hook re-renders once so nothing
|
|
255
|
+
downstream is left holding a released slot.
|
|
256
|
+
- **The key list may be built inline.** It is settled by contents, so `ids={['a', 'b']}` costs a length check and
|
|
257
|
+
two `===` on a render that did not change it, and takes no new holds.
|
|
258
|
+
- **It owns a lifetime, not a subscription.** Nothing here re-renders when a derivation's value moves; `useOIMValue`
|
|
259
|
+
does that. Hold with this hook, wake with that one. It needs no provider because it subscribes to nothing.
|
|
164
260
|
|
|
165
|
-
##
|
|
261
|
+
## The binding you hand `useOIMValue` must keep its identity
|
|
166
262
|
|
|
167
|
-
|
|
168
|
-
import {
|
|
169
|
-
OIMCollectionsProvider,
|
|
170
|
-
createOIMCollectionsContext,
|
|
171
|
-
useOIMCollectionsContext,
|
|
172
|
-
} from '@oimdb/react';
|
|
173
|
-
```
|
|
263
|
+
This is a hard requirement. Getting it wrong costs more than an allocation: it hangs the screen.
|
|
174
264
|
|
|
175
|
-
|
|
265
|
+
```tsx
|
|
266
|
+
import { useMemo } from 'react';
|
|
267
|
+
import { useOIMValue } from '@oimdb/react';
|
|
176
268
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
};
|
|
269
|
+
function Deck({ bucket }: { bucket: ReturnType<typeof byDeckService.getSlotByKey> }) {
|
|
270
|
+
// DO NOT. A new binding every render, and this never stops rendering.
|
|
271
|
+
const unstable = useOIMValue({ sourceSlots: [bucket],
|
|
272
|
+
derive: () => Array.from(byDeckService.slotContentService.getBucketMemberSlots(bucket)) });
|
|
181
273
|
|
|
182
|
-
|
|
274
|
+
// Do. The binding is memoised on what it was built from.
|
|
275
|
+
const stable = useOIMValue(useMemo(() => ({
|
|
276
|
+
sourceSlots: [bucket],
|
|
277
|
+
derive: () => Array.from(byDeckService.slotContentService.getBucketMemberSlots(bucket)),
|
|
278
|
+
}), [bucket]));
|
|
183
279
|
|
|
184
|
-
|
|
185
|
-
const { users } = useOIMCollectionsContext<AppCollections>();
|
|
186
|
-
const user = useSelectEntityByPk(users, userId);
|
|
187
|
-
return <span>{user?.name}</span>;
|
|
280
|
+
return <span>{unstable.length + stable.length}</span>;
|
|
188
281
|
}
|
|
189
282
|
```
|
|
283
|
+
|
|
284
|
+
The first form misses `useOIMValue`'s memo on every render, which gives `subscribe` a new
|
|
285
|
+
identity, and React responds by resubscribing on every commit. The counter bump on
|
|
286
|
+
`subscribe`'s first line then makes React's post-commit version check differ, so React forces another
|
|
287
|
+
render, which builds another binding. **Nothing converges.** React logs
|
|
288
|
+
`Maximum update depth exceeded`, which is a `console.error` and not a throw, so you
|
|
289
|
+
get a frozen screen and a growing console instead of an error you can catch.
|
|
290
|
+
|
|
291
|
+
`useOIMField` and `useOIMBucketSlots` exist to write that `useMemo` once, so no call site has to.
|
|
292
|
+
|
|
293
|
+
**This requirement does not apply to the key of a by-key hook.** `useOIMEntity`,
|
|
294
|
+
`useOIMBucketSlotsByKey`, `useOIMComputedValueByKey` and, through it, `useOIMSingleComputedValue`
|
|
295
|
+
share one body, because their key cannot be kept stable: a tree-driven key is an *array*. So the hook settles its
|
|
296
|
+
key before its memo sees it, by asking the collection which identity each names. Two keys that resolve to one
|
|
297
|
+
identity are one key.
|
|
298
|
+
|
|
299
|
+
## Things worth knowing
|
|
300
|
+
|
|
301
|
+
- **The snapshot `useOIMValue` compares is a local counter, not the value.** `createOIMEntityWriterInPlace` keeps
|
|
302
|
+
an entity's object reference stable, a bucket hands its slots back as the same live container, and a column
|
|
303
|
+
subscription fires only for its own column; a value-comparing binding would fire on none of the three. The counter
|
|
304
|
+
is bumped ON SUBSCRIBING too, so a write that lands between the render and the subscription is still shown. The
|
|
305
|
+
price is one extra render per SUBSCRIPTION commit, not per render and not per notification.
|
|
306
|
+
- **A subscription holds what it reads, and gives it back when it is torn down.** The by-key hooks resolve with
|
|
307
|
+
`holdSlotByKey` and give that hold straight back once the effect exists, so no root is counted twice.
|
|
308
|
+
`useOIMField` holds its ENTITY slot for itself; its root is the COLUMN, minted inside the subscription.
|
|
309
|
+
- **Services are yours, at module scope.** The provider carries the database and nothing else. A computed service
|
|
310
|
+
is one per kind of computed; a component that owns a computed gets only a store slot from
|
|
311
|
+
`useOIMComputedCollection` / `useOIMSingleComputedValue`.
|
|
312
|
+
- **Nothing re-files an entity when it is written into a bucket you maintain.** Write the buckets in the same code
|
|
313
|
+
that writes the entity, or use an index service (`createOIMIndexService`), which files entities by the key it
|
|
314
|
+
derives from each one.
|
|
315
|
+
- **A bucket never sorts.** A list sorted by a column is `createOIMComputedServiceSorted({ indexService, sortColumns })`
|
|
316
|
+
over an index service, with `createSortedStoreSlot(indexStoreSlot)`.
|
|
317
|
+
|
|
318
|
+
## Reference
|
|
319
|
+
|
|
320
|
+
The full core API is in **`llms.txt`**, which ships inside this package: read it beside this
|
|
321
|
+
README in the installed package, or
|
|
322
|
+
[on GitHub](https://github.com/abaikov/oimdb/blob/master/llms.txt).
|