Building CampaignHawk: Mapbox.js and Meteor Data (Part 12)

Now it’s time to add data layers on top of our map. We want to do this programmatically so we can add and remove these layers when a user checks or un-checks a box in a popout from the data layer icon in the sidenav. Fortunately, Mapbox makes this pretty easy.

The general structure of the function we’re going to use looks like the code below, with the relevant bits of information filled in:

var <layer name> = L.mapbox.featureLayer().addTo(map);
<layer name>.setGeoJSON(<array of data>);

We have a couple options for our data. We can use the data.geojson file that lives in our public directory or we can just use the data in our VoterDataGeoJSON collection. We’re going to use the data straight from the collection, but I’ll also show you how you’d do it if you wanted to pull it from the data.geojson file.

To use the data from the data.geojson file, you’ll need to make an HTTP request. Within the Mapbox.loaded conditional in Map.jsx, we need to make a GET request to retrieve the file, then create the layer, then set the GeoJSON to the layer. Keep in mind that the GeoJSON needs to be in an array or it will not work.

HTTP.get(Meteor.absoluteUrl("data.geojson"), function(err, res) {
var voterLayer = L.mapbox.featureLayer().addTo(map);
voterLayer.setGeoJSON([JSON.parse(res.content)]);
});

But we’re going to use the data in our VoterDataGeoJSON collection. This is where we need to start thinking about how Meteor data works with React. We’re going to do a few things that are a general best practice and a few things that are relatively new to React and Meteor.

One thing we’re definitely going to do is wrap our components in a higher order component so we can keep our views pure and manage our data separately. In short, we want our component structure to look more like this:

<MainView>
  <Data
<SubView1 />
/>
  <Data
<SubView2 />
/>
</MainView>

The other thing we’re going to do is turn our data components into composable components, which will manage our subscriptions and fetch our data in a clean and easily understood manner. You can read more about composable components here.

So now that we’re dealing with data, we need to remove two default packages:

$ meteor remove autopublish insecure

This means we need to set up our publication on our server. Within Server.jsx we need to add:

Meteor.publish('geojson', function() {
return VoterDataGeoJSON.find();
})

So now that we have a publication, we need to subscribe to that publication on the client. This is where our composable components come in handy. First we need to create a MeteorData wrapper component. I’ll stick it in App.jsx since it’s a global wrapper and that file is pretty empty.

What we’re doing in this component is taking advantage of the ReactMeteorData mixin that Meteor gives us. Although mixins are on the way out, this is a perfectly legitimate use of a mixin, because we’re using it to compose a higher-order component which neatly separates our concerns.

We’re also passing subscribe(), fetch(), and render() as props, as well as the data and the loading state. Again, if you want to read more about composable components, you should read this article.

MeteorData = React.createClass({
mixins: [ReactMeteorData],
getMeteorData() {
const sub = this.props.subscribe()
const data = this.props.fetch()
data.loading = !sub.ready()
return data;
},
render() {
return this.props.render(this.data)
}
})

So now that we’re publishing our data and we have a way to manage it in a composable component, we need to prepare our data layers. I haven’t decided on the best way to handle this once we have multiple data layers, but for we’ll just do something simple.

First let’s extract the content of our render function in our Map component and put it in a new component cleverly named MapChild. We then need to pass in all the functions and state variables that the child components need.

<MapChild
showModalState={this.state.showModalState}
hideModal={this.hideModal}
showModal={this.showModal} />

Note that I had to change the name of the state variable showModal to showModalState. I’m going to skip the part where I change all of them. We also need to change the functions in MapChild to this.props.____ because the reference is now coming from props.

Now we wrap our MapChild component in our MeteorData composable component to handle our subscription. This might look daunting at first, but it’s actually quite simple. Just follow the props being passed:

<MeteorData
subscribe = { () => {
return Meteor.subscribe('geojson') }}
fetch = { () => {
return {data: VoterDataGeoJSON.find().fetch() } }}
render = { ({loading, data}) => {
return <MapChild
showModalState={this.state.showModalState}
hideModal={this.hideModal}
showModal={this.showModal}
loading={loading}
data={data}
/> }
}
/>

We’re subscribing to geojson, which we are publishing in Server.jsx, fetching the data from that subscription, and passing in the loading state and the data to the child component.

Now let’s implement a function to show that our data actually works even though we will eventually call this function from somewhere else. Within the render function of MapChild, we’re going to add the following to add a data layer as soon as the data loads. This is not how we’re going to implement it in the long-run, but we want to get something to render before we move forward.

if (!this.props.loading) {
var voterLayer = L.mapbox.featureLayer().addTo(map);
voterLayer.setGeoJSON(this.props.data);
}

Also note that we need to change map to a global variable the top of Map.jsx by removing the “var” in front of it. You should end up with a map like the one below.

Unfortunately, we’re passing in too much data. It might seem like your browser is frozen when you refresh, but that’s only because it’s trying really hard to render all these datapoints. We’ll optimize this later.

One last thing, I noticed that the styling of our box-shadows take in a grey color. That’s not ideal, especially once we start stacking items on top of each other. I went ahead and changed all my box shadows to black with 50% opacity:

box-shadow: 0px 0px 15px 0px rgba(0,0,0,0.5);

Next Steps

Now we need to do some more work on the frontend to allow a user to programmatically turn data layers on and off. Once we do that, we need to optimize the data so it doesn’t almost crash our browser every time it renders, and make a few functions to show different data in different ways.