Skip to content

Commit

Permalink
feat: restore map state when changing mapId (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
usefulthink committed Feb 15, 2024
1 parent 01d8658 commit 0db363f
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 39 deletions.
31 changes: 4 additions & 27 deletions examples/change-map-styles/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import React, {useState} from 'react';
import {createRoot} from 'react-dom/client';

import {
Marker,
APIProvider,
InfoWindow,
Map,
useMarkerRef
} from '@vis.gl/react-google-maps';
import {APIProvider, Map} from '@vis.gl/react-google-maps';

import ControlPanel from './control-panel';

Expand Down Expand Up @@ -93,35 +87,18 @@ const API_KEY =

const App = () => {
const [mapConfig, setMapConfig] = useState<MapConfig>(MAP_CONFIGS[0]);
const [infowindowOpen, setInfowindowOpen] = useState(true);
const [markerRef, marker] = useMarkerRef();

return (
<APIProvider apiKey={API_KEY}>
<Map
defaultCenter={{lat: 22, lng: 0}}
defaultZoom={3}
mapId={mapConfig.mapId}
mapId={mapConfig.mapId || null}
mapTypeId={mapConfig.mapTypeId}
styles={mapConfig.styles}
gestureHandling={'greedy'}
disableDefaultUI={true}>
<Marker
ref={markerRef}
onClick={() => setInfowindowOpen(true)}
position={{lat: 28, lng: -82}}
/>

{infowindowOpen && (
<InfoWindow
anchor={marker}
maxWidth={200}
onCloseClick={() => setInfowindowOpen(false)}>
This marker is here to show that marker and infowindow persist when
changing the mapId.
</InfoWindow>
)}
</Map>
disableDefaultUI={true}
/>

<ControlPanel
mapConfigs={MAP_CONFIGS}
Expand Down
4 changes: 2 additions & 2 deletions examples/change-map-styles/src/control-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ function ControlPanel({
</p>
<p>
Due to the way the Maps API works, a new <code>google.maps.Map</code>{' '}
instance has to be created when the mapId is changed, which could affect
the number of paid map-views.
instance has to be created when changing the mapId. This will affect the
number of paid map-views.
</p>

<div>
Expand Down
7 changes: 5 additions & 2 deletions src/components/info-window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ export const InfoWindow = (props: PropsWithChildren<InfoWindowProps>) => {

setContentContainer(null);
};
// We don't want to re-render a whole new infowindow

// `infoWindowOptions` is missing from dependencies:
//
// we don't want to re-render a whole new infowindow
// when the options change to prevent flickering.
// Update of infoWindow options is handled in the useEffect below.
// Excluding infoWindowOptions from dependency array on purpose here.
//
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, children]);

Expand Down
4 changes: 1 addition & 3 deletions src/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {APIProviderContext} from '../api-provider';

import {MapEventProps, useMapEvents} from './use-map-events';
import {useMapOptions} from './use-map-options';
import {useTrackedCameraStateRef} from './use-tracked-camera-state-ref';
import {useApiLoadingStatus} from '../../hooks/use-api-loading-status';
import {APILoadingStatus} from '../../libraries/api-loading-status';
import {
Expand Down Expand Up @@ -89,8 +88,7 @@ export const Map = (props: PropsWithChildren<MapProps>) => {
);
}

const [map, mapRef] = useMapInstance(props, context);
const cameraStateRef = useTrackedCameraStateRef(map);
const [map, mapRef, cameraStateRef] = useMapInstance(props, context);

useMapCameraParams(map, cameraStateRef, props);
useMapEvents(map, props);
Expand Down
41 changes: 36 additions & 5 deletions src/components/map/use-map-instance.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import {Ref, useEffect, useState} from 'react';
import {Ref, useEffect, useRef, useState} from 'react';

import {MapProps} from '../map';
import {APIProviderContextValue} from '../api-provider';

import {useCallbackRef} from '../../libraries/use-callback-ref';
import {useApiIsLoaded} from '../../hooks/use-api-is-loaded';
import {
CameraState,
CameraStateRef,
useTrackedCameraStateRef
} from './use-tracked-camera-state-ref';

/**
* The main hook takes care of creating map-instances and registering them in
Expand All @@ -16,11 +21,17 @@ import {useApiIsLoaded} from '../../hooks/use-api-is-loaded';
export function useMapInstance(
props: MapProps,
context: APIProviderContextValue
): readonly [map: google.maps.Map | null, containerRef: Ref<HTMLDivElement>] {
): readonly [
map: google.maps.Map | null,
containerRef: Ref<HTMLDivElement>,
cameraStateRef: CameraStateRef
] {
const apiIsLoaded = useApiIsLoaded();
const [map, setMap] = useState<google.maps.Map | null>(null);
const [container, containerRef] = useCallbackRef<HTMLDivElement>();

const cameraStateRef = useTrackedCameraStateRef(map);

const {
id,
defaultBounds,
Expand All @@ -41,12 +52,21 @@ export function useMapInstance(
if (!mapOptions.tilt && Number.isFinite(defaultTilt))
mapOptions.tilt = defaultTilt;

for (const key of Object.keys(mapOptions) as (keyof typeof mapOptions)[])
if (mapOptions[key] === undefined) delete mapOptions[key];

const savedMapStateRef = useRef<{
mapId?: string | null;
cameraState: CameraState;
}>();

// create the map instance and register it in the context
useEffect(
() => {
if (!container || !apiIsLoaded) return;

const {addMapInstance, removeMapInstance} = context;
const mapId = props.mapId;
const newMap = new google.maps.Map(container, mapOptions);

setMap(newMap);
Expand All @@ -56,10 +76,21 @@ export function useMapInstance(
newMap.fitBounds(defaultBounds);
}

// FIXME: When the mapId is changed, we need to maintain the current camera params.
// the savedMapState is used to restore the camera parameters when the mapId is changed
if (savedMapStateRef.current) {
const {mapId: savedMapId, cameraState: savedCameraState} =
savedMapStateRef.current;
if (savedMapId !== mapId) {
newMap.setOptions(savedCameraState);
}
}

return () => {
if (!container || !apiIsLoaded) return;
savedMapStateRef.current = {
mapId,
// eslint-disable-next-line react-hooks/exhaustive-deps
cameraState: cameraStateRef.current
};

// remove all event-listeners to minimize memory-leaks
google.maps.event.clearInstanceListeners(newMap);
Expand All @@ -77,5 +108,5 @@ export function useMapInstance(
[container, apiIsLoaded, id, props.mapId]
);

return [map, containerRef] as const;
return [map, containerRef, cameraStateRef] as const;
}

0 comments on commit 0db363f

Please sign in to comment.