@contember/bindx-react 0.1.44 → 0.1.46

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.
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
- import { GraphQlClient } from '@contember/graphql-client';
3
2
  import { SnapshotStore, UndoManager, type SchemaDefinition, type SchemaNames, type UndoManagerConfig, type UpdateMode } from '@contember/bindx';
3
+ import { type BindxGraphQlClient } from './BackendAdapterContext.js';
4
4
  /**
5
5
  * Converts SchemaNames (Contember format) to SchemaDefinition (bindx format)
6
6
  * so that a SchemaRegistry can be created for standalone hooks.
@@ -16,7 +16,7 @@ export interface ContemberBindxProviderProps {
16
16
  store?: SnapshotStore;
17
17
  /** Children */
18
18
  children: ReactNode;
19
- client: GraphQlClient;
19
+ client: BindxGraphQlClient;
20
20
  /** Enable undo/redo functionality. Pass true to auto-create an UndoManager, or pass an UndoManager instance. */
21
21
  undoManager?: UndoManager | boolean;
22
22
  /** Configuration for auto-created undo manager (only used when undoManager={true}) */
@@ -1 +1 @@
1
- {"version":3,"file":"ContemberBindxProvider.d.ts","sourceRoot":"","sources":["../../src/hooks/ContemberBindxProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,OAAO,EAAoB,aAAa,EAAuF,WAAW,EAAqC,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAiB,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAIxS;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAwBnG;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,gDAAgD;IAChD,MAAM,EAAE,WAAW,CAAA;IACnB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,eAAe;IACf,QAAQ,EAAE,SAAS,CAAA;IAEnB,MAAM,EAAE,aAAa,CAAA;IAErB,gHAAgH;IAChH,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAA;IACnC,sFAAsF;IACtF,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,UAAU,CAAA;IAC9B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;GAEG;AACH,eAAO,MAAM,sBAAsB,mEAkEjC,CAAA"}
1
+ {"version":3,"file":"ContemberBindxProvider.d.ts","sourceRoot":"","sources":["../../src/hooks/ContemberBindxProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,EAAoB,aAAa,EAAuF,WAAW,EAAqC,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAiB,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACxS,OAAO,EAAwC,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAG1G;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAwBnG;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,gDAAgD;IAChD,MAAM,EAAE,WAAW,CAAA;IACnB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,eAAe;IACf,QAAQ,EAAE,SAAS,CAAA;IAEnB,MAAM,EAAE,kBAAkB,CAAA;IAE1B,gHAAgH;IAChH,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAA;IACnC,sFAAsF;IACtF,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,UAAU,CAAA;IAC9B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;GAEG;AACH,eAAO,MAAM,sBAAsB,mEAkEjC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../../../src/jsx/components/Entity.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAKrH,OAAO,KAAK,EAAkB,SAAS,EAAiB,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAiB,KAAK,UAAU,EAAgF,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAI1M;;;GAGG;AACH,UAAU,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAChE,kCAAkC;IAClC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IACxE,qBAAqB;IACrB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAA;CAC9C;AAED;;GAEG;AACH,UAAU,aAAa,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACjG,sFAAsF;IACtF,EAAE,EAAE,iBAAiB,CAAA;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,WAAW,CAAC,EAAE,KAAK,CAAA;IACnB,uBAAuB;IACvB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;GAEG;AACH,UAAU,iBAAiB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACrG,EAAE,CAAC,EAAE,KAAK,CAAA;IACV,8DAA8D;IAC9D,MAAM,EAAE,IAAI,CAAA;IACZ,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,OAAO,CAAC,EAAE,KAAK,CAAA;IACf,QAAQ,CAAC,EAAE,KAAK,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IACrF,aAAa,CAAC,QAAQ,CAAC,GACvB,iBAAiB,CAAC,QAAQ,CAAC,CAAA;AAiR9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,iBAAS,UAAU,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC1D,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,GAC1B,YAAY,GAAG,IAAI,CA2BrB;AAGD,eAAO,MAAM,MAAM,EAAkC,OAAO,UAAU,CAAA"}
1
+ {"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../../../src/jsx/components/Entity.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAkG,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAKhJ,OAAO,KAAK,EAAkB,SAAS,EAAiB,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAiB,KAAK,UAAU,EAAgF,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAI1M;;;GAGG;AACH,UAAU,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAChE,kCAAkC;IAClC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IACxE,qBAAqB;IACrB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAA;CAC9C;AAED;;GAEG;AACH,UAAU,aAAa,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACjG,sFAAsF;IACtF,EAAE,EAAE,iBAAiB,CAAA;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,WAAW,CAAC,EAAE,KAAK,CAAA;IACnB,uBAAuB;IACvB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;GAEG;AACH,UAAU,iBAAiB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACrG,EAAE,CAAC,EAAE,KAAK,CAAA;IACV,8DAA8D;IAC9D,MAAM,EAAE,IAAI,CAAA;IACZ,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,OAAO,CAAC,EAAE,KAAK,CAAA;IACf,QAAQ,CAAC,EAAE,KAAK,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IACrF,aAAa,CAAC,QAAQ,CAAC,GACvB,iBAAiB,CAAC,QAAQ,CAAC,CAAA;AAmT9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,iBAAS,UAAU,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC1D,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,GAC1B,YAAY,GAAG,IAAI,CA2BrB;AAGD,eAAO,MAAM,MAAM,EAAkC,OAAO,UAAU,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/bindx-react",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "Type-safe React data binding framework",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -15,8 +15,8 @@
15
15
  "react": ">=18.0.0"
16
16
  },
17
17
  "dependencies": {
18
- "@contember/bindx": "0.1.44",
19
- "@contember/bindx-client": "0.1.44",
18
+ "@contember/bindx": "0.1.46",
19
+ "@contember/bindx-client": "0.1.46",
20
20
  "@contember/graphql-client": "^2.1.0-beta.1",
21
21
  "@radix-ui/primitive": "^1.1.3",
22
22
  "@radix-ui/react-slot": "^1.2.4"
@@ -1,8 +1,7 @@
1
1
  import { memo, useMemo, type ReactNode } from 'react'
2
- import { GraphQlClient } from '@contember/graphql-client'
3
2
  import { ContentClient } from '@contember/bindx-client'
4
3
  import { ContemberAdapter, SnapshotStore, ActionDispatcher, BatchPersister, MutationCollector, ContemberSchemaMutationAdapter, UndoManager, SchemaRegistry, NotificationStore, type SchemaDefinition, type SchemaNames, type FieldDef, type UndoManagerConfig, type UpdateMode } from '@contember/bindx'
5
- import { BindxContext, type BindxContextValue } from './BackendAdapterContext.js'
4
+ import { BindxContext, type BindxContextValue, type BindxGraphQlClient } from './BackendAdapterContext.js'
6
5
  import { QueryBatcher } from '../batching/QueryBatcher.js'
7
6
 
8
7
  /**
@@ -46,7 +45,7 @@ export interface ContemberBindxProviderProps {
46
45
  /** Children */
47
46
  children: ReactNode
48
47
 
49
- client: GraphQlClient
48
+ client: BindxGraphQlClient
50
49
 
51
50
  /** Enable undo/redo functionality. Pass true to auto-create an UndoManager, or pass an UndoManager instance. */
52
51
  undoManager?: UndoManager | boolean
@@ -1,4 +1,4 @@
1
- import React, { memo, useCallback, useEffect, useMemo, useRef, useSyncExternalStore, type ReactElement } from 'react'
1
+ import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactElement } from 'react'
2
2
  import { useBindxContext, useSchemaRegistry } from '../../hooks/BackendAdapterContext.js'
3
3
  import { annotateElement } from '../devAnnotations.js'
4
4
  import { useEntity } from '../../hooks/useEntity.js'
@@ -163,49 +163,83 @@ interface CreateModeSnapshot {
163
163
  }
164
164
 
165
165
  /**
166
- * Internal component for create mode (creating a new entity)
166
+ * Internal component for create mode (creating a new entity).
167
+ *
168
+ * The draft entity is minted in a layout effect, NEVER during render. A
169
+ * render-phase store mutation is unsafe two ways:
170
+ * - it leaks the draft whenever the render is discarded — StrictMode
171
+ * double-invoke, an aborted concurrent render, a thrown render, Suspense —
172
+ * because no effect cleanup runs for a render that never commits;
173
+ * - it synchronously notifies the store's global subscribers, force-updating an
174
+ * already-mounted aggregate subscriber (e.g. a `usePersist` consumer)
175
+ * mid-render. React rejects that ("Cannot update a component while rendering a
176
+ * different component") and the create form renders blank.
177
+ * A layout effect runs before paint, so the draft exists before the user sees
178
+ * anything and its creation notifies subscribers normally (no stale dirty state).
167
179
  */
168
180
  function EntityCreateMode({
169
181
  entityType,
170
182
  children,
171
- error: errorFallback,
172
183
  onPersisted,
173
184
  }: EntityCreateModeProps): ReactElement {
174
- const { store, dispatcher } = useBindxContext()
175
- const schemaRegistry = useSchemaRegistry()
185
+ const { store } = useBindxContext()
176
186
  const tempIdRef = useRef<string | null>(null)
177
-
178
- // Create entity once on mount (using ref to ensure only one creation)
179
- const tempId = useMemo(() => {
180
- if (tempIdRef.current) {
181
- return tempIdRef.current
182
- }
183
- const id = store.createEntity(entityType)
184
- tempIdRef.current = id
185
- return id
186
- }, [entityType, store])
187
-
188
- // Lifecycle of the draft entity:
189
- // - Re-seed it under the SAME temp id if a previous unmount's cleanup removed
190
- // it. This keeps the form bound across a React StrictMode mount→cleanup→mount
191
- // cycle (effects run twice in dev), where the memoized id survives but the
192
- // snapshot would otherwise be gone. No-op on first mount and after the entity
193
- // was persisted (rekeyed to a server id).
194
- // - On unmount, discard a never-persisted draft. Drop its root registration and
195
- // let the reachability-aware sweep reclaim it ONLY if nothing else references
196
- // it — a draft connected into another live parent stays reachable and survives
197
- // (the diamond / shared-create case). A persisted entity is left untouched.
198
- useEffect(() => {
199
- if (!store.getPersistedId(entityType, tempId) && !store.hasEntity(entityType, tempId)) {
200
- store.createEntity(entityType, { id: tempId })
187
+ const [tempId, setTempId] = useState<string | null>(null)
188
+
189
+ useLayoutEffect(() => {
190
+ if (!tempIdRef.current) {
191
+ tempIdRef.current = store.createEntity(entityType)
192
+ setTempId(tempIdRef.current)
193
+ } else if (!store.getPersistedId(entityType, tempIdRef.current) && !store.hasEntity(entityType, tempIdRef.current)) {
194
+ // Re-seed under the SAME temp id if a previous cleanup removed it (a React
195
+ // StrictMode mount→cleanup→mount cycle), so the form stays bound across it.
196
+ store.createEntity(entityType, { id: tempIdRef.current })
201
197
  }
202
198
  return () => {
203
- if (!store.getPersistedId(entityType, tempId)) {
204
- store.unregisterRootEntity(entityType, tempId)
199
+ const id = tempIdRef.current
200
+ // On unmount, discard a never-persisted draft: drop its root registration
201
+ // and let the reachability-aware sweep reclaim it ONLY if nothing else
202
+ // references it — a draft connected into another live parent stays reachable
203
+ // and survives (the diamond / shared-create case). A persisted entity is
204
+ // left untouched.
205
+ if (id && !store.getPersistedId(entityType, id)) {
206
+ store.unregisterRootEntity(entityType, id)
205
207
  store.sweepUnreachableCreated()
206
208
  }
207
209
  }
208
- }, [store, entityType, tempId])
210
+ }, [store, entityType])
211
+
212
+ // First render, before the layout effect mints the draft. The draft is created
213
+ // before paint, so this empty frame is never visible.
214
+ if (tempId === null) {
215
+ return <></>
216
+ }
217
+
218
+ return (
219
+ <EntityCreateModeInner entityType={entityType} tempId={tempId} onPersisted={onPersisted}>
220
+ {children}
221
+ </EntityCreateModeInner>
222
+ )
223
+ }
224
+
225
+ /**
226
+ * Renders the create-mode draft once it exists. Split out of EntityCreateMode so
227
+ * its store subscriptions and selection collection always run against a real
228
+ * tempId (never null) — the hooks stay unconditional.
229
+ */
230
+ function EntityCreateModeInner({
231
+ entityType,
232
+ tempId,
233
+ children,
234
+ onPersisted,
235
+ }: {
236
+ entityType: string
237
+ tempId: string
238
+ children: (entity: EntityAccessor<unknown>) => React.ReactNode
239
+ onPersisted?: (id: string) => void
240
+ }): ReactElement {
241
+ const { store, dispatcher } = useBindxContext()
242
+ const schemaRegistry = useSchemaRegistry()
209
243
 
210
244
  // Subscribe to store changes for this entity
211
245
  const subscribe = useCallback(