fix(chart): upgrade recharts to 3.9.2 to stop the mobile dashboard render loop (PHP-LARAVEL-47) (#659)
## What & why The dashboard white-screened with React **"Maximum update depth exceeded"** on mobile (iOS 18.7 / Opera Touch), fingerprinted in Sentry as **PHP-LARAVEL-47** (`https://whisper.money/dashboard`). ### Root cause (upstream, recharts < 3.9.0) The crashing first-party frame is inside recharts' internal `CartesianChart` → `useElementOffset` (`node_modules/recharts/es6/util/useElementOffset.js`). In < 3.9.0 that hook: - memoizes its measuring **callback ref** with `useCallback(..., [lastBoundingBox.width, .height, .top, .left, ...])`, so **every `setState` changes the ref identity → React re-invokes the ref → it re-measures on each render**, and - reads **viewport-relative** `getBoundingClientRect().top/left` with a `EPS = 1px` threshold. On mobile, any mid-render viewport shift (the iOS URL bar showing/hiding on scroll) moves the box `> 1px` on each commit, feeding an unbounded `render → measure → setState` loop until React aborts at the nested-update limit and the dashboard white-screens. This is the **same crash family** as the `ChartTooltipPortal` loop fixed in #657, but a **distinct root cause inside recharts** (that first-party loop is already fixed and covered by its own test; both are present on this branch). ### The fix Upgrade **recharts 3.7.0 → 3.9.2**. 3.9.0 rewrote `useElementOffset` to drive measurement off a **`ResizeObserver` with a stable callback ref** (deps no longer include the last bounding box). `ResizeObserver` fires on element **size** changes, not on scroll/viewport-position — so the iOS URL-bar shift no longer re-triggers measurement. This removes the loop at its source; it does not merely mask it. ## Commits 1. `fix(chart): upgrade recharts to 3.9.2 to stop the dashboard render loop` — the fix (bun.lock + a code comment on `ResponsiveContainer` recording the `>= 3.9.0` requirement). 2. `fix(chart): raise recharts floor to ^3.9.0 so the crash fix can't regress` — review follow-up: `package.json` was still `^3.7.0` (permits the crashing versions); bumped to `^3.9.0` and regenerated `package-lock.json` so both `bun install --frozen-lockfile` and `npm ci` enforce the floor. Also resolves a pre-existing bun.lock/package-lock.json divergence. 3. `test(chart): guard recharts >= 3.9.0 against a downgrade regression` — asserts the installed recharts is `>= 3.9.0` (the honest test for a "depend on the patched version" fix; a jsdom behavioral repro is impossible — zero-size rects, no ResizeObserver — and would pass on the buggy version too). ## Review (two agents, before this PR) Both reviewed against the actual `node_modules/recharts@3.9.2` source, not just release notes. - **No material regression risk.** The `ChartTooltipPortal`'s two hard dependencies still hold in 3.9.2: `.recharts-wrapper` is still emitted and is still the default tooltip portal target, and `Tooltip` still passes `coordinate` as `{ x, y }`. `ResponsiveContainer`'s `initialDimension` is still supported. No API we use was removed/renamed. - **Types & tests:** `bun run types` produces zero recharts-attributable errors; the chart-related JS tests (tooltip portal/position, budget-spending-chart, stacked-bar custom-shape path) pass on 3.9.2. ### Lockfile note The production bundle is built by `bun run build` off **bun.lock** (pinned to 3.9.2). `package-lock.json` is consumed **only** by `release.yml`'s `npm ci`, which runs `release-it` (versioning/changelog/tag) and does **not** build the shipped bundle — so its large regen diff is mechanical drift-repair and cannot affect users. Follow-up worth considering (out of scope): consolidate on bun and drop the vestigial `package-lock.json`. ## Why draft (not auto-merged) The root-cause fix is high-confidence, but this is a **charting-library bump touching all chart surfaces** and the original bug is **mobile-only**, which I can't visually QA here. Please spot-check before merge: - [ ] **Mobile dashboard** (ideally iOS Safari + Opera Touch): charts render, no white-screen, scroll is smooth, tooltips position correctly. - [ ] **Animations**: recharts 3.9 rewrote the animation system — glance that Area/Bar/Line still animate acceptably on data refresh (esp. `account-balance-chart`, `budget-spending-chart`). - [ ] **New Sentry noise**: watch for `"ResizeObserver loop completed with undelivered notifications"` — that's a benign warning, not the loop returning; suppress rather than treat as a regression. Refs PHP-LARAVEL-47
This commit is contained in:
parent
5b3400909e
commit
e339aaa193
10
bun.lock
10
bun.lock
|
|
@ -51,7 +51,7 @@
|
|||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^3.7.0",
|
||||
"recharts": "^3.9.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
|
|
@ -1248,7 +1248,7 @@
|
|||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
||||
"immer": ["immer@11.1.11", "", {}, "sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
|
|
@ -1766,7 +1766,7 @@
|
|||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="],
|
||||
"recharts": ["recharts@3.9.2", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-G4fy+Pk46RaXgwWMh+Nzhyo/lbFAVqXo9gtetlyehe6Ehge9CsgDuOTwQDD+i1+llaLktNBiNq4bhnGlDRXFtw=="],
|
||||
|
||||
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
|
||||
|
||||
|
|
@ -1792,7 +1792,7 @@
|
|||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
|
||||
|
||||
"resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||
|
||||
|
|
@ -2224,6 +2224,8 @@
|
|||
|
||||
"@reduxjs/toolkit/immer": ["immer@11.0.1", "", {}, "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA=="],
|
||||
|
||||
"@reduxjs/toolkit/reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="],
|
||||
|
||||
"@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -91,7 +91,7 @@
|
|||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^3.7.0",
|
||||
"recharts": "^3.9.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue