paint-brush
Maps on the Web Using React and Mapboxby@rusanovn
2,169 reads
2,169 reads

Maps on the Web Using React and Mapbox

by Nikita RusanovFebruary 1st, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

I will illustrate how to use Mapbox with React and explain how these technologies work together.
featured image - Maps on the Web Using React and Mapbox
Nikita Rusanov HackerNoon profile picture

Web applications have greatly benefited from the inclusion of maps, providing users with valuable location-based information. Maps have transformed our interaction with the world, from navigating unfamiliar places to discovering nearby eateries. As a result, the integration of maps into websites has become increasingly popular in recent times. Nevertheless, designing maps that are both functional and user-friendly can present a challenge, especially for those lacking experience in this domain. In this article, I will share useful tips on how to create effective maps within your browser.

Tech stack

Let’s discuss technologies. When working with maps, we typically use three layers:

  • Rendering the user interface, which includes buttons and forms. In our stack, React performs this role;

  • Rendering the map and enabling user interaction. We use Mapbox for this;

  • Fetching data from the server, such as information about markers or polygons. To retrieve data, we use the browser’s built-in fetch feature.


Let’s review each item to gain a better understanding of our technology stack when working with maps.

React

Markers and map controls — these are React components.


The React library enables you to work with page elements conveniently and efficiently. It was developed by Facebook for their own use, and includes numerous components such as buttons, forms, and other interactive elements on the pages. For example, Facebook created a reconciliation algorithm to quickly compare different states.


As changing elements on a page is the most expensive operation for a browser, it’s essential to do it as efficiently as possible. To address this issue, Facebook engineers developed the React library, which enables fast and straightforward element changes on a page. Besides providing rapid state changes on a page, React allows us to do this declaratively without working with DOM elements directly. Instead, we use an abstraction, usually JSX, which looks like HTML. Let’s consider an example:

// It's our state. Is the user our friend or not?
// false by default
const [isFriend, setIsFriend] = useState(false)

// Depending on the state, we show the text on the button
const buttonText = isFriend ? 'Your my Friend' : 'Add as Friend'

// There is JSX, syntax for UI
// In this case, we display a button, when clicked, we change the state
return (
  <button onClick={() => setIsFriend(true)}>{buttonText}</button>
)


It is possible to nest components with ordinary DOM elements like forms, buttons, and inputs at the bottom of the hierarchy. By assembling these simple elements, we can create more complex ones, such as a complete form:

const Form = () => (
  <form>
      <input name="Email"/>
      <input name="Password"/>
  </form>
)

const App = () => (
  <main>
      <h1>My form!</h1>
      <Form />
  </main>
)


How does React assist us in the context of maps? As the map on the page is interactive, similar to a button or form, we aim to optimize its rendering and interaction through events like clicks on the map. React can help achieve this optimization. Here is an example of how it works:

// Use React to render the map with different event handlers
// and render markers
return (
  <BaseMap
    onInitMap={() => console.log('I am alive!')}
    onClickMap={() => console.log('Click!')}
    onDestroyMap={() => console.log('Oh no!')}
  >
    <ClustersMarkers />
    <PostsMarkers />
    <ListingsMarkers />
  </BaseMap>
)


When working with React, it is essential to remember that it allows for efficient manipulation of elements on the page, quick changes, and interaction with them through events. This is achieved through an abstraction that resembles HTML, making it easy to create complex components from simpler ones.

Mapbox

Mapbox allows us to display our own data on the map.


Now, let’s discuss the map itself. Creating and using maps can be challenging, and only a few product companies can design maps from scratch. Typically, most people rely on pre-made libraries with a user-friendly API that has been tried and tested.


Numerous dynamic map providers are available, including Google Maps, Leaflet, Bing Maps, Mapbox, and more. However, we will focus on Mapbox due to its extensive features and fair pricing policy. For instance, Google Maps charges $700 for 100k map views per month, while Mapbox only charges $250. Moreover, Mapbox offers free access for up to 50k map views per month.


Mapbox offers Mapbox Studio, a tool often compared to Photoshop for maps. With this tool, designers can create custom styles, hide unnecessary map elements, and remove house displays. One of its benefits is the ability to enhance the visibility of Points of Interest. Another advantage is the opportunity to experiment with map styling in corporate colors. However, it’s essential to consider the user experience and maintain a familiar color palette, including green grass and blue water. The best part about Mapbox Studio is that it simplifies the map design process by eliminating the need for developing and transferring requirements, ultimately reducing the cost of working with maps.


Additionally, Mapbox provides a geocoding tool that can convert addresses to coordinates or vice versa, simplifying the process of locating points on a map. However, the tool’s worldwide coverage may be insufficient, leading to difficulties for certain projects. Mapbox gathers data from numerous sources, such as governments, open data initiatives like OpenStreetMap, and private companies.


Examples of using Mapbox.

Mapbox provides a variety of practical examples, although the documentation could benefit from some improvement. For instance, when incorporating marker functionality, examples can be more informative than the documentation. Additionally, challenges may arise when merging multiple components, such as marker clusters, custom data loading, normalization, and marker state changes.


Let’s revisit Mapbox maps. What does the Mapbox API do?


  • It initializes the map in an HTML element on the page;

  • It loads and renders images that make up the map;

  • It draws additional elements, such as markers, using GeoJson as input data;

  • It generates events, such as clicks or zoom changes, that can be handled.


Let’s take a closer look at each of these items.


The map is divided into tiles (512x512).

Mapbox specializes in map rendering using tiles. Tiles are small square images that make up the larger map. The default size of a tile is 512x512 pixels, and it can be either vector or rasterVector tiles are used to display roads, buildings, Points of Interest (PoI), and more. They can be dynamically styled, are lightweight, and allow for smooth interaction with the map. Raster tiles, on the other hand, are used for displaying satellite imagery.


Just so you know, Mapbox Studio allows us to choose the specific data we want to include in the map tiles. These tiles are then placed onto a canvas, which is a special DOM element in the browser that displays images and other graphical elements on the web. To give you an idea, it’s similar to how documents are displayed on a canvas in Google Docs.

<canvas width="100" height="100" />


Mapbox handles the loading, insertion, and updating of tiles. All we have to do is specify where we want the map to be displayed and the initial conditions, like the zoom level or the map’s coordinates. To use Mapbox, you’ll need an access token, which is a unique key that can be found in your Mapbox account. To set up a basic map, here’s a quick example, but for more information, check out the link provided:

mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';

const map = new mapboxgl.Map({
  container: 'map', // we can use an Id or an element
  style: 'mapbox://styles/mapbox/streets-v11', // URL for styles
  center: [-74.5, 40], // initial coordinates [lng, lat]
  zoom: 9, // initial zoom
});


After this, we will get a map on the page in an element with the id ‘map.’


Displaying the map without additional content.


To provide users with more information on the map, we often display the location of a certain establishment or the boundaries of a specific area. To achieve this, we use a specific data format called GeoJSON and instruct Mapbox on how to showcase this data.


GeoJSON is the standard format for storing geographic structures on maps. It can store various primitive types that describe geographic objects such as addresses, locations, streets, highways, borders, countries, states, and combinations of these, known as multipart. GeoJSON was introduced in 2008 and is represented like this:

{
  "type": "Feature", // also can be FeatureCollection, it's collection of Feature
  "geometry": {
    "type": "Point", // also can be LineString, Polygon, MultiPolygon
    "coordinates": [125.6, 10.1] // for other types you can use Array with coordinates
  },
  "properties": { // it's metadata, we can you that to show something on the map
    "name": "Dinagat Islands"
  }
}


Let’s talk about the coordinate system used in Mapbox. By default, Mapbox employs EPSG 3857, which is a way of projecting the Earth onto a flat surface to work with tiles. This is known as Web Mercator and is the standard for online maps. However, when dealing with data about markers and polygons, it uses a different coordinate system called EPSG 4326. This system relies on latitude and longitude to describe coordinates on the Earth’s ellipsoid. Mapbox and other map providers automatically convert coordinates from EPSG 4326 to EPSG 3857. If we need to work with different projections, we can use the map.setProjection method.


Now, we will discuss how to display GeoJSON on the map. Mapbox offers two entities that we will find helpful:

  • Source — This is the data seed. We can create a new source with GeoJSON data and configure it, for example, to generate an ID for each feature in the feature collection.

  • Layer — This is the data representation. We can display the data from the source in different ways, such as showing boundaries.


To display polygons or markers on the map, we must retrieve the data in GeoJson format from the server. Then, we create a source, input the data into it, and connect it to the required layer.

const geoJsonFeature = {
  'type': 'Feature',
  'geometry': {
      'type': 'Polygon',
      'coordinates': [
          [-67.13734, 45.13745],
          [-66.96466, 44.8097],
          [-68.03252, 44.3252],
          [-67.13734, 45.13745]
      ]
  }
}

// Create source with our data
map.addSource('ourSource', {
  'type': 'geojson',
  'data': geoJsonFeature
});

// Add layer for background
map.addLayer({
  'id': 'background',
  'type': 'fill',
  'source': 'ourSource', // название нашего source
  'layout': {},
  'paint': {
      'fill-color': '#0080ff',
      'fill-opacity': 0.5
  }
});

// Add layer for border
map.addLayer({
  'id': 'border',
  'type': 'line',
  'source': 'ourSource',
  'layout': {},
  'paint': {
      'line-color': '#000',
      'line-width': 3
  }
});


After running this code, we get the result:

We can display our own data on Mapbox.


To learn more about this topic, you can refer to the documentation provided by Mapbox. In this tutorial, we have covered the process of initializing the map and presenting our data. Next, we will explore how to handle events like clicking, dragging, and zooming. For instance, we can use Mapbox to monitor events and display the coordinates in the console as the user moves their cursor on the map. To achieve this, we simply call the on method with the event type we want, similar to how we work with DOM elements.

map.on('mousemove', (e) => {
  console.log(JSON.stringify(e.point));
});

// Result: {"x":330,"y":49}


In summary, what do we need to remember? Mapbox allows us to display a map, draw our data on top of it, and process map events. At the same time, Mapbox takes care of loading and displaying images (tiles).

Fetch

Fetch allows us to load data for the map.


A word about fetch. We have already seen how to render data on a map, but first, we must retrieve it from the server. When we dynamically request data from the server in the background without reloading the page, we call this approach AJAX (“Asynchronous JavaScript and XML”). There are many tools for loading data asynchronously from the server, such as Axios or XMLHttpRequest (native).


What to remember? We retrieve data from the server, and there are many libraries for this, but we will use fetch. Next, we will look at how we do this specifically when working with maps, as there are nuances.

React + Mapbox

Now let’s see how the technologies described above work together. First, we will retrieve the data for displaying the polygon using fetch. Then we will declare the initialization of the map, and after it loads, we will add the polygon to the map.


You can also find a working example at the link provided.

const useFetch = () => {
  /*
  Our data
  {
      'type': 'Feature',
      'geometry': {
          'type': 'Polygon',
          'coordinates': [
              [
                  [-67.13734, 45.13745],
                  [-68.03252, 44.3252],
                  [-68.90478, 47.18479],
                  [-67.13734, 45.13745],
              ]
          ]
      }
  }
  */
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('https://our-api.com/polygon')
      .then(response => response.json())
      .then(setData)
      .catch(e => {
        console.error(e)
      })
  }, [setData])

  return { data }
}

const BaseMap = () => {
  // Use the hook to fetch data
  const { data } = useFetch(GET_REGION);

  // Map instance
  const map = useRef(null);
  // DOM element
  const mapContainer = useRef(null);

  // Main logic - init the map and add the event
  useEffect(() => {
    if (map.current) {
      return; // initialize map only once
    }

    mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
    map.current = new mapboxgl.Map({
      container: mapContainer.current,
      style: 'mapbox://styles/mapbox/light-v10', // style URL (it's Mapbox's core style)
      center: [-68.137343, 45.137451], // starting position
      zoom: 5 // starting zoom
    });

    // Handle event
    map.on('load', () => {
      const sourceId = 'source-region'

      // Add a data source containing GeoJSON data
      map.addSource(sourceId, {
        'type': 'geojson',
        'data': data.region // our data from Apollo
      });

      // Add a new layer to visualize the polygon
      map.addLayer({
        'id': 'background',
        'type': 'fill',
        'source': sourceId, // reference the data source
        'paint': {
            'fill-color': '#0080ff', // blue color fill
            'fill-opacity': 0.5
        }
      });
      // Add a black outline around the polygon
      map.addLayer({
        'id': 'outline',
        'type': 'line',
        'source': sourceId,
        'paint': {
            'line-color': '#000',
            'line-width': 3
        }
      });
    });
  });

  return <div ref={mapContainer} />;
}


Mapbox and React make it easy to work with maps on the web.

Conclusion

We looked at the technology stack that underpins our future architecture. In the following article, we will discuss the principles that help design a map architecture, how to achieve maximum low coupling and high cohesion of modules, and how to maintain and develop a scalable map system.


Thank you very much for your attention! Have a great day.