From 0b1d800dc5e4b9bf0b1ddb42b9fed392b23b8dae Mon Sep 17 00:00:00 2001 From: Martin Schuhfuss Date: Tue, 7 Nov 2023 23:20:23 +0100 Subject: [PATCH] feat: update map viewport when props are changed Improves handling of view props (center, zoom, heading, tilt) and introduces a new `useCallbackRef` hook --- src/components/map.tsx | 87 ++++++++++++++++++------------ src/libraries/use-callback-ref.tsx | 8 +++ 2 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 src/libraries/use-callback-ref.tsx diff --git a/src/components/map.tsx b/src/components/map.tsx index ba4539b7..22d2d694 100644 --- a/src/components/map.tsx +++ b/src/components/map.tsx @@ -2,6 +2,8 @@ import React, { CSSProperties, PropsWithChildren, + Ref, + RefCallback, useCallback, useContext, useEffect, @@ -14,6 +16,7 @@ import {APIProviderContext, APIProviderContextValue} from './api-provider'; import {useApiIsLoaded} from '../hooks/use-api-is-loaded'; import {logErrorOnce} from '../libraries/errors'; +import {useCallbackRef} from '../libraries/use-callback-ref'; // Google Maps context export interface GoogleMapsContextValue { @@ -75,6 +78,7 @@ export const Map = (props: PropsWithChildren) => { } const [map, mapRef] = useMapInstanceHandlerEffects(props, context); + useMapOptionsEffects(map, props); useDeckGLCameraUpdateEffect(map, viewState); const isViewportSet = useMemo(() => Boolean(viewport), [viewport]); @@ -117,13 +121,10 @@ Map.deckGLViewProps = true; function useMapInstanceHandlerEffects( props: MapProps, context: APIProviderContextValue -): readonly [ - map: google.maps.Map | null, - mapRef: (instance: HTMLDivElement | null) => void -] { +): readonly [map: google.maps.Map | null, containerRef: Ref] { const apiIsLoaded = useApiIsLoaded(); const [map, setMap] = useState(null); - const [container, setContainer] = useState(null); + const [container, containerRef] = useCallbackRef(); const { id, @@ -134,10 +135,6 @@ function useMapInstanceHandlerEffects( ...mapOptions } = props; - const mapRef = useCallback((node: HTMLDivElement | null) => { - setContainer(node || null); - }, []); - // create the map instance and register it in the context useEffect( () => { @@ -175,7 +172,7 @@ function useMapInstanceHandlerEffects( }, // Dependencies need to be inaccurately limited here. The cleanup function - // will remove the map-instance with all it's internal state, and we can't + // will remove the map-instance with all its internal state, and we can't // have that happening. This is only ok when the id or mapId is changed, // since this requires a new map to be created anyway. @@ -190,28 +187,6 @@ function useMapInstanceHandlerEffects( [id, container, apiIsLoaded, props.mapId] ); - // update the map options when mapOptions is changed - useEffect( - () => { - if (!map) { - return; - } - - // FIXME: for now, we have to filter all options describing the viewport - // here, since those are updated in google maps internally or using the - // viewState parameter externally. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {center, zoom, heading, tilt, mapId, ...otherOptions} = mapOptions; - - map.setOptions(otherOptions); - }, - // Not triggered when the map is changed, since in that case the - // options have already been passed to the map constructor. - - // eslint-disable-next-line react-hooks/exhaustive-deps - [mapOptions] - ); - // report an error if the same map-id is used multiple times useEffect(() => { if (!id) { @@ -229,7 +204,53 @@ function useMapInstanceHandlerEffects( } }, [id, context, map]); - return [map, mapRef] as const; + return [map, containerRef] as const; +} + +/** + * Internal hook to update the map-options and view-parameters when + * props are changed. + */ +function useMapOptionsEffects(map: google.maps.Map | null, mapProps: MapProps) { + const {center, zoom, heading, tilt, ...mapOptions} = mapProps; + + // All of these effects aren't triggered when the map is changed. + // In that case, the values have already been passed to the map constructor. + + // update the map options when mapOptions is changed + useEffect(() => { + if (!map) return; + + // NOTE: passing a mapId to setOptions triggers an error-message we don't need to see here + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {mapId, ...opts} = mapOptions; + + map.setOptions(opts); + }, [mapProps]); + + useEffect(() => { + if (!map || !center) return; + + map.setCenter(center); + }, [center]); + + useEffect(() => { + if (!map || !Number.isFinite(zoom)) return; + + map.setZoom(zoom as number); + }, [zoom]); + + useEffect(() => { + if (!map || !Number.isFinite(heading)) return; + + map.setHeading(heading as number); + }, [heading]); + + useEffect(() => { + if (!map || !Number.isFinite(tilt)) return; + + map.setTilt(tilt as number); + }, [tilt]); } /** diff --git a/src/libraries/use-callback-ref.tsx b/src/libraries/use-callback-ref.tsx new file mode 100644 index 00000000..2bfe46a7 --- /dev/null +++ b/src/libraries/use-callback-ref.tsx @@ -0,0 +1,8 @@ +import {Ref, useCallback, useState} from 'react'; + +export function useCallbackRef() { + const [el, setEl] = useState(null); + const ref = useCallback((value: T) => setEl(value), [setEl]); + + return [el, ref as Ref] as const; +}