title: React service singleton alternative
body: <p>I'm making a ReactJS application using the google maps library &amp; 
  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. </p>
  
  <p>The way it works now is that I have a component loading the map library and attaching it to a div. The <code>onLoad</code> callback will set the map in this service, so that I can keep a reference to it. </p>
  
  <pre><code>onMapLoad={(map: Map) =&gt; {
      mapService.setMap(map);
      props.mapLoaded();
  }}
  </code></pre>
  
  <p>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 <code>index.tsx</code> file</p>
  
  <pre><code>const store = getStore();
  mapService.setStore(store);
  </code></pre>
  
  <p>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?</p>
  
  <pre><code>interface MapService {
      predictPlaces: (query: string) =&gt; Observable&lt;AutocompletePrediction[]&gt;;
      setStore: (store: Store&lt;StoreState&gt;) =&gt; void;
      addMarkerToMap: (place: Place) =&gt; void;
      centerMapAroundSuggestion: (suggestion: Suggestion) =&gt; void;
      setMap: (newMap: Map) =&gt; 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) =&gt; {
      map = newMap;
  }
  
  const setStore = (store: Store) =&gt; {
      store = Store;
  }
  
  const centerMapAroundSuggestion = (suggestion: Suggestion) =&gt; {
      if (!map) {
          throwGoogleMapsNotLoaded();
      }
      if (!geocoder) {
          geocoder = new google.maps.Geocoder();
      }
      ... further implementation ...
  }
  
  const predictPlaces = (query: string): Observable&lt;AutocompletePrediction[]&gt; =&gt; {
      if (!map) {
          return of([]);
      }
      if (!predictService) {
          predictService = new google.maps.places.AutocompleteService();
      }
      ... further implementation ...
  }
  
  const addMarkerToMap = (place: Place, onSelect: () =&gt; void) =&gt; {
      const marker = createMarker(place, onSelect);
      markers.push(marker);
  }
  
  const createMarker = (place: Place): Marker =&gt; {
      if (!map) {
          throwGoogleMapsNotLoaded();
      }
      const marker = new google.maps.Marker({
          ...options...
      });
      marker.addListener('click', () =&gt; {
          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
  }
  </code></pre>