Using OpenLayers with React

Published

Updated Guide Available

I published an updated guide on how to use OpenLayers with React Functional Components and Hooks. Check it out here: https://taylor.callsen.me/using-openlayers-with-react-functional-components/

Flux Data Flow and Concepts

React and Flux are a great way to produce complex web applications while maintaining a simple and consistent data flow under the hood. When configured correctly, the UI and its various components will update on their own whenever their underlying data is changed.

Flux Data Flow based on Flux Basic Concepts Documentation

Data flows through out a Flux application in a single direction (notice the counter-clock wise arrows in the diagram above). The application’s single “true” source of data lives in the Flux Stores, and is communicated out to the various listening UI components anytime it is changed. These components simply consume data from the Stores, and never update it directly.

Flux Action calls are used when data within the Stores needs to be updated. Examples scenarios are things like new data being available from the backend, or when a user interaction occurs. Action calls can be executed within an AJAX response callback, or from inside a mouse click event handler. Placing an Action call in response to a click event is represented by the View to Action arrow above.

One invoked, that Action calls pass through the Dispatcher, and find their way to the Flux Store. Once they reach the store, they cause updates to the data within the Store. The newly updated data within the Store is emitted back out to the UI components, who will update or re-render based on this new data.

Integrating Open Layers with React

While it seems straight forward to achieve this pattern when working with React components, things get more complicated when working with external JavaScript libraries, especially ones that maintain their own Data Stores. Examples include the D3 charting library, OpenLayers, Google Maps, or any library where data has to be explicitly added or updated.

React provides several hooks that can be used to pass data into these external libraries whenever data within the Flux Stores is updated. Our example below will  make use of the React Component Lifecycle componentDidUpdate function.

Flux Data Flow updated to include External Libraries

Code Example

To integrate an OpenLayers Map (and other external libraries) into our React / Flux pattern, we will create a standard “wrapper” Component to house the OpenLayers Map. The wrapper Component’s render function will create a div element in the DOM, which will contain our OpenLayers map.

class Map extends React.Component {
  
  render () {
    return (
      <div ref="mapContainer"> </div>
    );
  }
 
}

module.exports = Map;

We need to make sure the container div has been rendered into the DOM before creating the Map. The wrapper Component’s componentDidMount() function executes after the wrapper Component’s render() function completes for the first time, which makes it the perfect place to create the OpenLayers map (since we know the map container div will have been rendered into the DOM).

Notice on line 18 below, we are using the React component’s Refs to retrieve the map container div (the call to this.refs.mapContainer). Once we create the OpenLayers Map and Vector Layer objects, we will save references to them into the wrapper Component’s state (see lines 36-39). Saving these references to state will allow us to interact with the external OpenLayers objects later on.

class Map extends React.Component {
  
  //other functions eliminated for brevity

  componentDidMount() {

    // create feature layer and vector source
    var featuresLayer = new ol.layer.Vector({
      source: new ol.source.Vector({
        features:[],
      })
    });

    // create map object with feature layer
    var map = new ol.Map({
      target: this.refs.mapContainer,
      layers: [
		//default OSM layer
		new ol.layer.Tile({
		  source: new ol.source.OSM()
		}),
        featuresLayer
      ],
      view: new ol.View({
        center: [-11718716.28195593, 4869217.172379018], //Boulder, CO
        zoom: 13,
      })
    });

    // save map and layer references to local state
    this.setState({ 
      map: map,
      featuresLayer: featuresLayer
    });

  }

  //other functions eliminated for brevity

}

module.exports = Map;

We will write logic in the wrapper Component’s componentDidUpdate() function to reference the OpenLayers Map and Vector Layer objects in state, and update these objects with new data anytime the wrapper Component is updated.

The new vector data is pushed onto the OpenLayers Map by replacing the Vector Layer’s Source object with the new routes data. In this example, routes are supplied by props, but they could of course come from Component state or directly from a Flux Store.

Keep in mind that the componentDidUpdate() function is executed every time the Component is updated (i.e. new props are passed, state is changed, etc.). A simple linkage like this means OpenLayers will have to redraw the Vector Layer every time the React Component is updated! See a later article about using libraries like Immutable JS to prevent excessively updating external libraries.

class Map extends React.Component {
 
  // other functions eliminated for brevity

  // pass new features from props into the OpenLayers layer object
  componentDidUpdate(prevProps, prevState) {
    this.state.featuresLayer.setSource(
      new ol.source.Vector({
        features: this.props.routes
      })
    );
  }

  // other functions eliminated for brevity

}

module.exports = Map;

Finally, to allow user interactions and events within OpenLayers to hook back into our React/Flux app, we will set a click event handler on the OpenLayers Map. The event handler will fire off an Action call to notify the Flux Store of the event, and include data about the event (which in our case is the clicked locations on the Map). The Action call is placed on line 28 below, with various steps being taken before it to convert the clicked location into WKT format.

At this point the Flux Data Store is aware of the UI click Interaction (and the location of the click) even though it occurred within OpenLayers. The Flux Data Store can respond to this interaction, and emit updated data out to the rest of the application as needed.

class Map extends React.Component {
 
  // other functions eliminated for brevity

  componentDidMount() {

    // placed at the bottom of the componentDidMount function
    //  (other function content elimintated for brevity)
    map.on('click', this.handleMapClick.bind(this));

  }

  handleMapClick(event) {

    // create WKT writer
    var wktWriter = new ol.format.WKT();

    // derive map coordinate (references map from Wrapper Component state)
    var clickedCoordinate = this.state.map.getCoordinateFromPixel(event.pixel);

    // create Point geometry from clicked coordinate
    var clickedPointGeom = new ol.geom.Point( clickedCoordinate );

    // write Point geometry to WKT with wktWriter
    var clickedPointWkt = wktWriter.writeGeometry( clickedPointGeom );
    
    // place Flux Action call to notify Store map coordinate was clicked
    Actions.setRoutingCoord( clickedPointWkt );

  }

  // other functions eliminated for brevity

}

module.exports = Map;

Wrap up

The methodology above can be used to easily integrate data and interactions between a React/Flux application and an external library such as Open Layers 3. See this article for optimizations that can be made to the shouldComponentUpdate() and componentDidUpdate() functions of our Component to prevent triggering excess re-renders inside these external libraries. The complete Wrapper component discussed in our example is assembled below. Hope this article was helpful!

//externals
import ReactDOM from 'react-dom';
import React from 'react';

//open layers and styles
var ol = require('openlayers');
require('openlayers/css/ol.css');

class Map extends React.Component {
 
  componentDidMount() {

    // create feature layer and vector source
    var featuresLayer = new ol.layer.Vector({
      source: new ol.source.Vector({
        features:[]
      })
    });

    // create map object with feature layer
    var map = new ol.Map({
      target: this.refs.mapContainer,
      layers: [
        //default OSM layer
        new ol.layer.Tile({
          source: new ol.source.OSM()
        }),
        featuresLayer
      ],
      view: new ol.View({
        center: [-11718716.28195593, 4869217.172379018], //Boulder
        zoom: 13,
      })
    });

    map.on('click', this.handleMapClick.bind(this));

    // save map and layer references to local state
    this.setState({ 
      map: map,
      featuresLayer: featuresLayer
    });

  }

  // pass new features from props into the OpenLayers layer object
  componentDidUpdate(prevProps, prevState) {
    this.state.featuresLayer.setSource(
      new ol.source.Vector({
        features: this.props.routes
      })
    );
  }

  handleMapClick(event) {

    // create WKT writer
    var wktWriter = new ol.format.WKT();

    // derive map coordinate (references map from Wrapper Component state)
    var clickedCoordinate = this.state.map.getCoordinateFromPixel(event.pixel);

    // create Point geometry from clicked coordinate
    var clickedPointGeom = new ol.geom.Point( clickedCoordinate );

    // write Point geometry to WKT with wktWriter
    var clickedPointWkt = wktWriter.writeGeometry( clickedPointGeom );
    
    // place Flux Action call to notify Store map coordinate was clicked
    Actions.setRoutingCoord( clickedPointWkt );

  }

  render () {
    return (
      <div ref="mapContainer"> </div>
    );
  }

}

module.exports = Map;

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 *