Search code examples
reactjsreact-hooksgoogle-maps-api-3qwikgoogle-maps-autocomplete

Best approach to create a GoogleMapsHook for loading the Google Maps API script and accessing functionalities like autocomplete and map in React/Qwik?


I'm working on a Qwik project where I need to integrate Google Maps and its functionalities, such as autocomplete and map rendering, into my application. I want to create a reusable hook that loads the Google Maps API script once and provides components with these functionalities.

What would be the best approach to implement such a hook? Specifically, I'm looking for guidance on how to structure the hook, load the API script efficiently, and provide access to components with autocomplete and map functionalities.

Here's what I have in mind for the desired usage of the hook:

import GoogleMapsHook from './GoogleMapsHook';

function MyComponent() {
  const { mapComponent, autocompleteComponent } = GoogleMapsHook();

  return (
    <div>
      {mapComponent}
      {autocompleteComponent}
    </div>
  );
}

In the above code, I'm expecting the GoogleMapsHook to handle loading the Google Maps API script once, and then provide mapComponent and autocompleteComponent that I can render in my MyComponent.

What would be the ideal structure of the GoogleMapsHook? How can I ensure that the API script is loaded only once and subsequent components using the hook can access the loaded script and its functionalities seamlessly? Any suggestions or code examples demonstrating the implementation would be greatly appreciated.

Thanks in advance!

The first thing I think it would be the cleanest option was to create a hook that every time is called, loads the script (if its not loaded yet), initialize Map and Autocomplete instances and export a MapComponent and an AutocompleteComponent calling the hook, but it breaks the hook rules.

import {
  useStore,
  useVisibleTask$,
  useSignal,
  useTask$,
} from "@builder.io/qwik";
  
export function useGoogleMaps(options: any = {}) {
  const loaded = useSignal(false);
  const map = useSignal();
  const mapRef = useSignal<Element>();
  const autocomplete = useSignal()
  const autocompleteRef = useSignal<Element>();
  
  useVisibleTask$(async () => {
  if (!window.google || !window.google.maps) {
    loaded.value = false;
    const script = document.createElement("script");
    script.src = `https://maps.googleapis.com/maps/api/js?key=${import.meta.env.PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places&maps`;
    script.async = true;
    document.head.appendChild(script);
    loaded.value = true;
  } else {
    loaded.value = true;
  }
  });

  useTask$(async ({ track }) => { 
    track(loaded);
    if (loaded.value) {
      map.value = new window.google.maps.Map(
        mapRef.value,
        {
          center: { lat: -33.8688, lng: 151.2195 },
          zoom: 13,
        }
      );
      autocomplete.value = new window.google.maps.places.Autocomplete(
        autocompleteRef.value,
        options
      );
    }
  });
  
  return { map, autocomplete, mapRef, autocompleteRef };
}

const AutocompleteComponent = () => {
  const { autocompleteRef } = useGoogleMaps();
  return <input ref={autocompleteRef} type="text" 
            class="rounded-l-full w-full py-4 px-6 text-gray-700 leading-tight focus:outline-none"
            placeholder="Enter a location"
  />;
};

const MapComponent = () => {
  const { mapRef } = useGoogleMaps();
  return <div ref={mapRef} />;
}


export { AutocompleteComponent, MapComponent}

Solution

  • Here is an example I created a global context to store Google Maps Object and a hook to attach the map to the dom.

    import { type Signal, useContext, useVisibleTask$ } from '@builder.io/qwik';
    import { AppContext } from '~/routes/layout';
    
    export function useGoogleMaps(
        elementRefSignal: Signal<Element | undefined>
    ) {
        const appStore = useContext(AppContext);
    
        useVisibleTask$(async () => {
            new appStore.googleMaps.maps.Map(elementRefSignal.value!, {
                center: { lat: -34.397, lng: 150.644 },
                zoom: 8,
            });
        });
    }