Mapbox and Tool Tips in React.js

Sylvia Pap - Apr 10 '20 - - Dev Community

While perusing popular posts, I was inspired by this COVID-19 map to get into learning Mapbox. The project covers a lot of what I do here and I hope I'm not coming off trying to steal anyone's thunder. This isn't a post about my creativity. I am a beginner/bootcamp student and felt like I could even further simplify the process of just using Mapbox at all, let alone connecting it to interesting COVID data and formatting.

Basic Setup of Mapbox

Mapbox GL JS is a JavaScript library that uses WebGL to render interactive maps from vector tiles and Mapbox styles. This tutorial on basic setup in React is very good and helpful! This post will mostly walk through/combine several already very good tutorials. Once again, not trying to reinvent the wheel here, but hoping to combine some good existing wheels.

Alt text
End goal!

Basic React setup:

npx create-react-app your-app-name
cd your-app-name
npm install mapbox-gl
Enter fullscreen mode Exit fullscreen mode

or add mapbox-gl to package.json manually and then run npm install. Both seem to accomplish the same thing - creating package-lock.json and having a package.json that contains mapbox-gl in dependencies.

Now this is probably a trivial difference, but the Mapbox tutorial includes everything in index.js, I've been learning React with keeping index.js short - like this:

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

And then keeping most of my code in App.js for now.

// src/App.js

import React, { Component } from 'react'
import "./App.css";
import mapboxgl from 'mapbox-gl';

mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;

export class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      lng: -90,
      lat: 45,
      zoom: 3
    };}

  componentDidMount() {
    const map = new mapboxgl.Map({
      container: this.mapContainer,
      style: 'mapbox://styles/mapbox/streets-v11',
      center: [this.state.lng, this.state.lat],
      zoom: this.state.zoom
    });}

  render() {
    return (
      <div className="App">
        <div ref={element => this.mapContainer = element} className="mapContainer" />
      </div>
    )}}

export default App
Enter fullscreen mode Exit fullscreen mode

and now we have a basic Mapbox! For the access token, you simply sign up for a free and easy account on Mapbox, and then, small side note that isn't super important since it's unlikely anyone would want to steal your free token, but good practice to use .env and .gitignore:

// in project main directory
touch .env

// .env
REACT_APP_MAPBOX_ACCESS_TOKEN=<mytoken>

// App.js
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;

// .gitignore
.env
Enter fullscreen mode Exit fullscreen mode

Fun note of caution! ⚠️ If you get the error Invalid LngLat latitude value: must be between -90 and 90 - you probably have your longitude and latitude mixed up! If you only knew how many things I tried to fix this without just simply googling the error because I didn't think I could be making such a simple mix up...

Alt Text
Longitude boi

Anyway, at this point I have my coords set to SF. You can mess around with console.logs and the React dev tools for state to experiment with different starting coords and zoom.

    this.state = {
      lat: 37.7524,
      lng: -122.4343,
      zoom: 11.43
    };
  }
Enter fullscreen mode Exit fullscreen mode

Still following the Mapbox tutorial - here is how you add a bar that shows your coordinates and zoom as you move around the map.

// added to existing componentDidMount() function 

componentDidMount() {
...
    map.on('move', () => {
      this.setState({
      lng: map.getCenter().lng.toFixed(4),
      lat: map.getCenter().lat.toFixed(4),
      zoom: map.getZoom().toFixed(2)
      });
      });
    }
Enter fullscreen mode Exit fullscreen mode

and in render(), add the follow <div> just under <div className="App">:

// added to existing render() 
...
   <div className="App">
     <div className="sidebarStyle">
        Longitude: {this.state.lng} | Latitude: {this.state.lat} | Zoom: {this.state.zoom}
     </div>
Enter fullscreen mode Exit fullscreen mode

At this point you should also have something like this in src/App.css. Note if something isn't working but you aren't getting any errors, it might be a CSS issue - a lot of this involves styling from Mapbox.

.mapContainer {
 position: absolute;
 top: 0;
 right: 0;
 left: 0;
 bottom: 0;
}

.sidebarStyle {
 display: inline-block;
 position: absolute;
 top: 0;
 left: 0;
 margin: 12px;
 background-color: #404040;
 color: #ffffff;
 z-index: 1 !important;
 padding: 6px;
 font-weight: bold;
 }
Enter fullscreen mode Exit fullscreen mode

A small tangent I found interesting but easy to look up - if you want to change the icon that appears in the browser tab next to title, save an image to your public folder, and add to index.html where the default icon link is already set:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/map.png" />
Enter fullscreen mode Exit fullscreen mode

and just change the portion after %PUBLIC_URL%/. I had saved mine as 'map.png' as you can see here.

This is where the Mapbox tutorial pretty much ends, and then links to examples on how to expand. As with everything in programming, there are so many good options! And different ways of doing every one of those options. For some reason, tool tips stood out to me. I didn't even know 'tool tips' was the official term for these little hover popups until now.

I had also come across this great blog post on React component libraries, and was interested in using react-portal-tooltip. But, I found the Mapbox official example on tooltips a bit easier to follow directly after this setup. react-portal-tooltip is more general, and useful for all sorts of apps, which is great, but it helped me to start with the Mapbox specific example to learn what was going on here.

Tooltips

The tooltip (or infotip, or hint) is a common graphical user interface element — a small "hover box" with information about the item. Again, pretty basic stuff, but I am a coding bootcamp student, and we just finished vanilla JS/started React, so this seemed like a cool thing that would have been harder without React! I always like to think of a clear example in my mind of why I am learning something, instead of just accepting it because it's a buzzword. Anyway!

This is the repo for the Mapbox specific tooltip example that I'm starting with.

First, create a components directory within src and a ToolTipBox.js Component (or you could name it anything you want, something shorter like just ToolTip.js, but if I end up using a tooltip library later, that could be not specific enough). Import the component, as well as ReactDOM which we now need in App.js, and add the following code:

...
import ReactDOM from 'react-dom';
import ToolTipBox from './components/ToolTipBox'
...

export class App extends Component {
 mapRef = React.createRef();
 tooltipContainer;

 componentDidMount() {
   // Container to put generated content in
   this.tooltipContainer = document.createElement('div');

   const map = new mapboxgl.Map({
     container: this.mapRef.current,
 ...
   });
 ...
   const tooltip = new mapboxgl.Marker(this.tooltipContainer).setLngLat([0,0]).addTo(map);

   map.on('mousemove', (e) => {
     const features = map.queryRenderedFeatures(e.point);
     tooltip.setLngLat(e.lngLat);
     map.getCanvas().style.cursor = features.length ? 'pointer' : '';
     this.setTooltip(features);
   }
   );
 }

 render() {
   return (
     <div className="App">
      ...
       <div ref={this.mapRef} className="absolute top right left bottom"/>
     </div>)}}
...
Enter fullscreen mode Exit fullscreen mode

Notice in map.on('mousemove') I have this.setTooltip(features). I define this outside of componentDidMount() and it connects to my ToolTipBox component.

export class App extends Component {
...
 setTooltip(features) {
   if (features.length) {
     ReactDOM.render(
       React.createElement(
         ToolTipBox, {
           features
         }
       ),
       this.tooltipContainer
     );
   } else {
     ReactDOM.unmountComponentAtNode(this.tooltipContainer);
   }
 }
...
}
Enter fullscreen mode Exit fullscreen mode

Important things used here - React.createRef(), which is good for:

Managing focus, text selection, or media playback.
Triggering imperative animations.
Integrating with third-party DOM libraries.

But should be avoided for anything that can be done declaratively.

queryRenderedFeatures comes from the Mapbox API and is how we get the 'features' that will gives us the tooltips/popups info!

React.createElement() - this doesn't seem common/standard and would usually be done with JSX. The React docs recommend using JSX and not React.createElement(), but it seems fine here.

Now more on the ToolTipBox component, which uses Static PropTypes to validate that the 'features' returned from queryRenderedFeatures is an array.

// src/components/ToolTipBox.js

import React from 'react'
import PropTypes from 'prop-types'

export default class Tooltip extends React.Component {

 static propTypes = {
   features: PropTypes.array.isRequired
 };

 render() {
   const { features } = this.props;

   const renderFeature = (feature, i) => {
     return (
       <div key={i}>
         <strong className='mr3'>{feature.layer['source-layer']}:</strong>
         <span className='color-gray-light'>{feature.layer.id}</span>
       </div>
     )
   };

    return (
      <div className="flex-parent-inline absolute bottom">
        <div className="flex-child">
          {features.map(renderFeature)}
        </div>
      </div>
    );}}
Enter fullscreen mode Exit fullscreen mode

There's a lot going on with CSS here, and you'll notice the actual example I am copying from had more styling, but I removed it and added some to my own App.css for simplicity of code blocks here. Here's what I added to my CSS after this step:

.flex-parent {
  flex-direction: column;
  position: absolute;
}
.flex-child {
  color: white;
  background: gray;
  text-overflow: clip;
  padding: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

Pretty simple, just enough styling to see a basic box show up. Not that aesthetic, but I can come back to that later, and so you can you!

Either way, though, unless you want to completely define all your own CSS, which I did not, you should probably have your index.html looking like the example as well, as they import stylesheets here from mapbox:

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="utf-8" />
   <link rel="icon" href="%PUBLIC_URL%/map.png" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
   <meta name="theme-color" content="#000000" />
   <meta
     name="description"
     content="Web site created using create-react-app"
   />
   <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
   <link href='https://api.mapbox.com/mapbox-assembly/mbx/v0.18.0/assembly.min.css' rel='stylesheet'>
   <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.css' rel='stylesheet' />
   <title>MapBox React Example</title>
 </head>
   <div id="root"></div>
   <script src='https://api.mapbox.com/mapbox-assembly/mbx/v0.18.0/assembly.js'></script>
 </body>
</html>
Enter fullscreen mode Exit fullscreen mode

React Tooltip Library

This post is already a little long so I won't actually go into react-portal-tooltip. But one very annoying thing I overcame while exploring it and thought worth sharing - if you get this guy:

Alt Text
Alt Text
Alt Text
Alt Text

There are many solutions on StackOverflow. This one worked for me:

touch src/declare_modules.d.ts

// in declare_modules.d.ts
declare module "react-portal-tooltip";

// if it still doesn't work, add import in `App.js`
// App.js
...
import './declare_modules.d.ts'
Enter fullscreen mode Exit fullscreen mode

Thanks for reading!

Alt Text

Resources:

. . . . . . . . . . . . . . . .