## Problem
Sentry **PHP-LARAVEL-3B** — React `Maximum update depth exceeded`
crashing `/dashboard`. Regressed, 5 occurrences / 4 users (mobile
Chrome/Android and Safari), last seen today.
## Root cause
`ChartTooltipPortal` in `resources/js/components/ui/chart.tsx`
positioned the portaled tooltip in a `useLayoutEffect` with **no
dependency array**, so it ran after every render and called `setPos`. An
equality guard was the only thing preventing a `render → effect → setPos
→ render` feedback loop, and it failed once the computed position
oscillated by a sub-pixel (fractional `getBoundingClientRect` / integer
`offsetWidth`). React then aborted after 50 nested updates, taking down
the dashboard.
## Fix
1. **`651c7d72`** — Depend on the primitive `coordinate.x/.y` (and
`offset`) so the effect runs only when the cursor moves, never as a
result of its own `setPos`. `coordinate` itself is excluded on purpose:
Recharts hands a fresh object every render, so depending on the object
would reopen the loop. Extract the flip/clamp math into a pure
`computeTooltipPosition` helper (`resources/js/lib/`) rounded to whole
pixels, and unit-test it.
2. **`1c1602db`** — Component regression test: asserts the positioning
effect does **not** re-run on a re-render with an unchanged coordinate,
and **does** on a coordinate change. Verified it fails against the
pre-fix dependency-less effect.
3. **`0354a031`** — Strengthen the helper test to assert sub-pixel
positions collapse to one pixel, and correct the doc note (the
dependency array removes the loop; rounding only narrows the
oscillation).
## Review
Two independent review agents (architecture/duplication/tests +
user-facing correctness) examined the diff:
- **Correctness**: no must-fix issues. First paint is correct (measured
at `-9999`/opacity 0 before paint, capped by a viewport-relative
`max-w`, so size is accurate and there is no flash). The loop cannot
recur through the `setHidden` effect or a repeated identical coordinate.
No mobile/edge regression — the flip/clamp logic is the extracted
original.
- **Architecture**: helper placement/naming/types match conventions; no
duplicate positioning logic elsewhere to fold in; both agents' top
request (regression-test the actual fix mechanism) is addressed by
commit 2. `ChartTooltipPortal` is exported for the test, matching how
`StackedBarShape` is exported for its own test.
## Known minor residual (acceptable)
If the tooltip's **content size** changes while the cursor stays on the
exact same point *and* it sits at a viewport edge, the flip/clamp won't
recompute until the next mouse move. In practice content size changes
together with the coordinate (Recharts hover). Worst case is a one-frame
cosmetic overflow that self-corrects — strictly better than the crash it
replaces.
## Testing
`chart-tooltip-position.test.ts` (5) + `chart-tooltip-portal.test.tsx`
(2) pass. Note: the infinite loop itself can't be reproduced in jsdom
(zero-size rects), so the component test guards the mechanism (effect
does not self-retrigger) rather than the throw.
Fixes PHP-LARAVEL-3B