DEV Community

Cover image for How to create the base of a map with ReactJS, Leaflet and TypeScript
AxelleDRouge
AxelleDRouge

Posted on • Updated on

How to create the base of a map with ReactJS, Leaflet and TypeScript

Hello !

This post is my first attempt at a technical post.
I am a front-end developer in a GIS company. I create maps and I manage geographic data, in the browser.
Here is an easy way I have found to create an easily maintainable and growing map, while being able to add functionalities in your app.

Prerequisites

This tutorial will use ReactJS, with the latest API Hooks and Context, with LeafletJS and TypeScript.
An understanding of ReactJS will be needed in order to follow this tutorial. I will explain the concepts specifics to Leaflet, Hooks and Context, as much as I can. But as nothing can beat the original, you will find the different documentations here :

  1. ReactJS
  2. Leaflet
  3. React-Leaflet

Install the dependencies

  • First, initiate the react app with CRA : Open a terminal in the folder where you will put your project and type the following command :

npx create-react-app <your-project-name> --template typescript

Go in your newly created folder :

cd <your-project-name>

Add the dependencies for LeafletJS and the plugin React-Leaflet to be able to use Leaflet class as Components.

Include the typing for TypeScript. More than "just" strenghening your code with strong types, they will add easily reached documentation and help you create documentation as part of your code.

npm i react-leaflet leaflet @types/react-leaflet

Setup the map

Setup the basis for a Leaflet application, as described in the Leaflet documentation

  • include leaflet in the application : in public/index.html add in the head section, add the leaflet CSS file :
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
   integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
   crossorigin=""/>
Enter fullscreen mode Exit fullscreen mode

then after the leaflet CSS, add the leaflet JS file :

 <!-- Make sure you put this AFTER Leaflet's CSS -->
 <script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
   integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
   crossorigin=""></script>
Enter fullscreen mode Exit fullscreen mode

Create a component Map

In the src folder, go to the file App.tsx and remove its contents, except the import of React. Create a folder named "components" in src/ and move the App.tsx file in this folder.
We could also keep the declaration of the function App, but I prefer to use the function expression method :

const myFunction = () => {}
Enter fullscreen mode Exit fullscreen mode

in src/components/App.tsx :

import React from 'react';

const App:React.FC = () => {
   return (
     <LeafletMap />
   )
}
Enter fullscreen mode Exit fullscreen mode

Create a new component for your Map, that will be called LeafletMap (but you can call it something else, it is just for distinguing from the Map Component from the Leaflet API).

in src/components/LeafletMap.tsx :

import React from 'react';
import { Map } from 'react-leaflet';
import { LatLngTuple } from 'leaflet';

const defaultLatLng: LatLngTuple = [48.865572, 2.283523];
const zoom:number = 8;

const LeafletMap:React.FC = () => {
   return (
     <Map>

     </Map>
   )
}
Enter fullscreen mode Exit fullscreen mode

The Map will need a few basics properties, its center, and its zoom. It will also need a basic width and height, necessary to see the map.

in src/components/LeafletMap.tsx :

import React from 'react';
import { Map } from 'react-leaflet';
import { LatLngTuple } from 'leaflet';

const defaultLatLng: LatLngTuple = [48.865572, 2.283523];
const zoom:number = 8;

const LeafletMap:React.FC = () => {
   return (
     <Map id="mapId"
          center={defaultLatLng}
          zoom={zoom}>

     </Map>
   )
}

export default LeafletMap;
Enter fullscreen mode Exit fullscreen mode

in src/index.css :

...
// those values can ofcourse be adapted depending on the kind of app you are 
// creating, but they must be set in order to see the map
#mapId {
   height:100vh;
   width:100vw;
}
Enter fullscreen mode Exit fullscreen mode

Include a basemap

You have now the base of your map, but you still wont see much in your app.
You first need to include a basemap. Fortunately, even if you don't have an API or a ressource, Leaflet allows you to use OpenStreetMap

In your Map component, add the TileLayer component with the url reference to the OpenStreetMap basemap :

...
    <Map id="mapId"
          center={defaultLatLng}
          zoom={zoom}>
      <TileLayer
         url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
         attribution="&copy; <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors">
      </TileLayer>
   </Map>
...
Enter fullscreen mode Exit fullscreen mode

The components included in the React-Leaflet library use, as props, the same options than the Leaflet class they are encapsulating.

Your map is now ready to be seen. Great !
Open "http://localhost:3000/" in your browser :
Alt Text

Of course, you don't have much to show apart from the basemap, but it is a start, right ?

Now, it is time to add some interaction.

setup the context API

First, we will setup the state management that will be need to interact with the map.

Create the Context Component

In src/components, add a new folder "context".
In this folder, create a new file "LayerContext".
The Context API of React is a solution really appropriate for managing the state of your app. If the app grows in size, and impact, Redux could be used for the same action, but in this case, Context is perfectly efficient.
Mostly, what's Context is allowing us to do is effectively manage the layers and features that will be add to the map from everywhere in the app. It will simplify the creation of menus, overlay or others buttons.
React-Leaflet is actually based on this React Context API.

Start creating your context object using the method React.createContext();

In src/components/context/LayerContext.tsx :

import React from 'react';

const LayerContext:any = React.createContext({});
Enter fullscreen mode Exit fullscreen mode

Then let's write the basis for the Context Provider which will wrap the Map Component, and provide all the states and functions included in it :

... 
\\ under the declaration of the LayerContext, declare the Provider Component

const LayerContextProvider = ({ children }: any) => {

    const defaultValue = {

    }

    return (
        <LayerContext.Provider value={defaultValue}>
            {children}
        </LayerContext.Provider>
    )
}

\\ and export both objects
export { LayerContext, LayerContextProvider };
Enter fullscreen mode Exit fullscreen mode

add a state using the Hook method, it will hold the point that we will add to the Map.

... 
const LayerContextProvider = ({ children }: any) => {
    const [point, setPoint] = useState<LatLng>([0, 0]);

    const defaultValue = {
        point,
        setPoint
    }

Enter fullscreen mode Exit fullscreen mode

Add the Provider around the Map Component

Go back to your App Component and surround the LeafletMap component with your newly-created LayerContextProvider. You will be able to manipulate your geometries from anywhere in the App, if it is in the Provider.

const App: React.FC = () => {

    return (
        <LayerContextProvider>
            <LeafletMap />
        </LayerContextProvider>
    )
}
Enter fullscreen mode Exit fullscreen mode

In your Map component, add the LayerGroup to receive your point

    const { point } = useContext(LayerContext);

    return (
    <Map id="mapId"
          center={defaultLatLng}
          zoom={zoom}>
      <LayerGroup>
      {point}
      </LayerGroup>
      <TileLayer
         url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
         attribution="&copy; <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors">
      </TileLayer>
   </Map>
  )
...
Enter fullscreen mode Exit fullscreen mode

setup the onClick hook

In your component folder, create a new customHooks folder.
The Rules of Hooks informs us that all hooks must start with "use".
In this folder, create a new useAddMarker.tsx for your custom hooks.

import { useContext, useEffect } from 'react';
import { useLeaflet, Marker } from 'react-leaflet';
import L, { LeafletMouseEvent } from 'leaflet';
import { LayerContext } from '../context/LayerContext';

// the hook Effect will be activated by the click on the button
function useAddMarker(selected:boolean) {

// The hook 'useLeaflet' is provided by the react-leaflet library. 
// This hook allow to access to the Leaflet Context and its variables. 
// It is a simple way to access the map and its content.

    const { map } = useLeaflet();

// the hook useContext is used to access to the previously defined LayerContext.
    const { setPoint } = useContext(LayerContext);

// add a state to activate the Event
    const [activate, setActivate] = useState(selected);

// define the MouseEvent with the useCallback hook 
    const markerEvent = useCallback(
        (e: LeafletMouseEvent) => {
            // if you want to use any event, 
            // be sure that the default is disabled.
            e.originalEvent.preventDefault();
            // create your Marker with the react leaflet component Marker
            setPoint(<Marker position={e.latlng} />);
            e.originalEvent.stopPropagation();
        }, [setPoint]);


    // activate the EventHandler with the useEffect handler
    useEffect(
        () => {
            map?.doubleClickZoom.disable()
            if (activate === true) {
                map?.on('dblclick', markerEvent);
            }
            return () => {
                map?.off('dblclick', markerEvent);
            }
        }, [map, activate, markerEvent]
    )
}

export default useAddMarker;

Enter fullscreen mode Exit fullscreen mode

use a button to activate the hook

Now that every functionnality are created, you just need to connect everything.

Create a button that will activate the AddMarker Hook.
In src/components, create a new AddMarkerButton Component. It will be used to add the function to any

In src/components/AddMarkerButton.tsx :

import useAddMarker from './customHooks/useAddMarker';

interface Props {}

const AddMarkerButton: React.FC<Props> = (props) => {
    const { setActivate, activate } = useAddMarker(false);

    return <button onClick={() => setActivate(!activate)}>Add Points</button>
}

export default AddMarkerButton;
Enter fullscreen mode Exit fullscreen mode

In src/index.css

button{
  position: absolute;
  top: 50px;
  left: 150px;
  z-index:2000;
  width:100px;
  height:50px;
  color:gray;
}
Enter fullscreen mode Exit fullscreen mode

Finish by adding your button component to the Map

...
    <Map id="mapId"
         center={defaultLatLng}
         zoom={zoom}>
            <AddMarkerButton />
            <LayerGroup>
                {point}
            </LayerGroup>
            <TileLayer
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                attribution="&copy; <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors">
            </TileLayer>
      </Map>
...
Enter fullscreen mode Exit fullscreen mode

Conclusion

You now have a ready to use and complete map.
You have currently only one marker that you can change at every double click. But it could be a List of Marker, creating a more interesting Layer.
You could now add some data, using the context to organise any actions with the geometry and the map.
I have recently found that GraphQL, using Hasura for the PostGreSQL/PostGIS implementation, goes really well with this logical.

That's it!

Open "http://localhost:3000/" in your browser :
Alt Text

Top comments (10)

Collapse
 
ibrahimawadhamid profile image
Ibrahim Awad

Thanks for the amazing content. It helped me get started, I'm trying to make an Ionic5 application with React support that includes leaflet maps. I've been using leafletJs in vanilla JS and in ReactJS before, but this is my first time to use it in typescript and in Ionic. Wish me luck (Y)

Collapse
 
axelledrouge profile image
AxelleDRouge

Good Luck ! :) I don't know Ionic, I would be interested to know more about how it went

Collapse
 
vikinatora profile image
Viktor Todorov

Amazing article, it helped me a lot with starting my univeristy project using this exact stack!
I think one thing you have missed at the end of useAddMarker hooks is return { activate, setActivate } as I was getting error when trying to destructure the hook in the AddMarkerButton component.

Collapse
 
axelledrouge profile image
AxelleDRouge

You're welcome, happy to know it helped
oh yes probably, I created this article based on an app I created for my work, I simplified a lot, I may have deleted too much, or made mistakes in copying the function

Collapse
 
javiarch profile image
javiarch

Axelle. Excelente publicación!!
Gracias por compartirlo. Saludos desde Argentina

Collapse
 
axelledrouge profile image
AxelleDRouge

Thank you !

Collapse
 
kingstontwelve profile image
KingstonTwelve • Edited

Help me!)
Module '"../../node_modules/react-leaflet/types"' has no exported member 'Map'

Collapse
 
axelledrouge profile image
AxelleDRouge

Okay but I can't if you don't tell me how

Collapse
 
kingstontwelve profile image
KingstonTwelve

Thank u for answer :) I resolved error.
Your post helped me.

Thread Thread
 
axelledrouge profile image
AxelleDRouge

Ah okay great you are welcome