fix(CartesianAxis): skip renderedTicks dispatch when tick values are unchanged#7565
Conversation
…unchanged RenderedTicksReporter dispatched setRenderedTicks on every render because the ticks array gets a new identity each render, and its effect cleanup also dispatched removeRenderedTicks on every deps change. Under rapid external updates (e.g. a slider driving chart data), these nested store dispatches from passive effects can trip React's maximum update depth. Bail out before dispatching when the tick items are value-equal to the last dispatched ones, and only dispatch removeRenderedTicks on unmount. Fixes recharts#7563
…redTicks dispatches Each affected test spied on a selector whose consumer re-rendered once per redundant setRenderedTicks dispatch. With the dispatch skipped when tick values are unchanged, every chart mounts with one fewer store update, so the expected spy call counts decrease by one. Asserted values are unchanged.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Walkthrough
ChangesRendered tick stability
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Chart
participant RenderedTicksReporter
participant ReduxStore
participant TickSelector
Chart->>RenderedTicksReporter: rerender with tick payload
RenderedTicksReporter->>RenderedTicksReporter: normalize and compare payload
RenderedTicksReporter->>ReduxStore: dispatch only when payload differs
ReduxStore->>TickSelector: provide rendered ticks
RenderedTicksReporter->>ReduxStore: remove ticks on axis cleanup
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/cartesian/CartesianAxis.tsx (1)
322-339: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick winInclude axis identity in the ref to prevent skipped dispatches on axis change.
If
axisIdoraxisTypechanges for this component instance, but the incomingtickshappen to be deeply equal to the previously dispatched ticks of the old axis, the effect will incorrectly return early and skip dispatchingsetRenderedTicksfor the new axis.To fix this, include
axisIdandaxisTypein the ref so you only skip the dispatch when both the axis identity and the tick contents are unchanged. This snippet strictly usesunknowninstead ofanyto comply with coding guidelines.🐛 Proposed fix to include axis identity in the ref
- const lastDispatchedTicksRef = useRef<ReadonlyArray<TickItemType> | null>(null); + const lastDispatchedTicksRef = useRef<{ + ticks: Array<{ + value: unknown; + coordinate: number; + offset?: number; + index: number; + }>; + axisId: AxisId; + axisType: 'xAxis' | 'yAxis'; + } | null>(null); useEffect(() => { if (axisId == null || axisType == null) { return; } // Filter out irrelevant internal properties before exposing externally const tickItems = ticks.map(tick => ({ value: tick.value, coordinate: tick.coordinate, offset: tick.offset, index: tick.index, })); - if (isEqual(lastDispatchedTicksRef.current, tickItems)) { + if ( + lastDispatchedTicksRef.current?.axisId === axisId && + lastDispatchedTicksRef.current?.axisType === axisType && + isEqual(lastDispatchedTicksRef.current?.ticks, tickItems) + ) { return; } - lastDispatchedTicksRef.current = tickItems; + lastDispatchedTicksRef.current = { ticks: tickItems, axisId, axisType }; dispatch(setRenderedTicks({ ticks: tickItems, axisId, axisType })); }, [dispatch, ticks, axisId, axisType]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/cartesian/CartesianAxis.tsx` around lines 322 - 339, Update the lastDispatchedTicksRef comparison in the useEffect to store and compare axisId and axisType alongside the filtered tickItems, using unknown rather than any for the ref’s stored shape. Skip setRenderedTicks only when both axis identity and tick contents are unchanged; dispatch and refresh the ref whenever either axis identity or ticks differ.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/cartesian/CartesianAxis.tsx`:
- Around line 322-339: Update the lastDispatchedTicksRef comparison in the
useEffect to store and compare axisId and axisType alongside the filtered
tickItems, using unknown rather than any for the ref’s stored shape. Skip
setRenderedTicks only when both axis identity and tick contents are unchanged;
dispatch and refresh the ref whenever either axis identity or ticks differ.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1444a1f1-d074-4394-90c3-a03bf294ff85
📒 Files selected for processing (12)
src/cartesian/CartesianAxis.tsxtest/cartesian/ErrorBar.spec.tsxtest/cartesian/ReferenceDot.spec.tsxtest/cartesian/XAxis/XAxis.state.spec.tsxtest/chart/ScatterChart.spec.tsxtest/component/Tooltip/Tooltip.sync.spec.tsxtest/component/Tooltip/Tooltip.visibility.spec.tsxtest/component/Tooltip/itemSorter.spec.tsxtest/state/selectors/areaSelectors.spec.tsxtest/state/selectors/axisSelectors.spec.tsxtest/state/selectors/lineSelectors.spec.tsxtest/state/selectors/selectIsTooltipActive.spec.tsx
If axisId or axisType changes while the tick contents stay deeply equal, the dispatch must not be skipped, because the store keys rendered ticks by axis identity and the new axis entry would otherwise stay stale. Co-Authored-By: Claude Fable 5 <[email protected]>
|
Fixed in b7c0206. The ref now stores axisId and axisType next to the tick contents and only skips the dispatch when all three are unchanged. |
Bundle ReportChanges will increase total bundle size by 5.36kB (0.09%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: recharts/bundle-cjsAssets Changed:
view changes for bundle: recharts/bundle-umdAssets Changed:
view changes for bundle: recharts/bundle-es6Assets Changed:
view changes for bundle: recharts/bundle-treeshaking-cartesianAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7565 +/- ##
=======================================
Coverage 88.20% 88.21%
=======================================
Files 615 615
Lines 14276 14284 +8
Branches 3591 3593 +2
=======================================
+ Hits 12592 12600 +8
Misses 1494 1494
Partials 190 190 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Description
RenderedTicksReporterdispatchedsetRenderedTicksfrom its passive effect on every render, because theticksarray it receives is rebuilt by a baregetTicks()call in render and so gets a new identity each time even when its contents are unchanged. On top of that, sinceticksis in the effect's dependency array, every re-render also ran the effect cleanup, dispatchingremoveRenderedTicks— two store dispatches per axis per render.This PR makes the reporter value-compare the outgoing tick items against the last dispatched ones (via a ref +
isEqual) and skip the dispatch when nothing changed, and moves theremoveRenderedTickscleanup into its own effect so it only fires on actual unmount.Related Issue
Fixes #7563
Motivation and Context
As described in #7563: when an app re-renders a chart rapidly (e.g. a range slider driving the chart's
dataeach frame), every render triggered nested store dispatches from passive effects. Interleaved with the app's ownsetStatecalls, this trips React's maximum update depth limit (RenderedTicksReporter.useEffect → dispatch → notifyNestedSubs → forceStoreRerenderduringcommitPassiveUnmountOnFiber) — a hard crash at real human drag speed in dev, and reproducible in production builds under higher-rate bursts. Even fully identity-stabilized apps still hit the dev-only warning floor because every legitimate data change was followed by a nested store dispatch.With this change, data-driven re-renders with unchanged tick output complete without any nested store dispatch.
How Has This Been Tested?
test/cartesian/XAxis/XAxis.state.spec.tsxasserting the rendered-ticks state stays referentially stable across a re-render with unchanged tick values. It fails against the previous implementation and passes with the fix.Types of changes
Checklist:
Summary by CodeRabbit
Performance Improvements
Tests