Skip to content

fix: stabilize register/unregister callbacks without React Compiler#3

Open
seanryy wants to merge 1 commit into
Boring-Software-Inc:mainfrom
seanryy:fix/stable-register-callbacks
Open

fix: stabilize register/unregister callbacks without React Compiler#3
seanryy wants to merge 1 commit into
Boring-Software-Inc:mainfrom
seanryy:fix/stable-register-callbacks

Conversation

@seanryy

@seanryy seanryy commented Jul 12, 2026

Copy link
Copy Markdown

Fixes #2.

registerSeries/unregisterSeries (chart-context.tsx) and registerVariant/unregisterVariant (polar-context.tsx) were plain closures recreated every render, while area.tsx/bar.tsx/pie.tsx/radar.tsx list them in useEffect deps. Without React Compiler the effect re-fires every render and the unregister/register setState pair loops until React throws "Maximum update depth exceeded" — see #2 for the full repro (plain Vite or Astro + @astrojs/react, no babel-plugin-react-compiler).

Changes:

  • Wrap the four callbacks in useCallback with empty deps. Each only calls its setState updater, which React guarantees stable, so [] is safe.
  • Adjust the "React Compiler memoizes everything" comments to note the exception, and add a short note at the wrapped callbacks explaining why they can't rely on the compiler.
  • Regenerated the r/ payloads with npm run build (only r/core.json changed).

No behavior change for compiler-enabled consumers — useCallback under React Compiler is a no-op memo-wise. We've been running this exact patch in a vendored copy in production (Astro 5 + React 19, no compiler) without issues.

registerSeries/unregisterSeries (chart-context.tsx) and registerVariant/
unregisterVariant (polar-context.tsx) are plain closures recreated every
render, and area/bar/pie/radar list them in their useEffect deps. With
React Compiler that's fine — it memoizes them — but in a setup without
babel-plugin-react-compiler every render re-fires the effect, and the
unregister/register setState pair triggers another render, looping until
React throws "Maximum update depth exceeded".

Wrap the four callbacks in useCallback with empty deps — each only calls
its setState updater, which React guarantees stable — so the components
work with or without the compiler. Regenerated r/ payloads.
DloomPlz added a commit to DloomPlz/dither-kit that referenced this pull request Jul 12, 2026
…t Compiler

Companion to Boring-Software-Inc#3 (which stabilizes the register/unregister callbacks):
Sparkline rebuilds rows/config on every render, and rows identity feeds
useRevision's adjust-state-during-render — which never stabilizes when
the array is fresh each pass. Memoize both against data/color.

Registry JSON regenerated via npm run build.
DloomPlz added a commit to DloomPlz/dither-kit that referenced this pull request Jul 12, 2026
…t Compiler

Companion to Boring-Software-Inc#3 (which stabilizes the register/unregister callbacks):
Sparkline rebuilds rows/config on every render, and rows identity feeds
useRevision's adjust-state-during-render — which never stabilizes when
the array is fresh each pass. Memoize both against data/color.

Registry JSON regenerated via npm run build.

@DloomPlz DloomPlz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Independently hit this exact bug adopting the kit in a production Next.js app (no React Compiler): mounting any chart entered the unregister/register effect loop and hung the tab. We applied the same fix pattern — useCallback with empty deps on all four register/unregister callbacks — and have been running it in production since; charts behave identically under the compiler-less runtime, and compiler-enabled projects are unaffected (this is what the compiler emits anyway).

Verified the diff covers both the cartesian (registerSeries/unregisterSeries) and polar (registerVariant/unregisterVariant) controllers, which is the complete set of effect-dep callbacks. One companion gap this PR doesn't cover (by design): Sparkline's derived rows/config rebuild per render and feed useRevision's adjust-state-during-render — #4 handles that piece so the two merge cleanly together and fully close #2.

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.

Infinite "Maximum update depth exceeded" loop in all charts without React Compiler

3 participants