Skip to content

fix(traces): stop the cold-start render loop in the trace store - #27

Merged
htcom-code merged 1 commit into
mainfrom
fix/trace-store-render-loop
Jul 27, 2026
Merged

htcom-code merged 1 commit into
mainfrom
fix/trace-store-render-loop

Conversation

@htcom-code

Copy link
Copy Markdown
Owner

What & why

Every cold start logged Maximum update depth exceeded ~50 times before any
data arrived. App passed data?.traces ?? [] into useTraceStore — the ??
fallback builds a new array on every render, and the hook's sample-mode
effect (src/hooks/use-trace-store.ts:52) lists that array in its dependencies
and calls setRows with rows derived from it. So: effect → setRows → render →
new [] → effect, until React's nested-update cap cut it off. It self-resolved
once the first snapshot (or the sample fallback at 2.5s) gave data.traces a
stable identity, which is why the UI looked fine — it just burned ~50 render
passes and filled the console at each start.

Two changes:

  • Root cause — App.tsx hands over a module-level NO_TRACES constant, so
    "no traces yet" has one stable identity.
  • Prevention — the sample-mode effect now commits passthrough only when
    the row keys actually differ (sameRows), so a future caller with an unstable
    array cannot reopen the loop. Nothing in the render path can catch this class
    of bug — tsc and oxlint both pass on the broken version.

Live mode is untouched: that effect returns early when persist is true.

Type of change

  • fix — bug fix (no new behaviour)

Correctness & conventions

  • HTTP stays in the data layer only (src/lib/api.ts); components take plain props.
  • src/lib/types.ts unchanged (no backend surface change).
  • The LIVE / SAMPLE-DATA fallback still works (UI explorable with no platform).
  • No visual change.

Verification

  • npm run lint passes (3 pre-existing only-export-components warnings, unchanged)
  • npm run build passes (tsc -b && vite build)
  • Verified in the app (npm run dev, SAMPLE data, no platform on 8080):
    console is clean apart from the expected 502 from the absent stream —
    Maximum update depth exceeded went from ~50 occurrences per load to 0
  • Clear-history still works on the sample path (the flow this change touches):
    arm + confirm empties the table to 0 shown · 0 retained, the "No traces yet"
    empty state renders, and the button disables
  • LIVE mode not exercised — no platform on :8080 in this environment. The
    changed effect is skipped entirely when persist is true, so the live
    seed/merge path is unaffected by construction.

Baseline for the counts above was an unmodified main worktree, which showed the
same error at 53 occurrences before this change.

- App passed `data?.traces ?? []`, a new array identity on every render;
  useTraceStore's sample-mode effect depends on that identity and sets
  state from it, so before the first snapshot arrived the pair looped
  until React's nested-update cap and logged "Maximum update depth
  exceeded" ~50 times per cold start
- a module-level constant fixes the identity, and the effect now commits
  only when the row keys actually change, so an unstable caller cannot
  reopen the loop

Tags: #traces #react #renderloop
Co-Authored-By: htjulia <htjulia1@gmail.com>
@htcom-code
htcom-code merged commit bf216ae into main Jul 27, 2026
2 checks passed
@htcom-code
htcom-code deleted the fix/trace-store-render-loop branch July 27, 2026 02:43
htcom-code added a commit that referenced this pull request Aug 24, 2026
- oxlint 1.79.0's react plugin flagged four sites: three were real, one
  was dead code
- module-detail-panel: the routes effect reset state when `moduleId`
  changed, so the previous module's routes stayed committed for one
  frame. The result now carries the module it belongs to and the view is
  derived from it, so a mismatch reads as "loading"
- use-trace-store: sample mode is a pure function of the traces handed
  in, so it is derived during render. That removes the setRows -> render
  -> effect cycle an unstable `liveTraces` identity used to drive (#27)
  and the `sameRows` guard that existed only to break it
- use-trace-store: the latest-rows ref is mirrored in an effect rather
  than written during render
- use-persistent-state: the latest-value ref was never read (the setter
  is already stable through its functional update), so it is gone

chore(lint): drop non-actionable virtualizer rule

- react/incompatible-library reports `useVirtualizer` returning
  functions the React Compiler cannot memoize. We do not run the
  compiler, so the two warnings had nothing behind them; the config
  records the condition to re-enable

Tags: #lint #react
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