odoe

EsriJS with ReactJS (updated)

Blog Post created by odoe on Apr 1, 2015

esri-react2.jpg

Not too long ago, I did a blog post on using the ArcGIS JavaScript API with ReactJS for widgets. I've been talking to a few people about React lately, so I thought I might revisit the subject here. The biggest question you might ask, is why use React? Personally, I think React has a really elegant API. When you start using it and begin to find how easy it is to compose your components that make up the user interface, I think that's when React really shines. A neat feature of React is the use of a virtual DOM. Basically, if you update the state of your application at the root, it will rerender the entire applications DOM. It sounds like it would be slow, but it's really not. It can be, if you do something silly like try to create a very large JSON editor **ahem**. You can read more about how the updates occur here.

 

It's all just DOM

If you are familiar with working with Dijits and the Dijit lifecycle, React components have a similar lifecycle. In both cases it's beneficial to get to know them. React uses an optional syntax called JSX. It's important to remember that JSX is totally optional. Some folks get all up in arms about mixing DOM with JavaScript, but that's not the case. JSX is simply used to help you define your DOM structure. It still has to be compiled to pure JavaScript in order to in the browser and there are tools for that.

 

Widgets for the masses

The source code for the sample application is available here. I won't go over the entire application in detail, but I do want to highlight the widgets.

 

First off there is a widget called Locator that simply displays the map coordinates of the cursor.

/** @jsx React.DOM */
define([
  'react',
  'dojo/topic',
  'helpers/NumFormatter'
], function(
  React,
  topic,
  format
) {
  var fixed = format(3);
  var LocatorWidget = React.createClass({
    getInitialState: function() {
      return {
        x: 0,
        y: 0
      };
    },
    componentDidMount: function() {
      this.handler = this.props.map.on('mouse-move', function(e) {
        this.update(e.mapPoint);
      }.bind(this));
    },
    componentWillUnMount: function() {
      this.handler.remove();
    },
    update: function(data) {
      this.setState(data);
      topic.publish('map-mouse-move', data);
    },
    render: function() {
      return (
        <div className='well'>
          <label>x: {fixed(this.state.x)}</label>
          <br/>
          <label>y: {fixed(this.state.y)}</label>
        </div>
      );
    }
  });
  return LocatorWidget;
});

So this component is pretty simple. It will just display coordinates and uses dojo/topic to publish those coordinates to the application. I've talked about dojo/topic before. The next component is a little more interesting. It has a button that will activate a draw tool and a label that will display the distance being drawn.

 

/** @jsx React.DOM */
define([
  'react',
  'esri/toolbars/draw',
  'esri/geometry/geometryEngine',
  'dojo/topic',
  'dojo/on',
  'helpers/NumFormatter'
], function(
  React,
  Draw, geomEngine,
  topic, on,
  format
) {
  var fixed = format(3);
  var DrawToolWidget = React.createClass({
    getInitialState: function() {
      return {
        startPoint: null,
        btnText: 'Draw Line',
        distance: 0,
        x: 0,
        y: 0
      };
    },
    componentDidMount: function() {
      this.draw = new Draw(this.props.map);
      this.handler = this.draw.on('draw-end', this.onDrawEnd);
      this.subscriber = topic.subscribe(
        'map-mouse-move', this.mapCoordsUpdate
      );
    },
    componentWillUnMount: function() {
      this.handler.remove();
      this.subscriber.remove();
    },
    onDrawEnd: function(e) {
      this.draw.deactivate();
      this.setState({
        startPoint: null,
        btnText: 'Draw Line'
      });
    },
    mapCoordsUpdate: function(data) {
      this.setState(data);
      // not sure I like this conditional check
      if (this.state.startPoint) {
        this.updateDistance(data);
      }
    },
    updateDistance: function(endPoint) {
      var distance = geomEngine.distance(this.state.startPoint, endPoint);
      this.setState({ distance: distance });
    },
    drawLine: function() {
      this.setState({ btnText: 'Drawing...' });
      this.draw.activate(Draw.POLYLINE);
      on.once(this.props.map, 'click', function(e) {
        this.setState({ startPoint: e.mapPoint });
        // soo hacky, but Draw.LINE interaction is odd to use
        on.once(this.props.map, 'click', function() {
          this.onDrawEnd();
        }.bind(this));
      }.bind(this))
    },
    render: function() {
      return (
        <div className='well'>
          <button className='btn btn-primary' onClick={this.drawLine}>
            {this.state.btnText}
          </button>
          <hr />
          <p>
            <label>Distance: {fixed(this.state.distance)}</label>
          </p>
        </div>
      );
    }
  });
  return DrawToolWidget;
});

This component is a little more involved, but it will use the geometryEngine to calculate the distance between the start point and the end point in a straight line. I do some ugly hacky-ness to stop the drawing interaction after a single line segment, because single drawing with the Draw tool is a little odd behavior (in my opinion). If you wanted to track the total distance of the polyline segments while being drawn, it's a little more involved and you would need to update the start point with the last end point and accumulate the distances as you go along. Sounds like a nice reduce function. I'll leave that exercise up to you. If I spent more time on this, I might separate some of the logic happening here into helper methods and maybe clean up some listeners, but as it stands, it works pretty well.

 

The last thing you need to do is actually render these components on the page.

 

/** @jsx React.DOM */
define([
  'react',
  'dojo/query',
  'dojo/dom',
  'dojo/dom-construct',
  './components/Locator',
  './components/DrawTools'
], function(
  React,
  query, dom, domConstruct,
  Locator, DrawTools
) {
  var createContainer = function() {
    var c = query('.widget-container');
    if (c.length) {
      return c.shift();
    }
    var container = domConstruct.create('div', {
      className: 'widget-container'
    }, dom.byId('map_root'), 'first');
    return container;
  };
  var addContainer = function(map) {
    React.render(
      <div>
        <Locator map={map} />
        <DrawTools map={map} />
      </div>,
      createContainer());
  };
  return {
    addContainer: addContainer
  };
});

This is where React will render the components to the defined DOM element on the page and I can pass the map to my widgets as properties. I didn't really discuss props or propTypes, but you can read more how they work here. Also here is a nice write-up on Properties vs State.

 

Get hacking

You can see a demo of this application in action here. In case you missed it the source code is here.

As you can see, this is just another example of being able to integrate any library or framework of your choice in your ArcGIS API for JavaScript applications. There are certain parts of Dojo that you need to use (and some are very handy, such as dojo/topic), but beyond that it's not too hard to mix-n-match your JavaScript libraries. React is not a total framework, and the FB devs will be the first to tell you that, but it does do the V in MVC very well. So hack away folks and most of all enjoy it!

 

For more geodev tips and tricks, check out my blog.

Outcomes