How to make a simple map app using Ruby on Rails, React and Leaflet, part 2: Frontend

Tidal Basin, Washington, D.C., source: author

I recently built an app that has an interactive map of street trees in Washington, D.C. In the previous post I wrote about the challenges of reorganizing data and creating Ruby on Rails API. This time I’ll describe the process of building frontend using React.js and relevant mapping libraries.

Getting the map to show up

The first step was to get the map to show up. I decided to use React-Leaflet library with Mapbox base map. After installing React-Leaflet, I simply imported Map, TileLayer, Marker and Popup components. All this and more are part of the ‘Getting started’ section of the React-Leaflet docs. If you follow the examples in the docs you can easily get the map or any other component to show up.

The next step was to implement the Mapbox base map. After getting the public access token from Mapbox, I simply passed it, together with the tile URL, as props to TileLayer component. This way I got the desired static map to show up. My App.js contains the following code:

App.js

Adding data to the map

Now that I got the map to show up, it was time to add some data to it. This step proved to be trickier than it seemed. If you follow examples from the docs, all you need to do is import the Marker and Popup components, and pass the necessary props. It’s very straightforward, however I couldn’t get the markers to show up on my map. After hours of debugging and researching, I realized that the problem was not on my end. It turned out that the Leaflet’s default marker was linked to photo that has been deleted. In order to fix this issue, I had to delete the custom marker icon, and import a new one.

For apps that work with smaller amount of data, markers with popups might be sufficient. In my case, it only made the map look extremely clustered and almost impossible to navigate, as it was covered with thousands of tree markers. React-Leaflet-Markercluster is a library that solves this problem in (literally) just few lines of code.

After all these changes, App.js contained the following code:

App.js after adding markers, popups and marker clusters

Improving performance

Large data consisting of nearly 200,000 trees is probably the most difficult part of this project. There are numerous ways to improve speed and performance, and I’ve been coming back to this issue since ever since the day I started working on this app. One of the ideas I had uses bounding box queries (that are already implemented on the backend) to fetch only trees that are within the boundaries of the user’s screen. In order to fully implement this feature, I had to do the following:

  • add coordinates to the state and define its initial value that’s equal to the boundaries of the map first rendered on the screen
  • add filter to the API fetch so that it returns only trees that are within the boundaries of the screen
  • add event listener to the map that will update the state with new coordinates as they change, as well as with newly fetched trees

This may sounds complicated, however a lot of the main work has already been done. The main logic that does all the work for us has already been written at the backend. In addition to that, Leaflet’s built-in methods make this step a lot easier. One of them is .getBounds() method that grabs the coordinates of the screen corners (lines 58–61). The scope of this method changes, so my advice is to play with it in the debugger to figure out the exact syntax.

As for the event listener, Leaflet comes with plenty of options. React-Leaflet uses slightly different syntax that is explained in the docs. The event listener that suits my app the best fires when user stops dragging the map (onMoveEnd, line 74). In the callback function I set the new state and fetch trees. This makes the app a lot faster, and creates better user experience.

At this point App.js contains the following code:

App.js after implementing the bounding box filter

Implementing filters

The data that I’m using is quite detailed. Every tree comes with information about its condition, ward, scientific name, genus, family, etc. I wanted to make use of all that information, and allow users to filter trees by ward, condition, common name or scientific name. In order to build this feature, I had to add two new components, Navbar and Filters. I decided to use React-Semantic-UI and its sidebar component, despite the fact that it might not be the most elegant or simple solution (its components are in App.js, Navbar.js and Filters.js).

Navbar.js is a fairly simple component that doesn’t contain any important logic.

Components/Navbar.js

Filters.js contains the form and all the necessary callbacks that grab the values from the form.

Components/Filters.js

Those values are then sent to App.js and applied to the fetch call.

In theory, the state change would cause the map to re-render every time the user applies and resets the filters. However, in practice that wasn’t the case. While the state contains the current filter values (ward, condition, commonName or sciName), the tree data was always one step behind, containing the results of the previous search.

After hours or debugging and a thorough research, I realized that the very nature of React is the reason for this. Before I ran into this issue I wasn’t aware of the fact that setState is asynchronous. What this means is that the order of actions was different from what I was expecting it to be. The fetch call would occur before the state updates, resulting in tree data always being one step behind. This problem was fixed by passing the filter values directly to the function doing the fetch call (lines 53–59 and 71–84). For alternative solutions, check out this blog.

App.js contains the following code:

App.js after the implementation of filters (the final version)

Now that all functionality has been added to the map, the only thing left to do is add styling. A lot of the map elements can be customized using plain CSS, (for instructions to style marker clusters click here). And the map is ready! This is what my map looks like:

source: author
source: author

For more info and live demo of this app, click here.

--

--

--

I moved from Europe to USA and from human languages to programming languages. www.anavharris.com

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

How Git best practices saved me hours of rework

Step 2: Types / Zero to Front End Dev

Everything you need to know about NodeJs

That’s So Fetch

Creating Purchase Contract with Ethers.js Using Angular NgRx v8

Writing Files with Node.js and JSDOM

Jenkins CI/CD to deploy Angular application on Azure Storage

Next.js SSR vs. SSG & Incrementalism

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ana Harris

Ana Harris

I moved from Europe to USA and from human languages to programming languages. www.anavharris.com

More from Medium

Introduction to Ruby Rack Application

How to use Active Record in Sinatra (RUBY)

Google Speech-to-Text Api with MediaRecorder Api and Rails

*RECORD THE AUDIO USING MEDIA RECORDER

Building React.js App With Ruby on Rails API