[Enhancement] Consider using `useSyncExternalStore` where applicable
useSyncExternalStore is a powerful hook that allows you to connect external stores to React components in an idiomatic way. This would be particularly beneficial for synchronizing with the MapLibre GL map instance state, making the code more declarative and potentially solving tearing with React concurrent rendering.
Example Use Case
Current <CompassButton />: Imperative DOM manipulation with useEffect
function CompassButton({ onClick }: { onClick: () => void }) {
const { isLoaded, map } = useMap();
const compassRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!isLoaded || !map || !compassRef.current) return;
const compass = compassRef.current;
const updateRotation = () => {
const bearing = map.getBearing();
const pitch = map.getPitch();
// Manipulating transform style using ref, not so idiomatic React.
compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`;
};
map.on("rotate", updateRotation);
map.on("pitch", updateRotation);
updateRotation();
return () => {
map.off("rotate", updateRotation);
map.off("pitch", updateRotation);
};
}, [isLoaded, map]);
return (
<ControlButton onClick={onClick} label="Reset bearing to north">
<svg
ref={compassRef}
viewBox="0 0 24 24"
className="size-5 transition-transform duration-200"
style={{ transformStyle: "preserve-3d" }}
>
<path d="M12 2L16 12H12V2Z" className="fill-red-500" />
<path d="M12 2L8 12H12V2Z" className="fill-red-300" />
<path d="M12 22L16 12H12V22Z" className="fill-muted-foreground/60" />
<path d="M12 22L8 12H12V22Z" className="fill-muted-foreground/30" />
</svg>
</ControlButton>
);
}Proposed <CompassButton />: Declarative state synchronization with useSyncExternalStore
function CompassButton({ onClick }: { onClick: () => void }) {
const { map } = useMap();
const bearing = useSyncExternalStore(
(onStoreChange) => {
map?.on("pitch", onStoreChange);
map?.on("rotate", onStoreChange);
return () => {
map?.off("pitch", onStoreChange);
map?.off("rotate", onStoreChange);
};
},
() => map?.getBearing() ?? 0,
() => map?.getBearing() ?? 0 // Server snapshot, map will be undefined and it will fallback to 0.
);
const pitch = useSyncExternalStore(
(onStoreChange) => {
map?.on("pitch", onStoreChange);
return () => {
map?.off("pitch", onStoreChange);
};
},
() => map?.getPitch() ?? 0,
() => map?.getPitch() ?? 0 // Server snapshot, map will be undefined and it will fallback to 0.
);
return (
<ControlButton onClick={onClick} label="Reset bearing to north">
<svg
viewBox="0 0 24 24"
className="size-5 transition-transform duration-200 transform-3d"
style={{
// ✅ Idiomatic React, declarative style.
transform: `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`,
}}
>
<path d="M12 2L16 12H12V2Z" className="fill-red-500" />
<path d="M12 2L8 12H12V2Z" className="fill-red-300" />
<path d="M12 22L16 12H12V22Z" className="fill-muted-foreground/60" />
<path d="M12 22L8 12H12V22Z" className="fill-muted-foreground/30" />
</svg>
</ControlButton>
);
}[!IMPORTANT]
I did not wrap my functions inuseCallbackbecause I had React compiler enabled in my project which automatically memoized the functions for me.
Additional Context
The compass button is just one example where useSyncExternalStore could be leveraged for a more idiomatic React integration. I haven't scanned the entire codebase to identify all potential use cases, but this hook is particularly useful for:
- Any component that needs to read values from the map instance and stay in sync with map events (bounds, center, zoom level, etc.)
- Components that currently use
useEffectto subscribe to map events and update local state
Since we're already supporting React 19, useSyncExternalStore is available without any additional dependencies. It's the recommended React pattern for integrating external stores, especially useful for preventing potential tearing issues in concurrent rendering scenarios.
Should you come across similar scenarios where components need to remain synchronized with external state, useSyncExternalStore might be worth considering! If you agree with this approach, I'd be happy to open a PR to enhance this. Thanks for the great work as always!
Source: AnmolSaini16/mapcn