diff --git a/src/components/advanced-marker.tsx b/src/components/advanced-marker.tsx index 21b6bba..d9afb46 100644 --- a/src/components/advanced-marker.tsx +++ b/src/components/advanced-marker.tsx @@ -200,7 +200,25 @@ export type CustomMarkerContent = HTMLDivElement | null; export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null; -function useAdvancedMarker(props: AdvancedMarkerProps) { +function useAdvancedMarker({ + children, + onClick, + className, + onMouseEnter, + onMouseLeave, + onDrag, + onDragStart, + onDragEnd, + collisionBehavior, + clickable, + draggable, + position: positionProp, + title, + zIndex, + anchorPoint, + anchorLeft, + anchorTop +}: AdvancedMarkerProps) { const [marker, setMarker] = useState(null); const [contentContainer, setContentContainer] = @@ -209,25 +227,19 @@ function useAdvancedMarker(props: AdvancedMarkerProps) { const map = useMap(); const markerLibrary = useMapsLibrary('marker'); - const { - children, - onClick, - className, - onMouseEnter, - onMouseLeave, - onDrag, - onDragStart, - onDragEnd, - collisionBehavior, - clickable, - draggable, - position, - title, - zIndex, - anchorPoint, - anchorLeft, - anchorTop - } = props; + const positionLng = positionProp?.lng; + const positionLat = positionProp?.lat; + const position = useMemo( + (): google.maps.marker.AdvancedMarkerElement['position'] => + positionLng !== undefined && positionLat !== undefined + ? { + lat: + typeof positionLat === 'function' ? positionLat() : positionLat, + lng: typeof positionLng === 'function' ? positionLng() : positionLng + } + : undefined, + [positionLat, positionLng] + ); const numChildren = Children.count(children); diff --git a/src/hooks/use-dom-event-listener.ts b/src/hooks/use-dom-event-listener.ts index 76ad42b..cfc5925 100644 --- a/src/hooks/use-dom-event-listener.ts +++ b/src/hooks/use-dom-event-listener.ts @@ -1,20 +1,29 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import {useEffect} from 'react'; +import {useEffectEvent} from './useEffectEvent'; + +const noop = () => {}; /** * Internally used to bind events to DOM nodes. * @internal */ export function useDomEventListener void>( - target?: Node | null, - name?: string, - callback?: T | null + target: Node | null | undefined, + name: string, + callback: T | null | undefined ) { + const callbackEvent = useEffectEvent(callback ?? noop); + const isCallbackDefined = !!callback; useEffect(() => { - if (!target || !name || !callback) return; + if (!target || !isCallbackDefined) return; + + // According to react 19 useEffectEvent and our ponyfill, the callback returned by useEffectEvent is NOT stable + // Thus, we need to create a stable listener callback that we can then use to removeEventListener with. + const listenerCallback: EventListener = (...args) => callbackEvent(...args); - target.addEventListener(name, callback); + target.addEventListener(name, listenerCallback); - return () => target.removeEventListener(name, callback); - }, [target, name, callback]); + return () => target.removeEventListener(name, listenerCallback); + }, [target, name, isCallbackDefined]); } diff --git a/src/hooks/use-maps-event-listener.ts b/src/hooks/use-maps-event-listener.ts index c105e0a..56121aa 100644 --- a/src/hooks/use-maps-event-listener.ts +++ b/src/hooks/use-maps-event-listener.ts @@ -1,20 +1,32 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import {useEffect} from 'react'; +import {useEffectEvent} from './useEffectEvent'; + +const noop = () => {}; /** * Internally used to bind events to Maps JavaScript API objects. * @internal */ export function useMapsEventListener void>( - target?: object | null, - name?: string, - callback?: T | null + target: object | null, + name: string, + callback: T | null | undefined ) { + const callbackEvent = useEffectEvent(callback ?? noop); + const isCallbackDefined = !!callback; useEffect(() => { - if (!target || !name || !callback) return; + if (!target || !isCallbackDefined) return; - const listener = google.maps.event.addListener(target, name, callback); + // According to react 19 useEffectEvent and our ponyfill, the callback returned by useEffectEvent is NOT stable + // Thus, it's best to create a stable listener callback to add to the event listener. + const listenerCallback = (...args: any[]) => callbackEvent(...args); + const listener = google.maps.event.addListener( + target, + name, + listenerCallback + ); return () => listener.remove(); - }, [target, name, callback]); + }, [target, name, isCallbackDefined]); } diff --git a/src/hooks/useEffectEvent.ts b/src/hooks/useEffectEvent.ts new file mode 100644 index 0000000..ad1cee0 --- /dev/null +++ b/src/hooks/useEffectEvent.ts @@ -0,0 +1,37 @@ +/** + * This file is based on https://github.com/sanity-io/use-effect-event/blob/main/src/useEffectEvent.ts + */ + +import {useRef, useInsertionEffect} from 'react'; + +function forbiddenInRender() { + throw new Error( + "A function wrapped in useEffectEvent can't be called during rendering." + ); +} + +/** + * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19. + * https://19.react.dev/learn/separating-events-from-effects#declaring-an-effect-event + * To learn more about the ponyfill itself, see: https://blog.bitsrc.io/a-look-inside-the-useevent-polyfill-from-the-new-react-docs-d1c4739e8072 + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function useEffectEvent void>( + fn: T +): T { + /** + * For both React 18 and 19 we set the ref to the forbiddenInRender function, to catch illegal calls to the function during render. + * Once the insertion effect runs, we set the ref to the actual function. + */ + const ref = useRef(forbiddenInRender as T); + + useInsertionEffect(() => { + ref.current = fn; + }, [fn]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((...args: any[]) => { + return ref.current(...args); + }) as T; +}