Skip to content

fix(appkit-ui): stabilize useAnalyticsQuery params reference to avoid infinite refetch#321

Open
jamesbroadhead wants to merge 2 commits intomainfrom
analytics-query-stable-params
Open

fix(appkit-ui): stabilize useAnalyticsQuery params reference to avoid infinite refetch#321
jamesbroadhead wants to merge 2 commits intomainfrom
analytics-query-stable-params

Conversation

@jamesbroadhead
Copy link
Copy Markdown
Contributor

Problem

useAnalyticsQuery(queryKey, params) invalidates its internal payload memo whenever the params object reference changes. A typical call site looks like:

const { data } = useAnalyticsQuery("revenue_by_destination", {
  limit: sql.number(10),
});

{ limit: ... } is a new object literal every render, so the memo invalidates, start re-runs, the SSE connection is torn down and reopened — and on the next render the cycle repeats. Result: infinite refetch.

The previous workaround was for every consumer to wrap params in useMemo. The LLM guide even told users to "always memoize query parameters". That's a pure API tax that every team using AppKit will eventually trip over.

Fix

Stabilize the parameters reference inside the hook using useRef + a shallow structural-equality check. If the new params are structurally equal to the previous render's, reuse the previous reference for downstream dependency comparisons. No public API change.

Analytics query params are produced by the sql.* builders and are always one-level objects keyed to primitives (string | number | boolean | null | undefined), so shallow equality is sufficient — and meaningfully cheaper than a full deep-equal.

Options considered

Option 1 (selected): deep-compare params inside the hook.

  • No API change.
  • Cost is negligible — params are tiny.
  • Structural equality is what users actually want.
  • TanStack Query / SWR ship the same approach (deep-equal of query keys / params).

Option 2: stable-stringify params and use the string as the internal cache key.

  • Also no API change.
  • Slightly more efficient for very large params.
  • But adds a JSON.stringify on every render and makes the internal cache-key indirection harder to reason about for the small-param common case.

Option 1 wins on simplicity for the common case.

Changes

  • packages/appkit-ui/src/react/hooks/use-analytics-query.ts — add shallowEqualParams + useStableParams helpers; route parameters through useStableParams before the payload memo.
  • packages/appkit-ui/src/react/hooks/__tests__/use-analytics-query.test.ts — new test file. Covers: structurally-equal fresh-literal params do NOT refetch, real param changes DO refetch, undefined params are stable, two empty {} literals are stable.
  • docs/docs/development/llm-guide.md — replace the stale "always memoize query parameters" guidance with a note that params are now reference-stabilized internally.

Test plan

  • pnpm test — all 1757 tests pass, including 4 new tests in use-analytics-query.test.ts.
  • pnpm check — Biome lint + format clean.
  • pnpm -r typecheck — clean across all packages.

This pull request and its description were written by Isaac.

useAnalyticsQuery treated `parameters` as a useMemo dep by reference. A
typical call site passes a fresh object literal every render
(`useAnalyticsQuery("k", { limit: sql.number(10) })`), which invalidated
the payload memo and re-ran `start` -> infinite refetch. The workaround
was for every consumer to wrap params in `useMemo` — a recurring footgun.

Stabilize the params reference inside the hook using a useRef + shallow
structural equality check. Analytics query params are produced by the
`sql.*` builders and are always 1-level objects keyed to primitives, so
shallow equality is sufficient and substantially cheaper than a full
deep-equal. No public API change.

Also considered: stable-stringify + cache-keying on the string. Rejected
because params are tiny, structural equality is what users actually
want, and TanStack Query / SWR ship the same approach.

Updates the LLM guide which previously told consumers to always memoize
params.

Co-authored-by: Isaac
Signed-off-by: James Broadhead <jamesbroadhead@gmail.com>
The override added DOM lib but accidentally downleveled inherited
ES2022 from the root tsconfig, breaking Object.hasOwn (used in the
shallowEqualParams helper).

Signed-off-by: James Broadhead <jamesbroadhead@gmail.com>
@jamesbroadhead jamesbroadhead changed the title useAnalyticsQuery: stabilize params reference to avoid infinite refetch fix(appkit-ui): stabilize useAnalyticsQuery params reference to avoid infinite refetch Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant