Creating a Dynamic & Responsive Weather Map with OpenWeatherMap API and React-Leaflet with Custom Marker Icon: A Step-by-Step Guide
Table of contents
Here I will share my learning on how I implement a weather Map in my Cloudsify project.
website link:
https://cloudsify-59854.web.app/
GitHub Repository Link:
https://github.com/AakashRaj20/Cloudify
Let's start with creating a react-app in vs-code
npx create-react-app weatherMap
This command will create a react project with the name weatherMap
Now let's install the dependencies for the project
npm install react-leaflet leaflet @mui/material @mui/icons-material @reduxjs/toolkit axios
With our dependencies installed let's start with the code
Create a components folder and inside the src folder and then inside the Components folder create a file called Map.jsx
//import all the neccessary packages and functions
import React, { useEffect, useState } from "react";
import { MapContainer, TileLayer, LayersControl } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { fetch50CityData } from "../slice/citiesDataSlice";
import { cityData } from "../slice/inputSlice";
import { useSelector, useDispatch } from "react-redux";
const Map = () => {
const dispatch = useDispatch();
const selectedCity = useSelector(cityData);
useEffect(() => {
selectedCity &&
dispatch(
fetch50CityData({
lon: selectedCity?.location.lon,
lat: selectedCity?.location.lat,
})
);
}, [selectedCity, dispatch]);
const [zoomLevel, setZoomLevel] = useState(12);
const handleZoomChange = (e) => {
setZoomLevel(e.target._zoom);
};
const [center, setCenter] = useState([19.076, 72.8777]);
useEffect(() => {
selectedCity &&
setCenter([selectedCity.location.lat, selectedCity.location.lon]);
}, [selectedCity]);
}
export default Map;
The selectedCity
variable captures the city name input provided by the user, which is subsequently stored in the Redux store.
Incorporating the useEffect
hook, we validate the existence of selectedCity
. When present, we dispatch the fetch50CitiesData
Async Thunk function. This function interfaces with OpenWeatherMap API, gathering current weather data for about 50 nearby cities, utilizing selectedCity
's latitude and longitude.
The useEffect
hook is optimized with a dependency array [selectedCity, dispatch]
. This array dictates the hook's execution upon changes to these values. Thus, the hook responds solely to alterations in selectedCity
or dispatch
.
Subsequently, we define a useState
variable zoomLevel
that establishes the initial map zoom level state as 12.
Next, we construct a handleZoom
function. This function utilizes the setZoomLevel
mechanism to update the zoomLevel
state with the current zoom level.
Following this, we create a center
state variable through the useState
hook. This variable establishes the initial center point for our map.
Subsequently, we employ another instance of the useEffect
hook. In this scenario, we perform a conditional check to determine the presence of the selectedCity
variable. When selectedCity
is truthy, we employ the setCenter
function to adjust the map's center coordinates, aligning them with the latitude and longitude of the chosen city.
This useEffect
activation is confined to changes in the selectedCity
value, as stipulated within the dependency array. As a result, this component remains optimized, recalibrating the map's focus exclusively when the user selects a new city.
//all the above code
const maps = (
<MapContainer
center={center}
zoom={zoomLevel}
style={{ width: "100%", height: "412px", borderRadius: "20px" }}
onZoomEnd={handleZoomChange}
>
<LayersControl position="topright">
<LayersControl.BaseLayer name="Temperature">
<TileLayer
url={`https://tile.openweathermap.org/map/temp_new/{z}/{x}/{y}.png?appid=85716d70713b33bf033f8a37df623121`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Precipitation">
<TileLayer
url={`https://tile.openweathermap.org/map/precipitation_new/{z}/{x}/{y}.png?appid=85716d70713b33bf033f8a37df623121`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Wind">
<TileLayer
url={`https://tile.openweathermap.org/map/wind_new/{z}/{x}/{y}.png?appid=85716d70713b33bf033f8a37df623121`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Clear Map" checked="Clear Map">
<TileLayer
url={`https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
</LayersControl>
<Markers />
</MapContainer>
);
We configure the map container as specified by the React Leaflet library.
To begin, we initialize a <MapContainer>
element. Within this component, we incorporate various attributes to enhance map interactivity and responsiveness.
We assign the center
attribute to the <MapContainer>
to determine its center coordinates. The zoom
attribute is populated with the zoomLevel
variable, optimizing the map's magnification. Additionally, we apply styling by defining the dimensions—height, width, and border radius—of the container. Crucially, either the height
or width
property should be specified in percentage units, while the other should be expressed as a fixed value so that the map remains responsive.
To facilitate the integration of various map types, we employ the <LayerControl>
tag. By inserting this tag, we enable the inclusion of distinct map layers within a single map view.
To position the layer control div optimally, we set the position
attribute to topRight
, thereby situating the layer control panel in the upper-right corner of the map. You can place it according to your convenience.
Subsequently, we craft a <LayerControl.BaseLayer>
element, delineating the map layer that we intend to incorporate. Within this element, we define the name
attribute, specifying the desired label to be displayed on the layer control panel. You can also pass the checked
attribute with the same value as in the name attribute to make that layer be checked as the default layer.
Now, we proceed by employing a self-closing <TileLayer>
tag. This tag facilitates the integration of map data from an OpenWeatherMap API or another preferred map API. Upon user interaction, typically a click event, the tag effectively retrieves and renders the specified map on the interface.
Following this, we present the <Markers />
component, meticulously designed with HTML and CSS to serve as custom markers.
To initiate the creation of the Markup
component begins by generating a new file named markup.jsx
within the designated component
folder.
import { useEffect, useCallback } from "react";
import ReactDOMServer from "react-dom/server";
import { cityData50 } from "../slice/citiesDataSlice";
import { cityData } from "../slice/inputSlice";
import { useSelector } from "react-redux";
import { Typography, Box } from "@mui/material";
import { Marker, Popup } from "react-leaflet";
import L from "leaflet";
import { useMap } from "react-leaflet";
const Marker = () => {
const icon = (iconCode, name) => {
return L.divIcon({
className: "custom-icon",
html: ReactDOMServer.renderToString(
<div className="custom-icon-container">
<div className="custom-icon-image">
<img src={getWeatherIconUrl(iconCode)} alt="icon" />
</div>
<div className="custom-icon-name-div">
<p className="custom-icon-name">{name}</p>
</div>
</div>
),
iconAnchor: [16, 32],
iconSize: [40, 40],
iconPosition: "top",
popupAnchor: [0, -32],
});
};
}
In this segment, we construct an icon
function, designed to return an L.divIcon
object. This object defines how our custom marker should appear on the map. The function accepts two parameters: iconcode
and name
. iconcode
provides the weather icon code for each city among the 50 cities, while name
offers the respective city's name.
Subsequently, we adhere to the React-Leaflet library's documentation, constructing an object housing various attributes. The classname
attribute assigns a class to our custom marker, while the html
attribute generates a customized HTML element to display the weather icon and city name. Additionally, the iconsize
attribute specifies the icon's dimensions, with the remaining settings retained as default.
Within the <img>
tag, we invoke the getWeatherIconUrl
function, passing the iconcode
to retrieve the weather icon's URL. This process necessitates implementing OpenWeatherMap API's functionalities. This will make your custom Marker. CSS code will be attached at the end.
Here is the code for getWeatherIconUrl
function.
const getWeatherIconUrl = (iconCode) => {
return `https://openweathermap.org/img/wn/${iconCode}.png`;
};
const cities = useSelector(cityData50);
const selectedCity = useSelector(cityData);
const maps = useMap();
const changeLocation = useCallback(() => {
selectedCity &&
maps.flyTo(
[selectedCity.location.lat, selectedCity.location.lon],
maps.getZoom()
);
}, [selectedCity, maps]);
useEffect(() => {
changeLocation();
}, [selectedCity, changeLocation]);
//previous code for marker component
Here, we utilize a cities
constant to retain data for the 50 cities fetched from the Redux store. Similarly, the selectedCity
constant captures the city sought by the user through a search, also stored in the Redux store.
In the changeLocation
function, we employ useCallback
to prevent excessive re-renders. Within this hook, we initially ascertain whether the selectedCity
is valid. Utilizing the maps.flyTo
function from React Leaflet, we seamlessly transition to the selected city's location by furnishing its latitude and longitude.
Furthermore, we leverage the getZoom
function from React Leaflet to acquire the current zoom level. This level is then employed to ensure a smooth and appropriate transition, aligning with the user's current zoom preference.
// all the previous code
return (
<>
{cities &&
cities.map((city) => (
<Marker
eventHandlers={{ changeLocation }}
key={city.id}
position={[city.coord.lat, city.coord.lon]}
icon={icon(city.weather[0].icon, city.name)}
>
<Popup className="custom-popup">
<Box sx={{ width: "100%", color: "white" }}>
<Typography sx={{ lineHeight: "10px" }}>
Temperature: {city.main.temp} °C
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Description: {city.weather[0].description}
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Humidity: {city.main.humidity}%
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Wind: {city.wind.speed} km/h
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Cloudiness: {city.clouds.all}%
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Pressure: {city.main.pressure} MB
</Typography>
</Box>
</Popup>
</Marker>
))}
</>
);
};
Within this code snippet, we iterate over the cities
array, creating a <Marker>
element for each city. Additionally, we incorporate a <Popup>
component for enhanced interactivity.
Attributes are assigned to the <Marker>
tag, each serving a distinct purpose. Initially, we apply an event handler that triggers the changeLocation
function, facilitating navigation to the searched city's latitude and longitude on the map. Subsequently, we employ the position
attribute to accurately position the icons in the respective locations.
The icon
attribute seamlessly integrates the icon
function, thereby rendering the icons and city names unique to each city.
Next, we capitalize on the <Popup>
tag, employed to showcase the data for each city within its respective popup.
Code for App.js
import Map from "./components/Map";
const App = () => {
return (
<Map />
)
}
export default App;
Code For Map.jsx
import React, { useEffect, useState } from "react";
import { Grid } from "@mui/material";
import { MapContainer, TileLayer, LayersControl } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { fetch50CityData } from "../slice/citiesDataSlice";
import { cityData } from "../slice/inputSlice";
import { useSelector, useDispatch } from "react-redux";
import Markers from "./Markers";
const WeatherMap = () => {
const dispatch = useDispatch();
const selectedCity = useSelector(cityData);
useEffect(() => {
selectedCity &&
dispatch(
fetch50CityData({
lon: selectedCity?.location.lon,
lat: selectedCity?.location.lat,
})
);
}, [selectedCity, dispatch]);
const [zoomLevel, setZoomLevel] = useState(12);
const handleZoomChange = (e) => {
setZoomLevel(e.target._zoom);
};
const [center, setCenter] = useState([19.076, 72.8777]);
useEffect(() => {
selectedCity &&
setCenter([selectedCity.location.lat, selectedCity.location.lon]);
}, [selectedCity]);
const maps = (
<MapContainer
center={center}
zoom={zoomLevel}
style={{ width: "100%", height: "412px", borderRadius: "20px" }}
onZoomEnd={handleZoomChange}
>
<LayersControl position="topright">
<LayersControl.BaseLayer name="Temperature">
<TileLayer
url={`https://tile.openweathermap.org/map/temp_new/{z}/{x}/{y}.png?appid=85716d70713b33bf033f8a37df623121`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Precipitation">
<TileLayer
url={`https://tile.openweathermap.org/map/precipitation_new/{z}/{x}/{y}.png?appid=85716d70713b33bf033f8a37df623121`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Wind">
<TileLayer
url={`https://tile.openweathermap.org/map/wind_new/{z}/{x}/{y}.png?appid=85716d70713b33bf033f8a37df623121`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Clear Map" checked="Clear Map">
<TileLayer
url={`https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`}
attribution='© <a href="https://openweathermap.org/">OpenWeatherMap</a>'
/>
</LayersControl.BaseLayer>
</LayersControl>
<Markers />
</MapContainer>
);
return (
<Grid item xs={12} sm={12} md={4} container>
{maps}
</Grid>
);
};
export default WeatherMap;
Code for Marker.jsx
import { useEffect, useCallback } from "react";
import ReactDOMServer from "react-dom/server";
import { cityData50 } from "../slice/citiesDataSlice";
import { cityData } from "../slice/inputSlice";
import { useSelector } from "react-redux";
import { Typography, Box } from "@mui/material";
import { Marker, Popup } from "react-leaflet";
import L from "leaflet";
import { useMap } from "react-leaflet";
const Markers = () => {
const cities = useSelector(cityData50);
const selectedCity = useSelector(cityData);
const maps = useMap();
const getWeatherIconUrl = (iconCode) => {
return `https://openweathermap.org/img/wn/${iconCode}.png`;
};
const changeLocation = useCallback(() => {
selectedCity &&
maps.flyTo(
[selectedCity.location.lat, selectedCity.location.lon],
maps.getZoom()
);
}, [selectedCity, maps]);
useEffect(() => {
changeLocation();
}, [selectedCity, changeLocation]);
const icon = (iconCode, name) => {
return L.divIcon({
className: "custom-icon",
html: ReactDOMServer.renderToString(
<div className="custom-icon-container">
<div className="custom-icon-image">
<img src={getWeatherIconUrl(iconCode)} alt="icon" />
</div>
<div className="custom-icon-name-div">
<p className="custom-icon-name">{name}</p>
</div>
</div>
),
iconAnchor: [16, 32],
iconSize: [40, 40],
iconPosition: "top",
popupAnchor: [0, -32],
});
};
return (
<>
{cities &&
cities.map((city) => (
<Marker
eventHandlers={{ changeLocation }}
key={city.id}
position={[city.coord.lat, city.coord.lon]}
icon={icon(city.weather[0].icon, city.name)}
>
<Popup className="custom-popup">
<Box sx={{ width: "100%", color: "white" }}>
<Typography sx={{ lineHeight: "10px" }}>
Temperature: {city.main.temp} °C
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Description: {city.weather[0].description}
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Humidity: {city.main.humidity}%
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Wind: {city.wind.speed} km/h
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Cloudiness: {city.clouds.all}%
</Typography>
<Typography sx={{ lineHeight: "10px" }}>
Pressure: {city.main.pressure} MB
</Typography>
</Box>
</Popup>
</Marker>
))}
</>
);
};
export default Markers;
Code for Custom Marker Styles and Dark mode for the map
html {
scroll-behavior: smooth;
}
.leaflet-layer,
.leaflet-control-zoom-in,
.leaflet-control-zoom-out,
.leaflet-control-attribution {
filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%);
}
.speedometer {
max-width: 500px;
width: 100%;
height: 100%;
max-height: 300px;
}
.custom-icon-container {
display: flex;
width: 170px;
height: 40px;
align-items: center;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: #518554;
}
.custom-icon-name {
font-size: 0.8rem;
font-weight: 600;
textalign: justify;
padding: 0 5px;
}
.custom-icon-image {
display: flex;
justify-content: flex-start;
}
.custom-icon-name-div {
display: flex;
}
.leaflet-popup-content-wrapper {
background-color: #1B1A1D;
}
Run npm run start
to see the map in action.
I'm venturing into the world of blogging for the very first time, so any constructive suggestions you might have to enhance my content are warmly welcomed.
Stay tuned for my upcoming blog, where I'll delve into the implementation of the Recharts library.
If you happen to come across any remote frontend development opportunities, I'm actively seeking them. Don't hesitate to reach out or recommend me if my work aligns with your expectations.
A big thank you for taking the time to read my blog!
Aakash Raj Signing Off!
Subscribe to my newsletter
Read articles from Aakash Raj directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Aakash Raj
Aakash Raj
I am Frontend Web Developer with Good Experience in internships and projects. I am actively looking for Remote Frontend roles