Charts throw an infinite setState loop when they're in a subtree hidden by a Suspense boundary in React 19.
Environment:
- recharts: 3.8.1 (also 3.8.0, 3.7.0 — tested all three)
- react: 19.2.7
- react-dom: 19.2.7
React 19 introduced disappearLayoutEffects — when a Suspense boundary triggers, React runs ref cleanup (safelyDetachRef) on the hidden subtree instead of fully unmounting it. The innerRef callback in RechartsWrapper calls setTooltipPortal(node) and setLegendPortal(node) unconditionally, including when called with null during cleanup. Calling setState inside a ref cleanup during disappearLayoutEffects causes React to loop.
Stack trace (abbreviated):
Maximum update depth exceeded...
at RechartsWrapper.js:213
at safelyDetachRef (react-dom-client.development.js:13529)
at disappearLayoutEffects (react-dom-client.development.js:15198)
The offending code (RechartsWrapper.js):
var innerRef = useCallback(node => {
setScaleRef(node);
ref?.(node);
setTooltipPortal(node); // called with null during Suspense cleanup → triggers re-render
setLegendPortal(node); // same
...
}, [...]);
Fix direction:
Guard the setState calls so they only fire when node is not null, or use a ref instead of state for portal targets:
if (node !== null) {
setTooltipPortal(node);
setLegendPortal(node);
}
Workaround:
Set useSuspense: false in i18n.ts config. Works
but undesirable.
Charts throw an infinite setState loop when they're in a subtree hidden by a Suspense boundary in React 19.
Environment:
React 19 introduced disappearLayoutEffects — when a Suspense boundary triggers, React runs ref cleanup (safelyDetachRef) on the hidden subtree instead of fully unmounting it. The innerRef callback in RechartsWrapper calls setTooltipPortal(node) and setLegendPortal(node) unconditionally, including when called with null during cleanup. Calling setState inside a ref cleanup during disappearLayoutEffects causes React to loop.
Stack trace (abbreviated):
Maximum update depth exceeded...
at RechartsWrapper.js:213
at safelyDetachRef (react-dom-client.development.js:13529)
at disappearLayoutEffects (react-dom-client.development.js:15198)
The offending code (RechartsWrapper.js):
var innerRef = useCallback(node => {
setScaleRef(node);
ref?.(node);
setTooltipPortal(node); // called with null during Suspense cleanup → triggers re-render
setLegendPortal(node); // same
...
}, [...]);
Fix direction:
Guard the setState calls so they only fire when node is not null, or use a ref instead of state for portal targets:
if (node !== null) {
setTooltipPortal(node);
setLegendPortal(node);
}
Workaround:
Set useSuspense: false in i18n.ts config. Works
but undesirable.