Skip to content

useEntityList refetches on unstable pre-resolved selection identity (DataGrid /live request loop) #38

@jindrak02

Description

@jindrak02

Summary

useEntityList re-fetches whenever the pre-resolved selection option is given a fresh object identity, even when its content is unchanged and a stable queryKey is supplied. Because DataGrid rebuilds selection from the children render-prop on every render, a grid nested under a re-rendering store subscriber spins into an unbounded refetch loop (/live request storm, grid flickers "No results").

Environment

Reproduction

function GridLike({ selectionVersion }: { selectionVersion: number }) {
	// new (content-identical) selection object whenever selectionVersion changes
	const selection = React.useMemo(
		() => resolveSelectionMeta<Author, Author>(a => a.id().name().email()),
		[selectionVersion],
	)
	const list = useEntityList(authorDef, {
		selection,
		queryKey: 'stable-author-query-key', // <- stable, never changes
	})
	return list.$status === 'ready' ? <div>{list.length}</div> : <div>Loading…</div>
}

// 1. render with selectionVersion={0}  -> 1 backend query (correct)
// 2. rerender with selectionVersion={1} (same fields, same queryKey)
//    -> EXPECTED: still 1 query; ACTUAL: 2 queries

The failing test asserts exactly this: expect(queryCount).toBe(1)Received: 2.

Expected behavior

When the caller supplies a stable queryKey (the documented cache-invalidation key), a new-but-content-identical selection object must not trigger another backend round-trip. This already works for the definer path (3rd argument), which is explicitly stabilized — the options.selection path should behave consistently.

Actual behavior

Every fresh selection object identity re-runs the data-loading effect and issues a new query. In DataGrid this is self-sustaining: a fetch completes → store.notify() → the surrounding <Entity>/store subscriber re-renders → useDataGridSetup's collection useMemo (deps include children) rebuilds selection with a new identity → effect re-fires → fetch → … unbounded. Observed in the reporting project: thousands of /_api/content/.../live requests per minute, grid permanently flickering "No results found".

Suspected root cause

packages/bindx-react/src/hooks/useEntityList.ts:

  • The definer path is deliberately stabilized via a ref keyed on the serialized query (lines ~167–177):

    const resolvedMeta = definer ? resolveSelectionMeta(definer) : null
    const definerQueryKey = resolvedMeta ? JSON.stringify(buildQueryFromSelection(resolvedMeta)) : null
    const selectionRef = useRef<{ meta: SelectionMeta; queryKey: string } | null>(null)
    if (definerQueryKey && resolvedMeta) {
    	if (!selectionRef.current || selectionRef.current.queryKey !== definerQueryKey) {
    		selectionRef.current = { meta: resolvedMeta, queryKey: definerQueryKey }
    	}
    }
    const selectionMeta = definer ? selectionRef.current!.meta : options.selection!  // <- raw reference
  • The options.selection path is not stabilizedselectionMeta is the raw options.selection reference.

  • selectionMeta is then a dependency of the data-loading effect by reference (line ~402):

    }, [entityType, optionsKey, effectiveQueryKey, batcher, dispatcher, store, selectionMeta])

    even though a stable serialized key is already available right next to it (optionsKey, and effectiveQueryKey which prefers options.queryKey, lines ~180–195).

So the options.selection consumers (DataGrid, DataView, EntityList) get refetch-on-identity, while definer callers do not. useDataGridSetup (packages/bindx-dataview/src/useDataGridSetup.ts:103–157) makes this trivially reachable: its collection useMemo lists children in its deps, so an unstable children render-prop yields a new selection object every render, which DataGrid.tsx (~line 99–106) passes straight into useEntityList.

Suggested fix

Stabilize the options.selection path the same way the definer path already is: keep a ref keyed on options.queryKey (or the serialized selection when no queryKey is given) and reuse the previous selectionMeta when the key is unchanged. Equivalently, drop selectionMeta from the effect dependency array and rely on the already-computed stable effectiveQueryKey/optionsKey. Deferring to maintainers on which is preferable, but the asymmetry between the definer and options.selection paths looks like the core defect.

Workaround shipped downstream

We applied a temporary workaround in our project, marked TODO [BindX] (<this-issue-url>): …. The workaround memoizes the DataGrid element (and thus its children closure) so the pre-resolved selection keeps a stable identity across re-renders of the surrounding store subscriber; we will remove it once this issue is resolved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions