Skip to content

Commit

Permalink
feat: update map viewport when props are changed
Browse files Browse the repository at this point in the history
Improves handling of view props (center, zoom, heading, tilt) and introduces a new `useCallbackRef` hook
  • Loading branch information
usefulthink committed Nov 9, 2023
1 parent 4d1dbde commit 0b1d800
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 33 deletions.
87 changes: 54 additions & 33 deletions src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import React, {
CSSProperties,
PropsWithChildren,
Ref,
RefCallback,
useCallback,
useContext,
useEffect,
Expand All @@ -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 {
Expand Down Expand Up @@ -75,6 +78,7 @@ export const Map = (props: PropsWithChildren<MapProps>) => {
}

const [map, mapRef] = useMapInstanceHandlerEffects(props, context);
useMapOptionsEffects(map, props);
useDeckGLCameraUpdateEffect(map, viewState);

const isViewportSet = useMemo(() => Boolean(viewport), [viewport]);
Expand Down Expand Up @@ -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<HTMLDivElement>] {
const apiIsLoaded = useApiIsLoaded();
const [map, setMap] = useState<google.maps.Map | null>(null);
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [container, containerRef] = useCallbackRef<HTMLDivElement>();

const {
id,
Expand All @@ -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(
() => {
Expand Down Expand Up @@ -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.

Expand All @@ -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) {
Expand All @@ -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]);
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/libraries/use-callback-ref.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Ref, useCallback, useState} from 'react';

export function useCallbackRef<T>() {
const [el, setEl] = useState<T | null>(null);
const ref = useCallback((value: T) => setEl(value), [setEl]);

return [el, ref as Ref<T>] as const;
}

0 comments on commit 0b1d800

Please sign in to comment.