Skip to content

fix(status): stop the freshness watcher updating TimeRangePicker during sibling renders - #1510

Merged
kylebernhardy merged 2 commits into
stagefrom
kyle/freshness-batched-notify
Jul 15, 2026
Merged

fix(status): stop the freshness watcher updating TimeRangePicker during sibling renders#1510
kylebernhardy merged 2 commits into
stagefrom
kyle/freshness-batched-notify

Conversation

@kylebernhardy

Copy link
Copy Markdown
Member

Summary

useAnalyticsFreshness (the "Updated Xs ago" / spinning-refresh watcher behind TimeRangePicker) called setState synchronously inside its QueryCache.subscribe callback. useQuery registers new queries during render, and React Query dispatches the cache event synchronously — so any component whose render introduces a new analytics query key made React log:

Cannot update a component (TimeRangePicker) while rendering a different component (MetricPanelInner/KpiTile)

Latent on stage today (a refresh tick minting fresh window keys can trigger it); the enhancement-batch PRs (#1506 especially, whose KPI tiles mount previous-window queries) exercise it on every Health-tab visit. Caught during a runtime verification pass over the merged batch; with this fix the same browser run reports zero console errors.

Change

Reimplements the hook the way RQ's own useIsFetching bridges cache events to React 18: useSyncExternalStore with the store notification deferred through notifyManager.batchCalls, and a referentially-stable snapshot cached in a ref. Behavior of isFetching/lastFetchedAt/now is unchanged.

Where to focus

  • The deferral is the load-bearing part: a bare uSES onStoreChange still warns (it schedules a cross-fiber update mid-render all the same). The new regression test was verified to fail on the old setState implementation and on the bare-uSES variant — the initialData in the test is what makes the mid-render event value-changing, without which the old code's functional-setState bailout hides the bug.
  • getSnapshot scans the analytics-prefixed cache entries per picker render — same complexity class as RQ's useIsFetching and as the previous implementation's event handler.

Comment generated by kAIle (Claude Fable 5)

…ng sibling renders

useAnalyticsFreshness called setState synchronously from its QueryCache
subscription. useQuery registers new queries during render and RQ fires the
cache event synchronously, so any component whose render introduced a new
analytics query (a KPI tile's previous-window fetch, a MetricPanel on a fresh
refresh window) made React log 'Cannot update a component (TimeRangePicker)
while rendering a different component'.

Reimplement on useSyncExternalStore with the notification deferred through
notifyManager.batchCalls — the same bridge RQ's useIsFetching uses. A bare
uSES onStoreChange still schedules the cross-fiber update mid-render; the
regression test fails on both the old setState version and the bare-uSES
version, passes only with the deferral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 49.04% 4910 / 10011
🔵 Statements 49.45% 5231 / 10577
🔵 Functions 41% 1189 / 2900
🔵 Branches 42.25% 3235 / 7656
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/status/analytics/hooks/useAnalyticsFreshness.ts 98.14% 80.55% 100% 100% 86
Generated in workflow #1506 for commit edca15c by the Vitest Coverage Report Action

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the useAnalyticsFreshness hook to use useSyncExternalStore and notifyManager.batchCalls to prevent React warnings about mid-render updates, and adds a regression test. The reviewer suggests splitting the hook into two separate useSyncExternalStore calls for primitive values (isFetching and lastFetchedAt). This avoids mutating a ref during the render phase, which is a React anti-pattern, and simplifies the code by removing the need for the FreshnessSnapshot interface.

Comment thread src/features/instance/status/analytics/hooks/useAnalyticsFreshness.ts Outdated
Comment thread src/features/instance/status/analytics/hooks/useAnalyticsFreshness.ts Outdated
Gemini review: splitting isFetching / lastFetchedAt into separate
useSyncExternalStore calls returning primitives removes the ref-cached
object snapshot and the FreshnessSnapshot interface entirely — Object.is
handles primitive stability for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
@kylebernhardy
kylebernhardy added this pull request to the merge queue Jul 15, 2026
Merged via the queue into stage with commit 2b34cf2 Jul 15, 2026
2 checks passed
@kylebernhardy
kylebernhardy deleted the kyle/freshness-batched-notify branch July 15, 2026 15:43
dawsontoth pushed a commit that referenced this pull request Jul 22, 2026
…ry metric

Two harnesses derived from the runtime-verification passes:

- src/testSetup/failOnRenderPhaseUpdate.ts (global vitest setup): any test
  during which React logs 'Cannot update a component while rendering a
  different component' now fails with the substituted component names. This
  class shipped silently twice (Transitioner/router rebuild, and the
  analytics freshness watcher fixed in #1510) and was only ever caught by
  watching a real browser console. Narrow match; intentional console.error
  paths are unaffected. Zero offenders in the current suite.

- registry-smoke.test.tsx: renders all 24 specRegistry metrics through the
  real MetricRenderer + PanelErrorBoundary with synthetic records derived
  from each spec's required fields (quantile columns, confidence-gate-
  clearing counts, timestamp: 'id' mirroring). Two tiers: no metric may
  trip the boundary; every line/stacked-area metric (derived from
  spec.primitive, not a hardcoded list) must emit an actual chart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
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.

2 participants