Skip to content

Commit

Permalink
feat: useMapsLibrary returns API object instead of boolean (#26)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: loading multiple libraries at once is no longer supported, changed the return type of useMapsLibrary.
  • Loading branch information
usefulthink authored Oct 27, 2023
1 parent bbcf78c commit a3aa4c5
Show file tree
Hide file tree
Showing 18 changed files with 329 additions and 72 deletions.
6 changes: 5 additions & 1 deletion src/components/__tests__/api-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import {act, render, screen} from '@testing-library/react';
import {initialize} from '@googlemaps/jest-mocks';
import '@testing-library/jest-dom';

// FIXME: this should no longer be needed with the next version of @googlemaps/jest-mocks
import {importLibraryMock} from '../../libraries/__mocks__/lib/import-library-mock';

import {
APILoadingStatus,
APIProvider,
APIProviderContext,
APIProviderContextValue
} from '../api-provider';
import {ApiParams} from '../../libraries/google-maps-api-loader';
import {useApiIsLoaded} from '../../hooks/api-loading-status';
import {useApiIsLoaded} from '../../hooks/use-api-is-loaded';

const apiLoadSpy = jest.fn();
const apiUnloadSpy = jest.fn();
Expand All @@ -29,6 +32,7 @@ jest.mock('../../libraries/google-maps-api-loader', () => {
class GoogleMapsApiLoader {
static async load(params: ApiParams): Promise<void> {
apiLoadSpy(params);
google.maps.importLibrary = importLibraryMock;
return new Promise(resolve => (triggerMapsApiLoaded = resolve));
}
static unload() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/__tests__/map.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ beforeEach(() => {

mockContextValue = {
importLibrary: jest.fn(),
loadedLibraries: new Set(),
loadedLibraries: {},
status: APILoadingStatus.LOADED,
mapInstances: {},
addMapInstance: jest.fn(),
Expand Down
2 changes: 1 addition & 1 deletion src/components/advanced-marker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {createPortal} from 'react-dom';
import {GoogleMapsContext} from './map';

import type {Ref, PropsWithChildren} from 'react';
import {useMapsLibrary} from '../hooks/api-loading-status';
import {useMapsLibrary} from '../hooks/use-maps-library';

export interface AdvancedMarkerContextValue {
marker: google.maps.marker.AdvancedMarkerElement;
Expand Down
62 changes: 39 additions & 23 deletions src/components/api-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
useCallback,
useEffect,
useMemo,
useReducer,
useState
} from 'react';

Expand All @@ -18,12 +19,13 @@ export enum APILoadingStatus {

const {NOT_LOADED, LOADING, LOADED, FAILED} = APILoadingStatus;

/**
* API Provider context
*/
type ImportLibraryFunction = typeof google.maps.importLibrary;
type GoogleMapsLibrary = Awaited<ReturnType<ImportLibraryFunction>>;
type LoadedLibraries = {[name: string]: GoogleMapsLibrary};

export interface APIProviderContextValue {
status: APILoadingStatus;
loadedLibraries: Set<string>;
loadedLibraries: LoadedLibraries;
importLibrary: typeof google.maps.importLibrary;
mapInstances: Record<string, google.maps.Map>;
addMapInstance: (map: google.maps.Map, id?: string) => void;
Expand Down Expand Up @@ -106,8 +108,14 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
const {onLoad, apiKey, libraries = [], ...otherApiParams} = props;

const [status, setStatus] = useState<APILoadingStatus>(NOT_LOADED);
const [loadedLibraries, setLoadedLibraries] = useState<Set<string>>(
new Set()
const [loadedLibraries, addLoadedLibrary] = useReducer(
(
loadedLibraries: LoadedLibraries,
action: {name: keyof LoadedLibraries; value: LoadedLibraries[string]}
) => {
return {...loadedLibraries, [action.name]: action.value};
},
{}
);

const librariesString = useMemo(() => libraries?.join(','), [libraries]);
Expand All @@ -116,6 +124,27 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
[otherApiParams]
);

const importLibrary: typeof google.maps.importLibrary = useCallback(
async (name: string) => {
if (loadedLibraries[name]) {
return loadedLibraries[name];
}

if (!google?.maps?.importLibrary) {
throw new Error(
'[api-provider-internal] importLibrary was called before ' +
'google.maps.importLibrary was defined.'
);
}

const res = await window.google.maps.importLibrary(name);
addLoadedLibrary({name, value: res});

return res;
},
[]
);

useEffect(
() => {
setStatus(LOADING);
Expand All @@ -129,7 +158,10 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
});

setStatus(LOADED);
setLoadedLibraries(new Set(['maps', ...libraries]));

for (const name of ['core', 'maps', ...libraries]) {
await importLibrary(name);
}

if (onLoad) {
onLoad();
Expand All @@ -144,22 +176,6 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
[apiKey, librariesString, serializedParams]
);

const importLibrary: typeof google.maps.importLibrary = useCallback(
async (name: string) => {
if (!google?.maps?.importLibrary) {
throw new Error(
'importLibrary was called before google.maps.importLibrary was defined'
);
}

const res = await window.google.maps.importLibrary(name);
setLoadedLibraries(new Set([...loadedLibraries, name]));

return res;
},
[]
);

return {
status,
loadedLibraries,
Expand Down
2 changes: 1 addition & 1 deletion src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import React, {

import {APIProviderContext, APIProviderContextValue} from './api-provider';

import {useApiIsLoaded} from '../hooks/api-loading-status';
import {useApiIsLoaded} from '../hooks/use-api-is-loaded';
import {logErrorOnce} from '../libraries/errors';

// Google Maps context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
APIProviderContext,
APIProviderContextValue
} from '../../components/api-provider';
import {useApiIsLoaded, useApiLoadingStatus} from '../api-loading-status';

import {useApiLoadingStatus} from '../use-api-loading-status';
import {useApiIsLoaded} from '../use-api-is-loaded';

let wrapper: ({children}: {children: React.ReactNode}) => JSX.Element | null;
let mockContextValue: jest.MockedObject<APIProviderContextValue>;
Expand All @@ -16,7 +18,7 @@ beforeEach(() => {

mockContextValue = {
importLibrary: jest.fn(),
loadedLibraries: new Set(),
loadedLibraries: {},
status: APILoadingStatus.LOADED,
mapInstances: {},
addMapInstance: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import '@testing-library/jest-dom';
import {renderHook} from '@testing-library/react';
import {initialize, mockInstances} from '@googlemaps/jest-mocks';

import {useMap} from '../map-instance';
import {useMap} from '../use-map';
import {
APILoadingStatus,
APIProviderContext,
Expand All @@ -28,7 +28,7 @@ beforeEach(() => {

mockContextValue = {
importLibrary: jest.fn(),
loadedLibraries: new Set(),
loadedLibraries: {},
status: APILoadingStatus.LOADED,
mapInstances: {},
addMapInstance: jest.fn(),
Expand Down
33 changes: 0 additions & 33 deletions src/hooks/api-loading-status.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/hooks/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useState, useRef, useEffect} from 'react';

import {useApiIsLoaded} from './api-loading-status';
import {useApiIsLoaded} from './use-api-is-loaded';

export interface AutocompleteProps {
inputField: HTMLInputElement | null;
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/directions-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useMemo, useEffect, useCallback} from 'react';

import {useApiIsLoaded} from './api-loading-status';
import {useMap} from './map-instance';
import {useApiIsLoaded} from './use-api-is-loaded';
import {useMap} from './use-map';
import {assertNotNull} from '../libraries/assert-not-null';

export interface DirectionsServiceHookOptions {
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/street-view-panorama.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable complexity */
import {useEffect, useState} from 'react';
import {useApiIsLoaded} from './api-loading-status';
import {useMap} from './map-instance';
import {useApiIsLoaded} from './use-api-is-loaded';
import {useMap} from './use-map';

export interface StreetViewPanoramaProps {
mapId?: string;
Expand Down
10 changes: 10 additions & 0 deletions src/hooks/use-api-is-loaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {APILoadingStatus} from '../components/api-provider';
import {useApiLoadingStatus} from './use-api-loading-status';
/**
* Hook to check if the Google Maps API is loaded
*/
export function useApiIsLoaded(): boolean {
const status = useApiLoadingStatus();

return status === APILoadingStatus.LOADED;
}
6 changes: 6 additions & 0 deletions src/hooks/use-api-loading-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {useContext} from 'react';
import {APILoadingStatus, APIProviderContext} from '../components/api-provider';

export function useApiLoadingStatus(): APILoadingStatus {
return useContext(APIProviderContext)?.status || APILoadingStatus.NOT_LOADED;
}
File renamed without changes.
40 changes: 40 additions & 0 deletions src/hooks/use-maps-library.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {useContext, useEffect} from 'react';

import {APIProviderContext} from '../components/api-provider';
import {useApiIsLoaded} from './use-api-is-loaded';

interface ApiLibraries {
core: google.maps.CoreLibrary;
maps: google.maps.MapsLibrary;
places: google.maps.PlacesLibrary;
geocoding: google.maps.GeocodingLibrary;
routes: google.maps.RoutesLibrary;
marker: google.maps.MarkerLibrary;
geometry: google.maps.GeometryLibrary;
elevation: google.maps.ElevationLibrary;
streetView: google.maps.StreetViewLibrary;
journeySharing: google.maps.JourneySharingLibrary;
drawing: google.maps.DrawingLibrary;
visualization: google.maps.VisualizationLibrary;
}

export function useMapsLibrary<
K extends keyof ApiLibraries,
V extends ApiLibraries[K]
>(name: K): V | null;

export function useMapsLibrary(name: string) {
const apiIsLoaded = useApiIsLoaded();
const ctx = useContext(APIProviderContext);

useEffect(() => {
if (!apiIsLoaded || !ctx) return;

// Trigger loading the libraries via our proxy-method.
// The returned promise is ignored, since importLibrary will update loadedLibraries
// list in the context, triggering a re-render.
void ctx.importLibrary(name);
}, [apiIsLoaded, ctx?.importLibrary]);

return ctx?.loadedLibraries[name] || null;
}
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ export * from './components/info-window';
export * from './components/map';
export * from './components/marker';
export * from './components/pin';
export * from './hooks/api-loading-status';
export * from './hooks/use-api-loading-status';
export * from './hooks/use-api-is-loaded';
export * from './hooks/use-maps-library';
export * from './hooks/use-map';
export * from './hooks/autocomplete';
export * from './hooks/directions-service';
export * from './hooks/map-instance';
export * from './hooks/street-view-panorama';

export {limitTiltRange} from './libraries/limit-tilt-range';
8 changes: 7 additions & 1 deletion src/libraries/__mocks__/google-maps-api-loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type {GoogleMapsApiLoader as ActualLoader} from '../google-maps-api-loader';

// FIXME: this should no longer be needed with the next version of @googlemaps/jest-mocks
import {importLibraryMock} from './lib/import-library-mock';

export class GoogleMapsApiLoader {
static load: typeof ActualLoader.load = jest.fn(() => Promise.resolve());
static load: typeof ActualLoader.load = jest.fn(() => {
google.maps.importLibrary = importLibraryMock;
return Promise.resolve();
});
}
Loading

0 comments on commit a3aa4c5

Please sign in to comment.