Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: useMapsLibrary returns API object instead of boolean #26

Merged
merged 1 commit into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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