-
Notifications
You must be signed in to change notification settings - Fork 179
refactor(AdvancedMarker): improve stability of position and event handlers #866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T extends (...args: any[]) => void>( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For both useDomEventListener and useMapsEventListener, to prevent adding and removing listeners on every render (which was the previous logic), I verify if the callback is defined and I useEffectEvent as the callback. eslint makes sure |
||
| 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]); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T extends (...args: any[]) => 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]); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<const T extends (...args: any[]) => void>( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is based on https://github.com/sanity-io/use-effect-event/blob/main/src/useEffectEvent.ts and on a ponyfill that I use at my company.
|
||
| 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; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why I added this was so that devs can still send inline positions to AdvancedMarker, e.g:
And we make sure to not set the marker to this position again (which can trigger potentially heavy calculations on google maps API side. At least to my knowledge they don't do a deep comparison to check if the previously assigned position is deeply different from the new one.