Skip to content

Commit 5653a85

Browse files
authored
Merge pull request #108 from ubilabs/feat/multiple-map-instances
Feat/multiple map instances
2 parents 353abfa + c4c8451 commit 5653a85

File tree

11 files changed

+248
-23
lines changed

11 files changed

+248
-23
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Multiple Google Maps Setup Example
2+
3+
This is an example setup to show the usage of **multiple Google Maps instances** with the Google Maps React Hooks library.
4+
5+
## Instructions
6+
7+
To run this project, clone the Google Maps React Hooks repository locally.
8+
9+
First go to the root of the repository and run
10+
11+
```shell
12+
npm install
13+
```
14+
15+
once to install all dependencies.
16+
17+
Then start this example locally with
18+
19+
```shell
20+
npm run start:multiple-maps-example
21+
```
22+
23+
**NOTE**:
24+
To see the examples it is needed to add an `.env` file with a [Google Maps API key](https://developers.google.com/maps/documentation/embed/get-api-key#:~:text=Go%20to%20the%20Google%20Maps%20Platform%20%3E%20Credentials%20page.&text=On%20the%20Credentials%20page%2C%20click,Click%20Close.) in the following format:
25+
26+
```
27+
GOOGLE_MAPS_API_KEY="<YOUR API KEY HERE>"
28+
```
29+
30+
An example can be found in `.env.example`.
31+
32+
## Output
33+
34+
The project will start at [localhost:1234](http://localhost:1234) and show multiple Google Map instances.
35+
36+
![image](https://user-images.githubusercontent.com/12370310/199680106-a523d143-f3e4-43e3-b32f-9e7049c1ac0e.png)

examples/multiple-google-maps/app.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, {FunctionComponent, useState, useCallback} from 'react';
2+
import {GoogleMapsProvider} from '@ubilabs/google-maps-react-hooks';
3+
4+
import MapCanvas from './components/map-canvas/map-canvas';
5+
6+
import {GOOGLE_MAPS_API_KEY} from '../constants';
7+
8+
import './main.module.css';
9+
10+
const basicMapOptions = {
11+
zoom: 10,
12+
disableDefaultUI: true,
13+
zoomControl: true,
14+
zoomControlOptions: {
15+
position: 3 // Right top
16+
}
17+
};
18+
19+
// The Google Maps API parameters must be the same for all `GoogleMapsProvider` components!
20+
const googleMapsAPIParameters = {
21+
googleMapsAPIKey: GOOGLE_MAPS_API_KEY,
22+
language: 'it',
23+
region: 'IT'
24+
};
25+
26+
const App: FunctionComponent<Record<string, unknown>> = () => {
27+
const [hamburgMapContainer, setHamburgMapContainer] =
28+
useState<HTMLDivElement | null>(null);
29+
const hamburgMapRef = useCallback(
30+
(node: React.SetStateAction<HTMLDivElement | null>) => {
31+
node && setHamburgMapContainer(node);
32+
},
33+
[]
34+
);
35+
const hamburgMapOptions = {
36+
...basicMapOptions,
37+
center: {lat: 53.551086, lng: 9.993682}
38+
};
39+
40+
const [munichMapContainer, setMunichMapContainer] =
41+
useState<HTMLDivElement | null>(null);
42+
const munichMapRef = useCallback(
43+
(node: React.SetStateAction<HTMLDivElement | null>) => {
44+
node && setMunichMapContainer(node);
45+
},
46+
[]
47+
);
48+
const munichMapOptions = {
49+
...basicMapOptions,
50+
center: {lat: 48.137154, lng: 11.576124}
51+
};
52+
53+
const [sanFranciscoMapContainer, setSanFranciscoMapContainer] =
54+
useState<HTMLDivElement | null>(null);
55+
const sanFranciscoMapRef = useCallback(
56+
(node: React.SetStateAction<HTMLDivElement | null>) => {
57+
node && setSanFranciscoMapContainer(node);
58+
},
59+
[]
60+
);
61+
const sanFranciscoMapOptions = {
62+
...basicMapOptions,
63+
center: {lat: 37.773972, lng: -122.431297}
64+
};
65+
66+
return (
67+
<div id="grid">
68+
<GoogleMapsProvider
69+
mapContainer={hamburgMapContainer}
70+
mapOptions={hamburgMapOptions}
71+
{...googleMapsAPIParameters}>
72+
<React.StrictMode>
73+
<MapCanvas ref={hamburgMapRef} />
74+
{/** The `useGoogleMap()` hook called inside this provider will return the map showing Hamburg. */}
75+
</React.StrictMode>
76+
</GoogleMapsProvider>
77+
78+
<GoogleMapsProvider
79+
mapContainer={munichMapContainer}
80+
mapOptions={munichMapOptions}
81+
{...googleMapsAPIParameters}>
82+
<React.StrictMode>
83+
<MapCanvas ref={munichMapRef} />
84+
{/** The `useGoogleMap()` hook called inside this provider will return the map showing Munich. */}
85+
</React.StrictMode>
86+
</GoogleMapsProvider>
87+
88+
<GoogleMapsProvider
89+
mapContainer={sanFranciscoMapContainer}
90+
mapOptions={sanFranciscoMapOptions}
91+
{...googleMapsAPIParameters}>
92+
<React.StrictMode>
93+
<MapCanvas ref={sanFranciscoMapRef} />
94+
{/** The `useGoogleMap()` hook called inside this provider will return the map showing San Francisco. */}
95+
</React.StrictMode>
96+
</GoogleMapsProvider>
97+
</div>
98+
);
99+
};
100+
101+
export default App;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.map {
2+
width: 100%;
3+
height: 100%;
4+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React, {forwardRef} from 'react';
2+
3+
import styles from './map-canvas.module.css';
4+
5+
const MapCanvas = forwardRef<HTMLDivElement, Record<string, unknown>>(
6+
(_, ref) => <div ref={ref} className={styles.map} />
7+
);
8+
9+
export default MapCanvas;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html lang="de-DE">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1.0, user-scalable=no"
8+
/>
9+
<title>Multiple Google Maps instances setup with the Google Maps React Hooks.</title>
10+
<meta
11+
name="description"
12+
content="Multiple Google Maps instances setup with the Google Maps React Hooks."
13+
/>
14+
</head>
15+
<body>
16+
<div id="app"></div>
17+
<script type="module" src="./index.tsx"></script>
18+
</body>
19+
</html>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
import {createRoot} from 'react-dom/client';
3+
4+
import App from './app';
5+
6+
const root = createRoot(document.getElementById('app')!);
7+
root.render(<App />);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
html,
2+
body,
3+
:global(#app),
4+
:global(#container) {
5+
height: 100vh;
6+
overflow: hidden;
7+
margin: 0;
8+
padding: 0;
9+
}
10+
11+
:global(#grid) {
12+
display: grid;
13+
grid-template-columns: repeat(3, calc(100% / 3));
14+
height: 100%;
15+
margin: 0;
16+
padding: 0;
17+
}

examples/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"start:example": "npm run clean-examples && cross-env $(cat .env) PARCEL_AUTOINSTALL=false parcel serve $EXAMPLE_ENTRY --dist-dir public --port 1234 --no-cache",
1111
"start:map": "EXAMPLE_ENTRY=./basic-google-map/index.html npm run start:example",
1212
"start:map-with-markers": "EXAMPLE_ENTRY=./google-map-with-markers/index.html npm run start:example",
13+
"start:multiple-maps": "EXAMPLE_ENTRY=./multiple-google-maps/index.html npm run start:example",
1314
"start:geocoding-service": "EXAMPLE_ENTRY=./geocoding-service/index.html npm run start:example",
1415
"start:places-service": "EXAMPLE_ENTRY=./places-service/index.html npm run start:example",
1516
"start:places-autocomplete-widget": "EXAMPLE_ENTRY=./places-autocomplete-widget/index.html npm run start:example",

library/docs/GoogleMapsProvider.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ interface GoogleMapsProviderProps {
2727
}
2828
```
2929

30+
**NOTE**:
31+
If you want to implement multiple maps in your application you can use multiple `GoogleMapsProvider` components to do so, but you have to pass the same Google Maps API parameters (`googleMapsAPIKey`, `libraries`, `language`, `region`, `version` and `authReferrerPolicy`) to all `GoogleMapsProvider` components.
32+
3033
- - - -
3134
__googleMapsAPIKey__ (_compulsory property_)
3235

library/src/google-maps-provider.tsx

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, {useState, useEffect, PropsWithChildren} from 'react';
22

3+
const GOOGLE_MAPS_API_URL = 'https://maps.googleapis.com/maps/api/js';
4+
35
// https://developers.google.com/maps/documentation/javascript/url-params
46
export interface GoogleMapsAPIUrlParameters {
57
googleMapsAPIKey: string;
@@ -57,18 +59,16 @@ export const GoogleMapsProvider: React.FunctionComponent<
5759
const [isLoadingAPI, setIsLoadingAPI] = useState<boolean>(true);
5860
const [map, setMap] = useState<google.maps.Map>();
5961

60-
// Load Google Maps API
62+
// Handle Google Maps API loading
6163
// eslint-disable-next-line complexity
6264
useEffect(() => {
63-
if (typeof google === 'object' && typeof google.maps === 'object') {
65+
const apiLoadingFinished = () => {
6466
setIsLoadingAPI(false);
65-
return () => {};
66-
}
67+
onLoadScript && onLoadScript();
68+
};
6769

68-
const scriptTag = document.createElement('script');
6970
const defaultLanguage = navigator.language.slice(0, 2);
7071
const defaultRegion = navigator.language.slice(3, 5);
71-
7272
/* eslint-disable camelcase */
7373
const params = new URLSearchParams({
7474
key: googleMapsAPIKey,
@@ -80,13 +80,49 @@ export const GoogleMapsProvider: React.FunctionComponent<
8080
});
8181
/* eslint-enable camelcase */
8282

83-
scriptTag.type = 'text/javascript';
84-
scriptTag.src = `https://maps.googleapis.com/maps/api/js?${params.toString()}`;
85-
scriptTag.onload = (): void => {
86-
setIsLoadingAPI(false);
87-
onLoadScript && onLoadScript();
88-
};
89-
document.getElementsByTagName('head')[0].appendChild(scriptTag);
83+
const existingScriptTag: HTMLScriptElement | null = document.querySelector(
84+
`script[src^="${GOOGLE_MAPS_API_URL}"]`
85+
);
86+
87+
// Check if Google Maps API was loaded with the passed parameters
88+
if (existingScriptTag) {
89+
const loadedURL = new URL(existingScriptTag.src);
90+
const loadedParams = loadedURL.searchParams.toString();
91+
const passedParams = params.toString();
92+
93+
if (loadedParams !== passedParams) {
94+
console.error(
95+
'The Google Maps API Parameters passed to the `GoogleMapsProvider` components do not match. The Google Maps API can only be loaded once. Please make sure to pass the same API parameters to all of your `GoogleMapsProvider` components.',
96+
'\n\nExpected parameters:',
97+
Object.fromEntries(loadedURL.searchParams),
98+
'\n\nReceived parameters:',
99+
Object.fromEntries(params)
100+
);
101+
}
102+
}
103+
104+
if (typeof google === 'object' && typeof google.maps === 'object') {
105+
// Google Maps API is already loaded
106+
apiLoadingFinished();
107+
} else if (existingScriptTag) {
108+
// Google Maps API is already loading
109+
setIsLoadingAPI(true);
110+
111+
const onload = existingScriptTag.onload;
112+
existingScriptTag.onload = event => {
113+
onload?.call(existingScriptTag, event);
114+
apiLoadingFinished();
115+
};
116+
} else {
117+
// Load Google Maps API
118+
setIsLoadingAPI(true);
119+
120+
const scriptTag = document.createElement('script');
121+
scriptTag.type = 'text/javascript';
122+
scriptTag.src = `${GOOGLE_MAPS_API_URL}?${params.toString()}`;
123+
scriptTag.onload = apiLoadingFinished;
124+
document.getElementsByTagName('head')[0].appendChild(scriptTag);
125+
}
90126

91127
// Clean up Google Maps API
92128
return () => {
@@ -138,16 +174,7 @@ export const GoogleMapsProvider: React.FunctionComponent<
138174
google.maps.event.clearInstanceListeners(newMap);
139175
}
140176
};
141-
}, [
142-
isLoadingAPI,
143-
mapContainer,
144-
googleMapsAPIKey,
145-
JSON.stringify(libraries),
146-
language,
147-
region,
148-
version,
149-
authReferrerPolicy
150-
]);
177+
}, [isLoadingAPI, mapContainer]);
151178

152179
return (
153180
<GoogleMapsContext.Provider

0 commit comments

Comments
 (0)