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.
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:
- Rendering a <div> element that OpenLayers will be loaded into
- 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
Comments
No responses yet