craigmadethis

Removing useEffect from your OpenLayers apps

Published: 24/11/2024, 16:30:00

Updated: 25/11/2024, 08:00:00

I think that everyone might have been mounting their OpenLayers maps wrong. There's been a lot of stuff flying around on Bluesky/xitter about callback refs from TkDodo and I realised that I, and probably a lot of other people, have probably been mounting my OpenLayers maps wrong.

The useEffect way

The OpenLayers docs (rightly) don't mention how to properly mount a map in React but there are a few very similar guides kicking around online. I've done this a few times, and the best guide I've found comes from the 2D/3D WebGIS platform using React, OpenLayers and Cesium notes from the 2023 FOSS4G conference. I just kind of accepted this as the way, but the big useEffect never really sat right with me.

import { Map as OLMap, View } from "ol";
import TileLayer from "ol/layer/Tile";
import { fromLonLat } from "ol/proj";
import { OSM } from "ol/source";
import { useEffect, useRef, useState } from "react";
 
export const MapComponent = () => {
  const [map, setMap] = useState<OLMap>();
  const mapElement = useRef<HTMLDivElement | null>(null);
  const mapRef = useRef<OLMap | null>(null);
 
  const [mapView] = useState(
    () =>
      new View({
        projection: "EPSG:3857",
        center: fromLonLat([-4.25, 55.86]),
        zoom: 12,
        maxZoom: 18,
        minZoom: 3,
        smoothExtentConstraint: true,
      }),
  );
 
  useEffect(() => {
    if (mapRef.current || !mapElement.current) return;
    const osmSource = new OSM();
    mapRef.current = new OLMap({
      target: mapElement.current,
      layers: [
        new TileLayer({
          source: osmSource,
          visible: true,
        }),
      ],
      view: mapView,
    });
    setMap(mapRef.current);
  }, [map, mapElement, mapView]);
 
  return <div id="map" ref={mapElement} className="absolute inset-0" />
};
 

Obviously you can change a lot and there are a few different ways of managing the map state and sharing the map context throughout the app.

The "better", callbackRef way

But, as I hinted to before I think there's a better way. I'm not going to explain how callback refs work, TkDodo created a much better guide. But, here is how I implemented it with OpenLayers.

import { Map as OLMap, View } from "ol";
import TileLayer from "ol/layer/Tile";
import { fromLonLat } from "ol/proj";
import { OSM } from "ol/source";
import { useCallback, useState } from "react";
 
export const MapComponent = () => {
  const [map, setMap] = useState<OLMap | null>(null);
 
  const [mapView] = useState(
    () =>
      new View({
        projection: "EPSG:3857",
        center: fromLonLat([-4.25, 55.86]),
        zoom: 12,
        maxZoom: 18,
        minZoom: 3,
        smoothExtentConstraint: true,
      }),
  );
  const [osmSource] = useState(new OSM());
 
  const mapElementCbRef = useCallback(
    (node: HTMLDivElement | null) => {
      if (!node) return;
 
      setMap(
        new OLMap({
          target: node,
          layers: [
            new TileLayer({
              source: osmSource,
              visible: true,
            }),
          ],
          view: mapView,
        }),
      );
    },
    [mapView, osmSource],
  );
 
  return <div id="map" ref={mapElementCbRef} className="absolute inset-0" />
};
 
export default App;

No more useEffect - success! Also, there's now no condition where the map is not mounted so there's none of the funny side-effects I've sometimes found with the map not being mounted.

Very excited to now rewrite all my OpenLayers maps...

References

Other Posts