In this post, I am going to show you how to integrate OpenStreetMap with React Typescript. If you have never worked with OpenStreetMaps ( OSM ), you can read this post on OSM and React, and then come back here to develop this demo application. I use TypeScript instead of JavaScript in this demo app.
Now, let’s jump into setting up the development environment.
Table of Contents
Setting up the development environment
- I used vite.js to set up my environment. Make sure to choose React and TypeScript when you are running Vite.js.
- You need the following libraries to develop this app. Make sure to download the latest versions when installing the libraries.
leaflet
is the main Leaflet library.react-leaflet
is a package that integrates Leaflet with React.@types/leaflet
is the TypeScript typings for Leaflet, which helps with type checking in TypeScript projects.
npm install --save leaflet react-leaflet @types/leaflet
- In your folder structure, locate the
src
folder. Create a folder named “map” inside thesrc
folder - In the map folder, create a file named
Map.ts
x Add the following base file. We will add a lot of code to this file.
export default function Map() {
return (
<div>Map</div>
)
}
5. Add the code below to App.tsx
import './App.css'
import Map from './map/Map'
function App() {
return (
<div className='App'>
<section className='map-container'>
<Map/>
</section>
</div>
)
}
export default App
At this point don’t worry if you do not see anything on the browser. We will replace the default CSS styles generated automatically by your setting-up tool.
Creating the OpenStreetMap Map view
We need to render the world map in the browser. You need to import at least 2 components ( MapContainer
and TileLayer
) from react-leaflet
for this.
Furthermore, it’s essential to import fundamental CSS from “leaflet/dist/leaflet.css” to ensure the correct rendering of the map.
import { TileLayer , MapContainer } from "react-leaflet"
import "leaflet/dist/leaflet.css";
export default function Map() {
return (
<MapContainer center={[0,0]} zoom={ 2 } scrollWheelZoom={false}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
)
}
Note: An alternative way to import this leaflet CSS file is to include it in the head section of the index.html file.
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
Changing the CSS styles in App.css and Index.css
If you use Vite.js ( or create-react-app ), you should have App.css and index.css files. You can remove all CSS code in the index.css and App.css ( and/or any CSS generated automatically ), then add the following CSS on index.css and App.css.
Index.css
body{
margin: 0;
padding: 0;
}
App.css
.leaflet-container {
width: 100vw;
height: 100vh;
}
If you did everything correctly, at this point, you should be able to see the map of the world on the browser.
Adding the Marker and the Popups
Using the Marker component of react-leaflet, you can show a specific place on the map. The Marker component takes position prop which takes two coordinates: longitude and latitude of place.
In addition, you can also add a popup for the marker. For example, the latitude and longitude coordinates for Los Angeles are [34.0522, -118.2437].
<Marker position={[34.0522, -118.2437]}>
<Popup>
Los Angeles
</Popup>
</Marker>
You can add the above code to your Map component.
import { TileLayer , MapContainer , Marker , Popup } from "react-leaflet"
import "leaflet/dist/leaflet.css";
export default function Map() {
return (
<MapContainer center={[0,0]} zoom={ 2 } scrollWheelZoom={false}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[34.0522, -118.2437]}>
<Popup>
Los Angeles
</Popup>
</Marker>
</MapContainer>
)
}
At this point, you should have a map of the world with a marker on Los Angeles, USA. When you click on the marker, it should show the name of the city.
You might see the world map showing up more than once in the browser, and the images that create the map are repeated.
I solved this problem by making the map narrower. Currently, the leaflet-container
takes up the full width of the screen (100vw). We can make it narrower, like 68.9vw, for desktop views. Feel free to try different values for the leaflet-container
width and see how it looks.
Getting the current position and displaying the name
At the moment, we have raw data for the current position and the name of the city on the popup. What we going to do now is to get the current location and set it on the map with a marker when the application launches, and then when you click on the marker, a popup callout will show the name of the place.
How are we going to achieve this?
We will keep Map.tsx
( the component that holds the Map ) separately, and then, in App.tsx
, we are going add code that utilizes JavaScript Geolocation API to get the latitude and longitude of the current location.
Once we fetch the data on the current location, we will pass that location data to the Map component.
Get the current position
- We need a state object to hold the current position’s coordinate and display name. On
App.tsx
, add the following state.
const [ currentLocation, setCurrentLocation ] = useState({
latitude: 0,
longitude:0,
display_name: "",
});
Make sure to import the useState
hook from ‘react’
- We are going to use JavaScript Geolocation API to get the latitude and longitude of the current location. Because we need to get these coordinates after our app is mounted, we should use them inside the
useEffect
hook. Make sure to import the useEffect hook from ‘react’.
useEffect(() => {
navigator.geolocation.getCurrentPosition(
getCurrentCityName,
);
}, []);
in the code above, the navigator.geolocation.getCurrentPosition(...)
is requesting the current geolocation of the user’s device using the getCurrentPosition
method of the navigator.geolocation
object. The getCurrentPosition method here takes one argument: a success callback function, which is the getCurrentCityName
.
Besides the success callback function, the navigator.geolocation.getCurrentPosition()
method can also accept an error callback function and options as parameters. We’ll make use of these additional features later in this post.
Get the name ( dispaly_name ) of the current location
getCurrentCityName
function serves as the callback function utilized by navigator.geolocation.getCurrentPosition()
. This callback function accepts the position object (which is returned by navigator.geolocation.getCurrentPosition()
) as its parameter. Subsequently, it employs this position object to generate a request to the OpenStreetMap Nominatim API, facilitating the retrieval of details such as the city name and additional location information.
Note: The process of converting geographic coordinates (latitude and longitude) into a human-readable address or location information is called reverse geocoding search.
function getCurrentCityName(position : any) {
const url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat='+ position.coords.latitude + '&lon=' +
position.coords.longitude ;
fetch(url, {
method: "GET",
mode: "cors",
}).then((response) => response.json())
.then((data) => setLocation({ latitude: position.coords.latitude,
longitude: position.coords.longitude,
display_name:`${ data.address.city }, ${ data.address.country }` })
);
}
getCurrentCityName
function utilize the setLocation
function to update the location
state. This state object includes latitude, longitude, and a display_name as its properties. We are going to make use of this state in setting the Marker and Popup components. For that, we need to pass the location
state object to the Map component. So, modify your code in App.tsx
as follows:
<Map location = { location }/>
Modify Map component
Now that our Map component accepts a prop, we need to change the code of the Map component accordingly.
Since we’re working with Typescript, I’ll create an interface to store location data. We’ll then apply this interface to define the properties( props ) that the Map component uses. To implement this, add the provided code snippet to your Map.tsx
file. Make sure these interfaces are placed outside the Map functional component as shown in the code below.
If you need to refresh your memory on what an interface is you can refer to the official docs of TypeScript. You can also check the resources section of this post to refresh your knowledge in TypeScript.
After you create interfaces and define props, you can modify the Map component to accept those props and use them in the Marker component and Popup component.
import { TileLayer , MapContainer , Marker , Popup } from "react-leaflet"
import "leaflet/dist/leaflet.css";
interface CurrentLocation {
latitude:number
longitude:number,
display_name:string
}
interface props {
curreocation: CurrentLocation
}
export default function Map( { location }: props) {
const currentCity: Location = location;
return (
<MapContainer center={[0,0]} zoom={ 2 } scrollWheelZoom={false}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker icon={ markerIcon } position={[ location.latitude, location.longitude ]}>
<Popup>
{ currentCity.display_name }
</Popup>
</Marker>
</MapContainer>
)
}
Show any location on the map
You learned how to show the current location. Let’s improve our app so that it will show almost any city ( most likely, only well-known cities as I choose only one API response ) you enter on the world map.
Let’s observe how data moves and how state management is handled.
- The user inputs the name of the city
- The name of the city is stored in a React state ( let’s name it “city” )
- Use the state(city) in the API request
- Set response data in another React state ( called “location” )
The Marker component and Popup component will use data stored in the “location” state to locate the position on the map and to display the name of the city.
I am going to keep the form UI as simple as possible. The important thing here is, of course, the request to the API. The search API that I use here is:
https://nominatim.openstreetmap.org/search?<params>
You can learn more about the <params> in the official docs. But, here I am going to use only the name of the city as <params>
. Because there can be the same name used for multiple places, I am going to limit the search response to one. However, by default, If available, this API request sends a maximum of 10 places( with the same city name in this case ) per request. But, to keep this demo app simple, I used only one location.
Check this gist to see the Full code of App.tsx
Adding a custom marker Image
At the moment, the image used for the marker comes from ../node_modules/leaflet/dist/images/marker-icon.png. However, this can cause some issues when deploying the app, and you may not see the marker image on the map. You can overcome this issue by using a custom image. I am going to use the same image as my custom image. But, I will store it in the src/assets folder. The “assets” is a folder created automatically when you set up the development environment using Vite.js.
You can download the different types of images used by the leaflet library at https://unpkg.com/browse/[email protected]/dist/images/
Once you have an image for a marker at ../src/assets, make the following changes to the Map.tsx
.
- Import the image to
Map.tsx
from the assets folder
import markerIconURL from '../assets/marker-icon.png';
- Add
import L from 'leaflet'
statement
L refers to the main namespace or object provided by the Leaflet library. When you import the L object from ‘leaflet’, you are essentially importing the core functionality and features of the Leaflet library. The L object contains various classes, methods, and properties that allow you to create and manipulate interactive maps, markers, layers, popups, and more.
- Use the L object to create a new Icon. use the above-defined markerIconURL as the
iconUrl
.
const markerIcon = new L.Icon({
iconUrl: markerIconURL,
iconSize: [25, 35],
iconAnchor: [5, 30]
});
- Utilize the recently defined custom icon (
markerIcon
) by assigning it to the icon prop within the Marker component.
<Marker icon={ markerIcon } position={[ location.latitude, location.longitude ]}>
<Popup>
{ currentCity.display_name }
</Popup>
</Marker>
Now,] you should be able to see your custom image as the marker on the map.
Error Handling and option in navigator.geolocation
At this point, you should have the following code in App.tsx
when to get the current position.
useEffect(() => {
navigator.geolocation.getCurrentPosition(
getCurrentCityName,
);
}, []);
As I mentioned before, getCurrentPosition also accepts an error callback function and option as parameters.
Therefore, you can modify the code above as below:
useEffect(() => {
navigator.geolocation.getCurrentPosition(
getCurrentCityName,
error,
options
);
},[]);
The error in the above code is a function that you can add to App.tsx
.
function error(err: any) {
if (
err.code === 1 || //if user denied accessing the location
err.code === 2 || //for any internal errors
err.code === 3 //error due to timeout
) {
alert(err.message);//modify your error messages as you wish
} else {
alert(err);
}
}
The options
is an object with the following properties:
const options = {
enableHighAccuracy: true,
maximumAge: 30000,
timeout: 27000
};
enableHighAccuracy
: Determines if the browser should prioritize high accuracy over speed when retrieving location.maximumAge
: Specifies the maximum age of a cached position that can be used to avoid unnecessary requests for a new location.timeout
: Sets the maximum time allowed for retrieving the location before triggering an error callback.