3

I'm making a ReactJS application using the google maps library & I'm trying to abstract most map logic to a service, so that I could exchange the google maps library with leaflet if I wanted to in the future.

The way it works now is that I have a component loading the map library and attaching it to a div. The onLoad callback will set the map in this service, so that I can keep a reference to it.

onMapLoad={(map: Map) => {
    mapService.setMap(map);
    props.mapLoaded();
}}

The service also needs to have access to my redux store to dispatch actions when for example a certain marker on the map is selected. I'm setting this while bootstrapping the application in the index.tsx file

const store = getStore();
mapService.setStore(store);

The service itself is a singleton, but I'm wondering if there's a better pattern to use with React or just in general. I'll post a shorter version of the service, with some methods left out for brevity. Does anyone have a pattern advice that might improve this practice?

interface MapService {
    predictPlaces: (query: string) => Observable<AutocompletePrediction[]>;
    setStore: (store: Store<StoreState>) => void;
    addMarkerToMap: (place: Place) => void;
    centerMapAroundSuggestion: (suggestion: Suggestion) => void;
    setMap: (newMap: Map) => void;
}

let predictService: google.maps.places.AutocompleteService;
let geocoder: google.maps.Geocoder;
let map: Map;
let store: Store;
let markers: google.maps.Marker[] = [];

const setMap = (newMap: Map) => {
    map = newMap;
}

const setStore = (store: Store) => {
    store = Store;
}

const centerMapAroundSuggestion = (suggestion: Suggestion) => {
    if (!map) {
        throwGoogleMapsNotLoaded();
    }
    if (!geocoder) {
        geocoder = new google.maps.Geocoder();
    }
    ... further implementation ...
}

const predictPlaces = (query: string): Observable<AutocompletePrediction[]> => {
    if (!map) {
        return of([]);
    }
    if (!predictService) {
        predictService = new google.maps.places.AutocompleteService();
    }
    ... further implementation ...
}

const addMarkerToMap = (place: Place, onSelect: () => void) => {
    const marker = createMarker(place, onSelect);
    markers.push(marker);
}

const createMarker = (place: Place): Marker => {
    if (!map) {
        throwGoogleMapsNotLoaded();
    }
    const marker = new google.maps.Marker({
        ...options...
    });
    marker.addListener('click', () => {
        createInfoWindow(marker)
        if(!!store) {
            store.dispatch(createMarkerClicked(place))
        }
    });
    ... further implementation ...
}

function throwGoogleMapsNotLoaded() {
    throw new Error('Google maps not loaded');
}

export const mapService: MapService = {
    predictPlaces,
    addMarkerToMap,
    setMap,
    setStore,
    centerMapAroundSuggestion
}
sjbuysse
  • 3,872
  • 7
  • 25
  • 37
  • Accessing store directly is discouraged in React+Redux. Usually a singleton can be maintained via a hierarchy of components. What you call a service could be connected 'smart' component. – Estus Flask Dec 25 '18 at 15:55
  • So it does not make sense to extract this logic into a seperate service that I could exchange easily? Would you put all this (& more) logic in a connected container component? – sjbuysse Dec 25 '18 at 16:10
  • It doesn't necessarily should be a container, could be a childless component because it interacts with the rest of the app via store. You can exchange a component easily, too, or even swap component function depending on the config (that's a form of DI). You can check this post, https://stackoverflow.com/a/52597161/3731501 , it mentions Angular services (which are basically singletons most times). – Estus Flask Dec 25 '18 at 16:28
  • Hmmm interesting, and the post makes a lot of sense. Some things don't completely click though. First of all, even if there are places added to the redux state, & the component gets the new places as props, it should never have re-render. In stead it has to update the global `maps` object. At the moment I'm doing this with redux saga, because it feels more like a side-effect. If I want to solve this with a smart component, I'd need to avoid re-renders through shouldComponentUpdate & update then the global map object from within this component. Or am I misunderstanding you @estus ? – sjbuysse Dec 26 '18 at 18:52

0 Answers0