Setting up React Leaflet with Mapbox and Vite

I believe that everyone at some point has added a map to a website, or tried to create an app to share with friends all the places they've traveled.

And also at some point, you used iframes to add maps on websites, at least to show where a business or a store is located.

However we are stuck with what is offered to us by the services we use, such as Google's apis, but nowadays we have an amazing service called Mapbox where we can style the maps the way we want and have a free plan that in my opinion seems to be generous.

First we will make a map using only the tiles that come by default in Leaflet. And if you are satisfied with the result, you can leave it at that. But if you want to use the Mapbox tiles, read the article until the end.

The application we are going to make today will only serve to show your current location. For this we will use the Geolocation Web API, this if the user allows access to the location, otherwise we will make an http request to ipapi (it's not that precise but it helps).

And this time, instead of using the webpack as a bundler, I'm going to use Vite, if you never used it now is the chance to do so.

Let's code

First let's create our project with Vite, for that we'll use the following command:

npm init @vitejs/app [PROJECT_NAME]

Now we can interact with the terminal, first we select our framework, which in our case is react and then the JavaScript language.

Then we will go to our project folder to install the dependencies and start the development environment:

cd [PROJECT_NAME]
npm install
npm run dev

You should now have an app like this on port 3000:

Now we can install the necessary dependencies to be able to work with Leaflet in React:

npm install react-leaflet leaflet axios

First we have to import the leaflet styles into the main file of our application:

// @src/main.jsx

import React from "react";
import ReactDOM from "react-dom";

import "./index.css";
import App from "./App";
import "leaflet/dist/leaflet.css"; // <- Leaflet styles

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

Now we are going to import the React Leaflet components needed to get the map. Make sure to set a height and width for the map.

// @src/app.jsx

import React from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";

const App = () => {
  const position = [51.505, -0.09];
  return (
    <MapContainer
      center={position}
      zoom={13}
      scrollWheelZoom={true}
      style={{ minHeight: "100vh", minWidth: "100vw" }}
    >
      <TileLayer
        attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker position={position}>
        <Popup>
          A pretty CSS3 popup. <br /> Easily customizable.
        </Popup>
      </Marker>
    </MapContainer>
  );
};

export default App;

You should now have an application similar to this one:

As you may have noticed in the code, we have a static position, but since we need to have a dynamic position, we will create a hook to get its current position.

Let's call our hook useMap:

// @src/hooks/index.jsx

export const useMap = () => {
  // Logic goes here
};

First we will create our state using the useState hook, and in it we will store our latitude and longitude positions. I will want the initial state to be in Nantes, France but you can choose another location.

// @src/hooks/index.jsx

import { useState } from "react";

export const useMap = () => {
  const [position, setPosition] = useState({
    lat: 47.21725,
    lng: -1.55336,
  });
  // More logic goes here
};

Then we will use the useEffect hook to make it run only when the page is rendered for the first time. And we know that the function's return is just going to be the position.

// @src/hooks/index.jsx

import { useState, useEffect } from "react";

export const useMap = () => {
  const [position, setPosition] = useState({
    lat: 47.21725,
    lng: -1.55336,
  });
  useEffect(() => {
    // More logic goes here
  }, []);
  return { position };
};

The next step is to access our location via the Web API and we will store those same data.

// @src/hooks/index.jsx

import { useState, useEffect } from "react";
import axios from "axios";

export const useMap = () => {
  const [position, setPosition] = useState({
    lat: 47.21725,
    lng: -1.55336,
  });
  useEffect(() => {
    navigator.geolocation.getCurrentPosition(
      ({ coords }) => {
        setPosition({ lat: coords.latitude, lng: coords.longitude });
      },
      (blocked) => {
        // More logic goes here
        }
      }
    );
  }, []);
  return { position };
};

However, if the user (or the device he is using) blocks access to his location, we will have to make an http request to an Api. For this we will use the axios and we will store the response data in our state.

The final code of our hook should look like this:

// @src/hooks/index.jsx

import { useState, useEffect } from "react";
import axios from "axios";

export const useMap = () => {
  const [position, setPosition] = useState({
    lat: 47.21725,
    lng: -1.55336,
  });
  useEffect(() => {
    navigator.geolocation.getCurrentPosition(
      ({ coords }) => {
        setPosition({ lat: coords.latitude, lng: coords.longitude });
      },
      (blocked) => {
        if (blocked) {
          const fetch = async () => {
            try {
              const { data } = await axios.get("https://ipapi.co/json");
              setPosition({ lat: data.latitude, lng: data.longitude });
            } catch (err) {
              console.error(err);
            }
          };
          fetch();
        }
      }
    );
  }, []);
  return { position };
};

Now we can go back to our map component again and we can import our hook to access our location in a dynamic way. And we'll change the zoom of the map from 13 to 4.5 (to see a larger area).

// @src/app.jsx

import React from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";

import { useMap } from "./hooks";

const App = () => {
  const { position } = useMap();
  return (
    <MapContainer
      center={position}
      zoom={4.5}
      scrollWheelZoom={true}
      style={{ minHeight: "100vh", minWidth: "100vw" }}
    >
      <TileLayer
        attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker position={position}>
        <Popup>
          A pretty CSS3 popup. <br /> Easily customizable.
        </Popup>
      </Marker>
    </MapContainer>
  );
};

export default App;

The current result should look very similar to this:

If you are satisfied with the result, you can stop here, but if you want to have some different tiles, keep reading the article because now we are going to use the Mapbox tiles.

First go to the Mapbox website and create an account.

Then go to Mapbox Studio and create a new style.

Then you can select the template that you want and its variant. In this case I will use the Basic template and the Galaxy variant.

In the configuration UI of the map, click on share and check if in the production tab you can find the Style URL and the Access Token.

Now at the root of our project, let's create an .env to store our environment variables. In the Style URL link you will have the username and the style id.

VITE_USERNAME=
VITE_STYLE_ID=
VITE_ACCESS_TOKEN=

Now back to our map component, let's import our environment variables as follows:

// @src/app.jsx

import React from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";

import { useMap } from "./hooks";

const { VITE_USERNAME, VITE_STYLE_ID, VITE_ACCESS_TOKEN } = import.meta.env;

// Hidden for simplicity

And in the <TileLayer /> component, we're going to replace the attribution and the url. In the url we will add the link to get Mapbox tiles by dynamically passing the values of our environment variables. Just like we're going to give Mapbox credits in attribution. Like this:

// @src/app.jsx

// Hidden for simplicity

const App = () => {
  const { position } = useMap();
  return (
    <MapContainer
      center={position}
      zoom={4.5}
      scrollWheelZoom={true}
      style={{ minHeight: "100vh", minWidth: "100vw" }}
    >
      <TileLayer
        attribution='Imagery &copy; <a href="https://www.mapbox.com/">Mapbox</a>'
        url={`https://api.mapbox.com/styles/v1/${VITE_USERNAME}/${VITE_STYLE_ID}/tiles/256/{z}/{x}/{y}@2x?access_token=${VITE_ACCESS_TOKEN}`}
      />
      // Hidden for simplicity
    </MapContainer>
  );
};

The map component code should be as follows:

// @src/app.jsx

import React from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";

import { useMap } from "./hooks";

const { VITE_USERNAME, VITE_STYLE_ID, VITE_ACCESS_TOKEN } = import.meta.env;

const App = () => {
  const { position } = useMap();
  return (
    <MapContainer
      center={position}
      zoom={4.5}
      scrollWheelZoom={true}
      style={{ minHeight: "100vh", minWidth: "100vw" }}
    >
      <TileLayer
        attribution='Imagery &copy; <a href="https://www.mapbox.com/">Mapbox</a>'
        url={`https://api.mapbox.com/styles/v1/${VITE_USERNAME}/${VITE_STYLE_ID}/tiles/256/{z}/{x}/{y}@2x?access_token=${VITE_ACCESS_TOKEN}`}
      />
      <Marker position={position}>
        <Popup>
          A pretty CSS3 popup. <br /> Easily customizable.
        </Popup>
      </Marker>
    </MapContainer>
  );
};

export default App;

The end result of our application should look as follows:

I hope it helped and that it was easy to understand! 😁
Have a nice day! 😉

7