Files
BanGUI/Docs/Tasks.md
Lukas bfe0daf754 Fix WorldMap hover highlight by memoizing style objects and handlers
Memoize per-Geography style objects with useMemo so React.memo can
skip re-renders when only the tooltip position changes. Stabilize
mouse event handlers with useCallback using data-* attributes instead
of per-Geography closures. This eliminates the state-update race
condition that caused hover fill colors to flash back to defaults.
2026-04-01 14:53:38 +02:00

87 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BanGUI — Task List
This document breaks the entire BanGUI project into development stages, ordered so that each stage builds on the previous one. Every task is described in prose with enough detail for a developer to begin work. References point to the relevant documentation.
Reference: `Docs/Refactoring.md` for full analysis of each issue.
---
## Open Issues
### ~~WorldMap hover highlight: fill color sometimes does not change~~ ✅ Done
**Status:** Completed.
**Summary:** Memoized Geography style objects via `useMemo` keyed by `geographies`, `countries`, `selectedCountry`, and threshold props. Stabilized `onMouseEnter`, `onMouseMove`, and `onMouseLeave` handlers with `useCallback` using `data-*` attributes to avoid per-Geography closures. This ensures `React.memo(Geography)` correctly skips re-renders when only tooltip position changes, eliminating the state-update race that caused hover fills to flash back to defaults.
**Symptom:** On mouse-over, a country's stroke correctly becomes bold (`strokeWidth: 1`), but the background fill colour sometimes stays at the default value instead of switching to the hover colour.
**Root cause — `React.memo` is defeated by new style objects on every render:**
`Geography.js` (`react-simple-maps-main/src/components/Geography.js`) is wrapped in `memo()`. It keeps an internal `isFocused` state that toggles between `style.default` and `style.hover`. This works correctly in isolation.
However, in `WorldMap.tsx` (`frontend/src/components/WorldMap.tsx`):
1. **`onMouseMove`** calls `setTooltip(...)` on every pixel of mouse movement, triggering a GeoLayer re-render.
2. Each GeoLayer render creates a **new `style` object literal** for every `<Geography>` (lines ~199222). Because `memo()` does a shallow reference comparison, it sees a "new" `style` prop every time and **re-renders all ~200 Geography components** on every mouse-move event.
3. During these rapid cascading re-renders, React's batched state updates can cause `Geography`'s internal `setFocus(true)` (fired in `handleMouseEnter`) and the parent's `setTooltip` to race. When the parent re-render is processed before `isFocused` is committed, Geography momentarily renders with `isFocused = false` and picks `style.default` — the fill stays at the default colour while the stroke (which changes less dramatically) appears correct.
A secondary contributing factor: for countries with `count === 0` that are not selected, `style.hover.fill` equals `style.default.fill` (both resolve to `fillColor`), so there is intentionally no visible fill change on hover for those countries. This may overlap with the perceived bug if the user expects all countries to highlight.
**Fix — memoize the style objects so `memo()` can skip re-renders:**
In `WorldMap.tsx`, compute each Geography's style object with `useMemo` (or move it outside the `.map()` callback with stable references). Concretely:
1. **Create a `useMemo`-based style map** keyed by `geo.rsmKey` that only recomputes when `countries`, `selectedCountry`, or the threshold props change — not on every tooltip update.
```tsx
const styleMap = useMemo(() => {
const map: Record<string, { default: CSSProperties; hover: CSSProperties; pressed: CSSProperties }> = {};
for (const geo of geographies as { rsmKey: string; id: string | number }[]) {
const numericId = String(geo.id);
const cc = ISO_NUMERIC_TO_ALPHA2[numericId] ?? null;
const count = cc !== null ? (countries[cc] ?? 0) : 0;
const isSelected = cc !== null && selectedCountry === cc;
const fillColor = getBanCountColor(count, thresholdLow, thresholdMedium, thresholdHigh);
map[geo.rsmKey] = {
default: {
fill: isSelected ? tokens.colorBrandBackground : fillColor,
stroke: tokens.colorNeutralStroke2,
strokeWidth: 0.75,
outline: "none",
},
hover: {
fill: isSelected
? tokens.colorBrandBackgroundHover
: cc && count > 0
? tokens.colorNeutralBackground3
: fillColor,
stroke: tokens.colorNeutralStroke1,
strokeWidth: 1,
outline: "none",
},
pressed: {
fill: cc ? tokens.colorBrandBackgroundPressed : fillColor,
stroke: tokens.colorBrandStroke1,
strokeWidth: 1,
outline: "none",
},
};
}
return map;
}, [geographies, countries, selectedCountry, thresholdLow, thresholdMedium, thresholdHigh]);
```
2. **Pass the memoized style** in the `.map()` callback:
```tsx
<Geography style={styleMap[geo.rsmKey]} ... />
```
3. **Wrap event handlers in `useCallback`** (`onMouseEnter`, `onMouseMove`, `onMouseLeave`) or move them outside the `.map()` so they are stable references and do not defeat `memo()`. Consider passing `cc`, `count`, and `countryNames` as data attributes and reading them from `e.currentTarget` inside a single shared handler.
With stable `style` references, `memo(Geography)` will correctly skip re-renders for countries whose data has not changed, eliminating the state-update race condition and the unnecessary rendering of ~200 SVG paths on every mouse-move event.
**Files to change:**
- `frontend/src/components/WorldMap.tsx` — memoize style objects and stabilize event handlers.
- No changes needed in `react-simple-maps-main/src/components/Geography.js`.