You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
useEntity / <Entity> has no public way to refetch the same query without unmounting the consumer. Today the only escape is key={...} on the component to force a remount, which throws away every piece of UI state below it (open dialogs, scroll position, undo history, local form drafts) and replaces it with a loading placeholder for the duration of the round trip.
This is the missing primitive every fetch layer (React Query, SWR, Apollo, urql) ships from day one. Its absence is what makes bindx painful to use side-by-side with workflow RPC that mutates the entity behind bindx's back — and that pattern is now the dominant write path in our app.
Why this matters — the workflow interop problem
We have two mutation paths in the project:
Bindx persist — user edits fields locally, persistAllWithFeedback() sends a batched mutation. Bindx is the writer; its in-memory state stays consistent because it did the write.
Workflow RPC — user clicks "Schválit", "Označit jako uskutečněné", "Přidělit konzultanta". The worker validates preconditions, writes under its service token, attributes to the actor via X-Contember-Assume-Identity, and applies cascading side-effects. Bindx never saw any of this happen.
After (2), the bindx tree is stale: the workflow flipped a status enum, set a respondedAt timestamp, maybe created an Audit row. Until bindx re-reads, the UI keeps showing the pre-RPC values, status badges render with the old enum, gating conditionals (status === 'planned' && canManage) keep showing buttons that no longer apply.
useEntityRefreshKey() is bumped by useWorkflowAction's bumpRefresh() after a successful RPC. The key={refreshKey} change forces a React unmount + remount, which restarts bindx's useEntity from scratch — fresh fetch, fresh state.
This works but the UX is terrible:
During the 300-1000ms refetch, <Entity> returns <DefaultLoading /> because result.$status === 'loading'. The entire subtree disappears.
After the data arrives, the whole subtree mounts fresh. React loses scroll position → page jumps to the top.
Any open <AlertDialog>, expanded row, in-flight bindx persist draft, undo history — gone.
For per-row workflow actions (flip one meeting's status from "planned" → "completed"), this is wildly disproportionate to the actual data change.
What we need
A way to tell bindx "re-run this query against the server, replace the data, but keep the accessor identity stable so React doesn't remount the consumer".
The minimal version exposes the existing internal queryKey plumbing (UseEntityOptions.queryKey already exists in useEntity for cache invalidation; useEntityList has it too — <Entity> just doesn't take a queryKey prop).
<Entityentity={schema.Foo}by={{ id }}queryKey={refreshKey}>{foo=>…}</Entity>
Or, equivalently, an imperative escape hatch on the accessor:
constaccessor=useEntity(schema.Foo,{by: { id }},e=>e.…)awaitaccessor.$refetch()
Either form would let us replace <Entity key={refreshKey}> with <Entity queryKey={refreshKey}> and the entire flicker problem goes away — bindx silently re-fetches under the existing accessor, swaps the data in, React rerenders with new field values, no unmount, no loading placeholder, no scroll skip.
We use useRefreshableEntityList for collections today and it works exactly this way (folds the refresh counter into queryKey), so the precedent is established. <Entity> just doesn't expose the same hook.
Suggested API
Path of least resistance: pass queryKey through EntityByProps.
// packages/bindx-react/src/jsx/components/Entity.tsx (rough sketch)interfaceEntityByProps<TRoleMap>{by: EntityUniqueWherequeryKey?: string// ← new, optional, passed straight to useEntity// …existing props}constresult=useEntity({$name: entityType}asEntityDef,{
by,
selection,queryKey: queryKey??internalQueryKey,// ← internal one is what useSelectionCollection returns today})
Behavior: a queryKey change triggers useEntity to re-fetch under the same accessor identity. The consumer's render-prop receives the same EntityAccessor instance with refreshed field values. No remount.
The accessor-level $refetch() form would also be useful for imperative cases (poll, retry-after-error, "refresh" button), but the prop form alone unblocks the workflow RPC story.
Open questions for maintainers:
Suspense semantics during refetch. Should the consumer see $isLoading: true again, or should $status stay 'ready' with stale data until the new data lands ("stale-while-revalidate" mode)? The latter eliminates the last 20% of flicker and matches what React Query / SWR do by default. Either works for our use case as long as the subtree doesn't unmount; SWR semantics is just nicer.
Persisted local edits at refetch time. If the user has dirty fields when refetch lands, does bindx overwrite them or merge? Probably should NOT silently overwrite. For our workflow use case the entity is read-only from the user (workflow-owned fields) so this is academic, but the broader contract matters.
Relationship to issue #28 (entity:persisted events never emitted). The persisted-event gap forced us to write BindxPersistRefreshBridge that watches usePersist().isPersisting for the false-edge as a proxy. With queryKey available on <Entity>, the bridge becomes simpler — but entity:persisting / entity:persisted events never emitted (silent no-op) #28 is still worth fixing for other consumers.
Workaround shipped downstream
We ship RefreshableEntity (link above) — a key={refreshKey} remount hack with the UX downsides described in detail. We will replace it with <Entity queryKey> (or accessor.$refetch()) the day this lands.
The marker on each call site will be:
// TODO [BindX] (<this-issue-url>): RefreshableEntity remounts the subtree on every workflow RPC; replace with `queryKey` prop once bindx exposes it.
Environment
@contember/bindx@0.1.37 (the version surfaced this; the gap is older)
Reproduces on contember/bindx@main as of 79dd562
No regression test attached — this is a missing API, not a behavioral bug. We're happy to send a failing test against the proposed queryKey prop once the API shape is agreed.
Summary
useEntity/<Entity>has no public way to refetch the same query without unmounting the consumer. Today the only escape iskey={...}on the component to force a remount, which throws away every piece of UI state below it (open dialogs, scroll position, undo history, local form drafts) and replaces it with a loading placeholder for the duration of the round trip.This is the missing primitive every fetch layer (React Query, SWR, Apollo, urql) ships from day one. Its absence is what makes bindx painful to use side-by-side with workflow RPC that mutates the entity behind bindx's back — and that pattern is now the dominant write path in our app.
Why this matters — the workflow interop problem
We have two mutation paths in the project:
persistAllWithFeedback()sends a batched mutation. Bindx is the writer; its in-memory state stays consistent because it did the write.X-Contember-Assume-Identity, and applies cascading side-effects. Bindx never saw any of this happen.After (2), the bindx tree is stale: the workflow flipped a
statusenum, set arespondedAttimestamp, maybe created anAuditrow. Until bindx re-reads, the UI keeps showing the pre-RPC values, status badges render with the old enum, gating conditionals (status === 'planned' && canManage) keep showing buttons that no longer apply.Our current downstream bridge is:
useEntityRefreshKey()is bumped byuseWorkflowAction'sbumpRefresh()after a successful RPC. Thekey={refreshKey}change forces a React unmount + remount, which restarts bindx'suseEntityfrom scratch — fresh fetch, fresh state.This works but the UX is terrible:
<Entity>returns<DefaultLoading />becauseresult.$status === 'loading'. The entire subtree disappears.<AlertDialog>, expanded row, in-flight bindx persist draft, undo history — gone.What we need
A way to tell bindx "re-run this query against the server, replace the data, but keep the accessor identity stable so React doesn't remount the consumer".
The minimal version exposes the existing internal
queryKeyplumbing (UseEntityOptions.queryKeyalready exists inuseEntityfor cache invalidation;useEntityListhas it too —<Entity>just doesn't take aqueryKeyprop).Or, equivalently, an imperative escape hatch on the accessor:
Either form would let us replace
<Entity key={refreshKey}>with<Entity queryKey={refreshKey}>and the entire flicker problem goes away — bindx silently re-fetches under the existing accessor, swaps the data in, React rerenders with new field values, no unmount, no loading placeholder, no scroll skip.We use
useRefreshableEntityListfor collections today and it works exactly this way (folds the refresh counter intoqueryKey), so the precedent is established.<Entity>just doesn't expose the same hook.Suggested API
Path of least resistance: pass
queryKeythroughEntityByProps.Behavior: a
queryKeychange triggersuseEntityto re-fetch under the same accessor identity. The consumer's render-prop receives the sameEntityAccessorinstance with refreshed field values. No remount.The accessor-level
$refetch()form would also be useful for imperative cases (poll, retry-after-error, "refresh" button), but the prop form alone unblocks the workflow RPC story.Open questions for maintainers:
$isLoading: trueagain, or should$statusstay'ready'with stale data until the new data lands ("stale-while-revalidate" mode)? The latter eliminates the last 20% of flicker and matches what React Query / SWR do by default. Either works for our use case as long as the subtree doesn't unmount; SWR semantics is just nicer.entity:persistedevents never emitted). The persisted-event gap forced us to writeBindxPersistRefreshBridgethat watchesusePersist().isPersistingfor the false-edge as a proxy. WithqueryKeyavailable on<Entity>, the bridge becomes simpler — but entity:persisting / entity:persisted events never emitted (silent no-op) #28 is still worth fixing for other consumers.Workaround shipped downstream
We ship
RefreshableEntity(link above) — akey={refreshKey}remount hack with the UX downsides described in detail. We will replace it with<Entity queryKey>(oraccessor.$refetch()) the day this lands.The marker on each call site will be:
// TODO [BindX] (<this-issue-url>): RefreshableEntity remounts the subtree on every workflow RPC; replace with `queryKey` prop once bindx exposes it.Environment
@contember/bindx@0.1.37(the version surfaced this; the gap is older)contember/bindx@mainas of79dd562queryKeyprop once the API shape is agreed.