Using OpenLayers with React Functional Components

Published

A few years ago, I wrote an article about how to add an OpenLayers map into a React application. I thought it would be appropriate to revisit this topic, and provide an updated example using React’s new Functional Components and Hooks.

A snapshot of the sample application discussed below

Code Posted to GitHub

The code referenced below is available on GitHub. Check the README.md file for installation instructions, and commands to start the local development server. This project was bootstrapped with Create React App with the goal of making it easy to work with.

Creating a Functional Component Wrapper

First we will create a “wrapper” component that acts as the interface between the React ecosystem and OpenLayers. Two important features of this component are:

  1. Rendering a <div> element that OpenLayers will be loaded into
  2. Maintaining a reference to the OpenLayers Map and Layer objects in the component state
import React, { useState, useRef } from 'react';

function MapWrapper(props) {

  // set intial state - used to track references to OpenLayers 
  //  objects for use in hooks, event handlers, etc.
  const [ map, setMap ] = useState()
  const [ featuresLayer, setFeaturesLayer ] = useState()
  const [ selectedCoord , setSelectedCoord ] = useState()

  // get ref to div element - OpenLayers will render into this div
  const mapElement = useRef()
  
  return (
    <div ref={mapElement} className="map-container"></div>
  )

}

export default MapWrapper

Initializing the Map

Next we will create a hook to initialize the OpenLayers map after the first render. We will use a useEffect hook, and pass an empty dependency array so that the hook is only executed once (see line 49). Traditionally this logic would have been placed in a componentDidMount() function.

Note: the OpenLayers Map and VectorLayer objects are being saved into the React Component state on lines 46-47. These objects will be used later in hooks and event handlers.

import React, { useState, useRef } from 'react';

import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import XYZ from 'ol/source/XYZ'

function MapWrapper(props) {

  // state and ref setting logic eliminated for brevity

  // initialize map on first render - logic formerly put into componentDidMount
  useEffect( () => {

    // create and add vector source layer
    const initalFeaturesLayer = new VectorLayer({
      source: new VectorSource()
    })

    // create map
    const initialMap = new Map({
      target: mapElement.current,
      layers: [
        
        // USGS Topo
        new TileLayer({
          source: new XYZ({
            url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
          })
        }),

        initalFeaturesLayer
        
      ],
      view: new View({
        projection: 'EPSG:3857',
        center: [0, 0],
        zoom: 2
      }),
      controls: []
    })

    // save map and vector layer references to state
    setMap(initialMap)
    setFeaturesLayer(initalFeaturesLayer)

  },[])

  // return/render logic eliminated for brevity

}

Adding Features to the Map

We can use another useEffect hook to add features to the map. When features are supplied via React props, adding ‘props.features’ to the dependency array (line 24) ensures that the hook will only execute when the features property changes. In the past, this logic would have been placed in a componentDidUpdate() function.

function MapWrapper(props) {

  // state, ref, and initialization logic eliminated for brevity

  // update map if features prop changes - logic formerly put into componentDidUpdate
  useEffect( () => {

    if (props.features.length) { // may be empty on first render

      // set features to map
      featuresLayer.setSource(
        new VectorSource({
          features: props.features // make sure features is an array
        })
      )

      // fit map to feature extent (with 100px of padding)
      map.getView().fit(featuresLayer.getSource().getExtent(), {
        padding: [100,100,100,100]
      })

    }

  },[props.features])

  // return/render logic eliminated for brevity

}

Using State inside Event Handlers

We can easily use the setMap(), setFeaturesLayer(), and setSelectedCoord() functions to update the React component’s state from within an OpenLayers event handler. However, reading State in this context is not so easy.

A closure is created when an event handler is set inside of a hook, meaning any state we try to access will be stale. Fortunately we can use a React Ref to allow access to the “current” state. The ref is created and set on lines 8-9, and then used on line 25 to gain access to the OpenLayers Map object.

function MapWrapper(props) {

  // other state, ref, and initialization logic eliminated for brevity
  const [ map, setMap ] = useState()

  // create state ref that can be accessed in OpenLayers onclick callback function
  //  https://stackoverflow.com/a/60643670
  const mapRef = useRef()
  mapRef.current = map

  // initialize map on first render - logic formerly put into componentDidMount
  useEffect( () => {

    // placed at the bottom of the initialization hook
    //  (other function content elimintated for brevity)
    initialMap.on('click', handleMapClick)

  },[])

  // map click handler
  const handleMapClick = (event) => {
    
    // get clicked coordinate using mapRef to access current React state inside OpenLayers callback
    //  https://stackoverflow.com/a/60643670
    const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel);

    // transform coord to EPSG 4326 standard Lat Long
    const transormedCoord = transform(clickedCoord, 'EPSG:3857', 'EPSG:4326')

    // set React state
    setSelectedCoord( transormedCoord )
    
  }

  // return/render logic eliminated for brevity

}

Conclusion

There are certainly benefits to React’s new Functional Components and Hooks. I like how easy it is to ensure hooks are only executed when select state and props change, which alleviates the need for complex evaluations in shouldComponentUpdate() functions. I look forward to using them more in the future!

Below is the complete MapWrapper component, which is also available on GitHub.

// react
import React, { useState, useEffect, useRef } from 'react';

// openlayers
import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import XYZ from 'ol/source/XYZ'
import {transform} from 'ol/proj'
import {toStringXY} from 'ol/coordinate';

function MapWrapper(props) {

  // set intial state
  const [ map, setMap ] = useState()
  const [ featuresLayer, setFeaturesLayer ] = useState()
  const [ selectedCoord , setSelectedCoord ] = useState()

  // pull refs
  const mapElement = useRef()
  
  // create state ref that can be accessed in OpenLayers onclick callback function
  //  https://stackoverflow.com/a/60643670
  const mapRef = useRef()
  mapRef.current = map

  // initialize map on first render - logic formerly put into componentDidMount
  useEffect( () => {

    // create and add vector source layer
    const initalFeaturesLayer = new VectorLayer({
      source: new VectorSource()
    })

    // create map
    const initialMap = new Map({
      target: mapElement.current,
      layers: [
        
        // USGS Topo
        new TileLayer({
          source: new XYZ({
            url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
          })
        }),

        // Google Maps Terrain
        /* new TileLayer({
          source: new XYZ({
            url: 'http://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}',
          })
        }), */

        initalFeaturesLayer
        
      ],
      view: new View({
        projection: 'EPSG:3857',
        center: [0, 0],
        zoom: 2
      }),
      controls: []
    })

    // set map onclick handler
    initialMap.on('click', handleMapClick)

    // save map and vector layer references to state
    setMap(initialMap)
    setFeaturesLayer(initalFeaturesLayer)

  },[])

  // update map if features prop changes - logic formerly put into componentDidUpdate
  useEffect( () => {

    if (props.features.length) { // may be null on first render

      // set features to map
      featuresLayer.setSource(
        new VectorSource({
          features: props.features // make sure features is an array
        })
      )

      // fit map to feature extent (with 100px of padding)
      map.getView().fit(featuresLayer.getSource().getExtent(), {
        padding: [100,100,100,100]
      })

    }

  },[props.features])

  // map click handler
  const handleMapClick = (event) => {

    // get clicked coordinate using mapRef to access current React state inside OpenLayers callback
    //  https://stackoverflow.com/a/60643670
    const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel);

    // transform coord to EPSG 4326 standard Lat Long
    const transormedCoord = transform(clickedCoord, 'EPSG:3857', 'EPSG:4326')

    // set React state
    setSelectedCoord( transormedCoord )

    console.log(transormedCoord)
    
  }

  // render component
  return (      
    <div ref={mapElement} className="map-container"></div>
  ) 

}

export default MapWrapper

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *