JavaScript Development Space

Create Stunning Maps Effortlessly - Master React Leaflet with TypeScript!

Add to your RSS feed30 September 202414 min read
Create Stunning Maps Effortlessly - Master React Leaflet with TypeScript!

In today’s web development landscape, integrating maps into applications has become increasingly popular. Whether you’re building a location-based service, a travel app, or a simple data visualization project, having a reliable mapping library is essential. This article will guide you through creating a simple map using React, TypeScript, and LeafletJS. By the end, you'll have a functional map application and a solid understanding of how to leverage React Leaflet for your projects.

What is React Leaflet?

React Leaflet is a powerful library that provides React components for Leaflet, a popular open-source JavaScript library for interactive maps. It allows developers to easily integrate and customize maps in React applications while taking advantage of the flexibility and capabilities of LeafletJS. With React Leaflet, you can create responsive, interactive maps that can display various geographic data layers and user interactions.

Setting Up Your Project

1. Create a New React Project

First, create a new React project using Vite with TypeScript template:

npm create vite@latest react-leaflet --template react-ts
cd react-leaflet
npm install

2. Install Required Packages

Next, install the necessary packages, including React Leaflet and Leaflet:

npm install react-leaflet leaflet

Also, you will need to install the types for Leaflet:

npm install -D @types/leaflet

3. Include Leaflet CSS

To properly display the map, you need to include Leaflet’s CSS. Open the src/App.tsx file and add the following line:

js
1 import 'leaflet/dist/leaflet.css';

Creating Your First Map

1. Set Up the Map Component

Now, let’s create a simple map component. Create a new file called MapComponent.tsx in the src directory:

tsx
1 // src/MapComponent.tsx
2
3 import React from 'react';
4 import { MapContainer, TileLayer } from 'react-leaflet';
5 import 'leaflet/dist/leaflet.css';
6
7 const MapComponent: React.FC = () => {
8 return (
9 <MapContainer center={[1.3521, 103.8198]} zoom={13} style={{ height: '100vh', width: '100%' }}>
10 <TileLayer
11 url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
12 attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
13 />
14 </MapContainer>
15 );
16 };
17
18 export default MapComponent;

2. Update Your App Component

Next, import the MapComponent into your main App.tsx file and render it:

tsx
1 // src/App.tsx
2
3 import React from 'react';
4 import MapComponent from './MapComponent';
5
6 const App: React.FC = () => {
7 return (
8 <div>
9 <h1>My Simple Map</h1>
10 <MapComponent />
11 </div>
12 );
13 };
14
15 export default App;
  1. Run Your Application

Now, you can run your application to see the map:

npm run dev

Open your browser and navigate to http://localhost:5173/. You should see a simple map centered on Singapore!

Map result

Let’s style the map. Open index.css, clear its contents, and add the following class for .leaflet-container:

css
1 .leaflet-container {
2 width: 100vw;
3 height: 80vh;
4 }
Centered map

Adding Markers and Popups

1. Install Marker Component

To add markers and popups, you will need the Marker and Popup components from React Leaflet. Modify your MapComponent as follows:

tsx
1 // src/MapComponent.tsx
2
3 import React from 'react';
4 import L from 'leaflet';
5 import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
6 import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
7 import markerIcon from 'leaflet/dist/images/marker-icon.png';
8 import markerShadow from 'leaflet/dist/images/marker-shadow.png';
9
10 import 'leaflet/dist/leaflet.css';
11
12 // Default marker icon
13 delete L.Icon.Default.prototype._getIconUrl;
14 L.Icon.Default.mergeOptions({
15 iconRetinaUrl: markerIcon2x,
16 iconUrl: markerIcon,
17 shadowUrl: markerShadow,
18 });
19
20 const MapComponent: React.FC = () => {
21 return (
22 <MapContainer center={[1.3521, 103.8198]} zoom={13} style={{ height: '100vh', width: '100%' }}>
23 <TileLayer
24 url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
25 attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
26 />
27 <Marker position={[1.3521, 103.8198]}>
28 <Popup>
29 A pretty CSS3 popup. <br /> Easily customizable.
30 </Popup>
31 </Marker>
32 </MapContainer>
33 );
34 };
35
36 export default MapComponent;

2. Test Your Markers

Run your application again. You should now see a marker on the map, and clicking it will open a popup.

Map marker

Add Shapes to the Map

Here’s how to add different shapes like polygons, circles, and rectangles to your map. Modify your MapComponent.tsx:

tsx
1 // src/MapWithShapes.tsx
2
3 import React from 'react';
4 import { MapContainer, TileLayer, Polygon, Circle, Rectangle } from 'react-leaflet';
5 import 'leaflet/dist/leaflet.css';
6
7 const MapWithShapes: React.FC = () => {
8 // Define positions for Polygon, Circle, and Rectangle
9 const polygonPositions = [
10 [51.51, -0.12],
11 [51.51, -0.1],
12 [51.52, -0.1],
13 ];
14
15 const circleCenter = [51.505, -0.09];
16 const rectangleBounds = [
17 [51.49, -0.08],
18 [51.5, -0.06],
19 ];
20
21 return (
22 <MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: '100vh', width: '100%' }}>
23 <TileLayer
24 url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
25 attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
26 />
27
28 {/* Add Polygon */}
29 <Polygon positions={polygonPositions} color='purple' />
30
31 {/* Add Circle */}
32 <Circle center={circleCenter} radius={500} color='blue' />
33
34 {/* Add Rectangle */}
35 <Rectangle bounds={rectangleBounds} color='green' />
36 </MapContainer>
37 );
38 };
39
40 export default MapWithShapes;
  1. Explanation of the Code

Polygon:

  • A polygon is drawn by specifying an array of latitude and longitude points in the positions prop.
  • In the example above, a triangle-shaped polygon is created with three sets of coordinates.
  • You can change the color property to any valid CSS color.

Circle:

  • A circle is drawn by providing a center prop with a latitude and longitude and a radius (in meters).
  • In this case, a blue circle with a 500-meter radius is drawn.

Rectangle:

  • A rectangle is defined by two opposite corners using the bounds prop.
  • The rectangle is rendered as a green box on the map.
  1. Run Your Application

To see the shapes on your map, run the application:

npm run dev
Map with Shape
  1. Customizing Shapes

You can further customize the shapes by adjusting properties like:

  • Color: Modify the color prop to change the border color of the shape.
  • Fill color: Use the fillColor prop to set the inside color of the shape.
  • Stroke weight: Adjust the border thickness with the weight prop.
  • Opacity: Use fillOpacity and opacity to control transparency.

Example:

tsx
1 <Circle
2 center={circleCenter}
3 radius={500}
4 color='red'
5 fillColor='pink'
6 fillOpacity={0.5}
7 weight={2}
8 />

With React Leaflet, adding shapes such as polygons, circles, and rectangles to your maps is simple and efficient.

Common React Leaflet Events

Let’s explore how you can use React Leaflet events in your project, including how to listen for and respond to events like map clicks, marker drags, and zoom changes.

Here are some of the most commonly used events in React Leaflet:

  • Map Events: onClick, onZoom, onMove, onLoad
  • Marker Events: onClick, onDragEnd, onMouseOver, onPopupOpen
  • Layer Events: onAdd, onRemove, onPopupOpen

Each component (such as MapContainer, Marker, Polygon) can listen for different events depending on its type. You can pass event handlers directly to these components as props.

Example: Handling Map Click and Marker Events

Let’s create an example where we listen to map clicks and move markers to the changes.

Create MapWithEvents.tsx component:

tsx
1 import { useState } from 'react';
2 import { Marker, Popup, useMapEvents } from 'react-leaflet';
3
4 const MapWithEvents = () => {
5 const [position, setPosition] = useState([1.3521, 103.8198]);
6 const map = useMapEvents({
7 click(e) {
8 setPosition(e.latlng);
9 map.flyTo(e.latlng, map.getZoom());
10 },
11 });
12
13 return position === null ? null : (
14 <Marker position={position}>
15 <Popup>You are here</Popup>
16 </Marker>
17 );
18 };
19
20 export default MapWithEvents;

Explanation:

  • onClick Event: Captures the latitude and longitude of the point where the user clicks on the map and updates the state with that information.

Display the Map

Finally, you need to render the MapWithShapes component in your main MapComponent.tsx file:

tsx
1 import React, { useState } from 'react';
2 import L from 'leaflet';
3 import { MapContainer, TileLayer, Marker, Popup, Rectangle, Circle, Polygon } from 'react-leaflet';
4 import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
5 import markerIcon from 'leaflet/dist/images/marker-icon.png';
6 import markerShadow from 'leaflet/dist/images/marker-shadow.png';
7 import MapWithEvents from './MapWithEvents';
8
9 import 'leaflet/dist/leaflet.css';
10
11 // Default marker icon
12 delete L.Icon.Default.prototype._getIconUrl;
13 L.Icon.Default.mergeOptions({
14 iconRetinaUrl: markerIcon2x,
15 iconUrl: markerIcon,
16 shadowUrl: markerShadow,
17 });
18
19 // Define positions for Polygon, Circle, and Rectangle
20 const polygonPositions = [
21 [1.3521, 103.8198],
22 [1.3521, 103.8197],
23 [1.3521, 103.8196],
24 ];
25
26 const circleCenter = [1.3521, 103.8198];
27 const rectangleBounds = [
28 [1.3521, 103.8197],
29 [1.3521, 103.8196],
30 ];
31
32 const MapComponent: React.FC = () => {
33 const [mapClickPosition, setMapClickPosition] = useState<string | null>(null);
34 const [zoomLevel, setZoomLevel] = useState<number>(13);
35
36 // Handler for map click
37 const handleMapClick = (event) => {
38 const { lat, lng } = event.latlng;
39 console.log('handleMapClick');
40 setMapClickPosition(`Latitude: ${lat}, Longitude: ${lng}`);
41 };
42
43 // Handler for zoom change
44 const handleZoomEnd = (event) => {
45 console.log('handleZoomEnd', event);
46 setZoomLevel(event.target.getZoom());
47 };
48
49 return (
50 <MapContainer
51 center={[1.3521, 103.8198]}
52 style={{ height: '100vh', width: '100%' }}
53 zoom={zoomLevel}
54 onClick={(e) => handleMapClick(e)} // Map click event
55 onZoomEnd={handleZoomEnd} // Zoom end event
56 >
57 <TileLayer
58 url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
59 attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
60 />
61 <Marker position={[1.3521, 103.8198]}>
62 <Popup>
63 A pretty CSS3 popup. <br /> Easily customizable.
64 </Popup>
65 </Marker>
66
67 {/* Add Polygon */}
68 <Polygon positions={polygonPositions} color='purple' />
69
70 {/* Add Circle */}
71 <Circle center={circleCenter} radius={500} color='blue' />
72
73 {/* Add Rectangle */}
74 <Rectangle bounds={rectangleBounds} color='green' />
75 <MapWithEvents />
76 </MapContainer>
77 );
78 };
79
80 export default MapComponent;
Leaflet Events

List GeoJSON Location Data in the Map

Let’s display a list of all the parks in Singapore using GeoJSON location data. You can download or copy the list from here - https://data.gov.sg/datasets/d_14d807e20158338fd578c2913953516e/view. If you choose to download it, rename the file to Park-Facilities.json.

Now, let’s create the Parks.tsx component and render it inside the MapComponent.tsx

tsx
1 // Parks.tsx
2
3 import { useEffect } from 'react';
4 import { useMap } from 'react-leaflet';
5
6 import SingaporesParks from './Park-Facilities.json';
7
8 const Parks = () => {
9 const map = useMap();
10 useEffect(() => {
11 console.log('map', map);
12 if (!map) {
13 return;
14 }
15 const parksInSingaporeGeoJson = new L.GeoJSON(SingaporesParks);
16 parksInSingaporeGeoJson.addTo(map);
17 }, [map]);
18 return <></>;
19 };
20 export default Parks;

Connect it to MapComponent

tsx
1 import React, { useState } from 'react';
2 import L from 'leaflet';
3 import { MapContainer, TileLayer, Marker, Popup, Rectangle, Circle, Polygon } from 'react-leaflet';
4 import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
5 import markerIcon from 'leaflet/dist/images/marker-icon.png';
6 import markerShadow from 'leaflet/dist/images/marker-shadow.png';
7 import MapWithEvents from './MapWithEvents';
8
9 import 'leaflet/dist/leaflet.css';
10 import Parks from './Parks';
11
12 // Default marker icon
13 delete L.Icon.Default.prototype._getIconUrl;
14 L.Icon.Default.mergeOptions({
15 iconRetinaUrl: markerIcon2x,
16 iconUrl: markerIcon,
17 shadowUrl: markerShadow,
18 });
19
20 // Define positions for Polygon, Circle, and Rectangle
21 const polygonPositions = [
22 [1.3521, 103.8198],
23 [1.3521, 103.8197],
24 [1.3521, 103.8196],
25 ];
26
27 const circleCenter = [1.3521, 103.8198];
28 const rectangleBounds = [
29 [1.3521, 103.8197],
30 [1.3521, 103.8196],
31 ];
32
33 const MapComponent: React.FC = () => {
34 const [mapClickPosition, setMapClickPosition] = useState<string | null>(null);
35 const [zoomLevel, setZoomLevel] = useState<number>(13);
36
37 // Handler for map click
38 const handleMapClick = (event) => {
39 const { lat, lng } = event.latlng;
40 console.log('handleMapClick');
41 setMapClickPosition(`Latitude: ${lat}, Longitude: ${lng}`);
42 };
43
44 // Handler for zoom change
45 const handleZoomEnd = (event) => {
46 setZoomLevel(event.target.getZoom());
47 };
48
49 return (
50 <MapContainer
51 center={[1.3521, 103.8198]}
52 style={{ height: '100vh', width: '100%' }}
53 zoom={zoomLevel}
54 onClick={(e) => handleMapClick(e)} // Map click event
55 onZoomEnd={handleZoomEnd} // Zoom end event
56 >
57 <TileLayer
58 url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
59 attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
60 />
61 <Marker position={[1.3521, 103.8198]}>
62 <Popup>
63 A pretty CSS3 popup. <br /> Easily customizable.
64 </Popup>
65 </Marker>
66
67 {/* Add Polygon */}
68 <Polygon positions={polygonPositions} color='purple' />
69
70 {/* Add Circle */}
71 <Circle center={circleCenter} radius={500} color='blue' />
72
73 {/* Add Rectangle */}
74 <Rectangle bounds={rectangleBounds} color='green' />
75 <MapWithEvents />
76 <Parks />
77 </MapContainer>
78 );
79 };
80
81 export default MapComponent;
Parks in Singapore

Loading all of these markers took quite some time because Singapore has a large number of parks 🤩

Render names on Markers

To render the names on the parks we need to modify the GeoJSON function:

tsx
1 const parksInSingaporeGeoJson = new L.GeoJSON(SingaporesParks, {
2 onEachFeature: (feature = {}, layer) => {
3 const { properties = {} } = feature;
4 const { Name } = properties;
5 if (!Name) {
6 return;
7 }
8 layer.bindPopup(`<p>${Name}</p>`);
9 },
10 });

Now when we click on the marker, we can see the name of the park, or his id number

Park name

Costumize the Markers With Custom Icons

You can create a custom icon by using L.icon(). This function allows you to set the URL for the icon image, size, anchor points, and more.

Download the icon:

Park icon

Modify the Parks.tsx component:

tsx
1 import { useEffect } from 'react';
2 import { useMap } from 'react-leaflet';
3
4 import ParkIcon from './park-icon.png';
5 import SingaporesParks from './Park-Facilities.json';
6
7 const Parks = () => {
8 const map = useMap();
9
10 useEffect(() => {
11 if (!map) {
12 return;
13 }
14
15 const parkIcon = L.icon({
16 iconUrl: ParkIcon, // URL to your custom icon
17 iconSize: [24, 24], // Size of the icon [width, height]
18 iconAnchor: [16, 32], // Point of the icon that will correspond to marker's location
19 popupAnchor: [0, -32], // Point from which the popup should open relative to the iconAnchor
20 });
21
22 const parksInSingaporeGeoJson = new L.GeoJSON(SingaporesParks, {
23 pointToLayer: (feature = {}, latlng) => {
24 return L.marker(latlng, {
25 icon: parkIcon,
26 });
27 },
28 onEachFeature: (feature = {}, layer) => {
29 const { properties = {} } = feature;
30 const { Name } = properties;
31 if (!Name) {
32 return;
33 }
34 layer.bindPopup(`<p>${Name}</p>`);
35 },
36 });
37 parksInSingaporeGeoJson.addTo(map);
38 }, [map]);
39 return <></>;
40 };
41 export default Parks;
Map With Custom Icons

The Explanation:

  1. L.icon(): This method is used to define a custom Leaflet icon.
  • iconUrl: The URL or path to the image file for the custom icon.
  • iconSize: Defines the width and height of the icon.
  • iconAnchor: Specifies which point of the icon image corresponds to the exact geographical location (usually the bottom center of the image).
  • popupAnchor: Determines the position of the popup relative to the icon.
  1. pointToLayer: A function that is called for each point feature in the GeoJSON file. It converts each park location (latlng) into a marker with the custom parkIcon.

Toggle Between Different Lyers With LayersControl

To add a Layers Control to a React Leaflet map, you can use the LayersControl component provided by React Leaflet. This allows you to toggle between different layers on the map.

Setup the Layers Control

Now, let’s set up different tile layers (like a satellite view, street view, etc.) and markers as layers that can be toggled using LayersControl.

Import Required Components

You will need LayersControl, LayerGroup, and optionally Marker or any other components for different layers.

js
1 import { LayersControl, LayerGroup } from 'react-leaflet';
tsx
1 // MapComponent.tsx
2
3 import React, { useState } from 'react';
4 import L from 'leaflet';
5 import {
6 MapContainer,
7 TileLayer,
8 Marker,
9 Popup,
10 Rectangle,
11 Circle,
12 Polygon,
13 LayersControl,
14 LayerGroup,
15 } from 'react-leaflet';
16 import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
17 import markerIcon from 'leaflet/dist/images/marker-icon.png';
18 import markerShadow from 'leaflet/dist/images/marker-shadow.png';
19 import MapWithEvents from './MapWithEvents';
20
21 import 'leaflet/dist/leaflet.css';
22 import Parks from './Parks';
23
24 // Default marker icon
25 delete L.Icon.Default.prototype._getIconUrl;
26 L.Icon.Default.mergeOptions({
27 iconRetinaUrl: markerIcon2x,
28 iconUrl: markerIcon,
29 shadowUrl: markerShadow,
30 });
31
32 // Define positions for Polygon, Circle, and Rectangle
33 const polygonPositions = [
34 [1.3521, 103.8198],
35 [1.3521, 103.8197],
36 [1.3521, 103.8196],
37 ];
38
39 const circleCenter = [1.3521, 103.8198];
40 const rectangleBounds = [
41 [1.3521, 103.8197],
42 [1.3521, 103.8196],
43 ];
44
45 const MapComponent: React.FC = () => {
46 const [mapClickPosition, setMapClickPosition] = useState<string | null>(null);
47 const [zoomLevel, setZoomLevel] = useState<number>(13);
48
49 // Handler for map click
50 const handleMapClick = (event) => {
51 const { lat, lng } = event.latlng;
52 console.log('handleMapClick');
53 setMapClickPosition(`Latitude: ${lat}, Longitude: ${lng}`);
54 };
55
56 // Handler for zoom change
57 const handleZoomEnd = (event) => {
58 setZoomLevel(event.target.getZoom());
59 };
60
61 const { BaseLayer, Overlay } = LayersControl;
62
63 return (
64 <MapContainer
65 center={[1.3521, 103.8198]}
66 style={{ height: '100vh', width: '100%' }}
67 zoom={zoomLevel}
68 onClick={(e) => handleMapClick(e)} // Map click event
69 onZoomEnd={handleZoomEnd} // Zoom end event
70 >
71 <LayersControl position='topright'>
72 {/* Base Layers */}
73 <BaseLayer checked name='OpenStreetMap'>
74 <LayerGroup>
75 <TileLayer
76 url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
77 attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
78 />
79 <Marker position={[1.3521, 103.8198]}>
80 <Popup>
81 A pretty CSS3 popup. <br /> Easily customizable.
82 </Popup>
83 </Marker>
84
85 {/* Add Polygon */}
86 <Polygon positions={polygonPositions} color='purple' />
87
88 {/* Add Circle */}
89 <Circle center={circleCenter} radius={500} color='blue' />
90
91 {/* Add Rectangle */}
92 <Rectangle bounds={rectangleBounds} color='green' />
93 <MapWithEvents />
94 {/* <Parks /> */}
95 </LayerGroup>
96 </BaseLayer>
97 <BaseLayer name='Satellite'>
98 <TileLayer
99 url='https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'
100 attribution='&copy; <a href="https://opentopomap.org/">OpenTopoMap</a> contributors'
101 />
102 </BaseLayer>
103 <Overlay checked name='Marker'>
104 <Marker position={[1.3521, 103.8198]} />
105 </Overlay>
106 </LayersControl>
107 </MapContainer>
108 );
109 };
110
111 export default MapComponent;

Explanation of LayersControl:

LayersControl: This component wraps all layers you want to control. You can switch between base layers and toggle overlays on or off.

  • BaseLayer: Represents layers that switch between each other. Only one base layer can be active at a time.
  • Overlay: Represents layers that can be turned on or off independently from the base layers.

Base Layers:

  • We define two base layers: one using OpenStreetMap tiles, and another using a satellite view. Only one of these can be visible at a time.
  • The checked attribute on a BaseLayer makes it the default layer when the map loads.

Overlays:

  • The Overlay layer is for additional features that can be toggled on and off independently of the base layers.
  • In this case, we add a marker as an overlay.

LayerGroup:

  • Groups multiple layers together.
  • Inside the LayerGroup, we have a several markers, each at different coordinates. These markers will be treated as one group.
  • The Overlay component here wraps the LayerGroup, allowing you to toggle the visibility of the grouped markers via the control or a button.
LayersControl in React Leaflet

You can find the code from the article on the github.

Conclusion

In this article, we covered how to create a simple map using React, TypeScript, and LeafletJS. You learned how to set up your project, create a map component, add markers and popups, and customize your map’s appearance. With these skills, you can further expand your application by integrating additional features like user location tracking, clustering, or displaying geographic data.

By mastering React Leaflet, you’re well on your way to building engaging and interactive map-based applications that enhance user experiences. Happy coding!

JavaScript Development Space

© 2024 JavaScript Development Space - Master JS and NodeJS. All rights reserved.