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.
5.3 KiB
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):
onMouseMovecallssetTooltip(...)on every pixel of mouse movement, triggering a GeoLayer re-render.- Each GeoLayer render creates a new
styleobject literal for every<Geography>(lines ~199–222). Becausememo()does a shallow reference comparison, it sees a "new"styleprop every time and re-renders all ~200 Geography components on every mouse-move event. - During these rapid cascading re-renders, React's batched state updates can cause
Geography's internalsetFocus(true)(fired inhandleMouseEnter) and the parent'ssetTooltipto race. When the parent re-render is processed beforeisFocusedis committed, Geography momentarily renders withisFocused = falseand picksstyle.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:
-
Create a
useMemo-based style map keyed bygeo.rsmKeythat only recomputes whencountries,selectedCountry, or the threshold props change — not on every tooltip update.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]); -
Pass the memoized style in the
.map()callback:<Geography style={styleMap[geo.rsmKey]} ... /> -
Wrap event handlers in
useCallback(onMouseEnter,onMouseMove,onMouseLeave) or move them outside the.map()so they are stable references and do not defeatmemo(). Consider passingcc,count, andcountryNamesas data attributes and reading them frome.currentTargetinside 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.