How to build a weather app in React
Building a weather app is one of the best React exercises because it combines state management, async data fetching, user input, and conditional rendering in a realistic context. As the creator of CoreUI with 25 years of software development experience, I use this project to evaluate how well developers understand React’s core patterns. The key is structuring the app with a custom hook that handles the fetch logic, keeping the component clean and focused on rendering. This separation makes the code easy to extend with additional features like forecasts or location-based lookup.
Create a custom hook to fetch weather data from the OpenWeatherMap API.
// useWeather.js
import { useState, useCallback } from 'react'
const API_KEY = import.meta.env.VITE_WEATHER_API_KEY
const BASE_URL = 'https://api.openweathermap.org/data/2.5'
export function useWeather() {
const [weather, setWeather] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const fetchWeather = useCallback(async (city) => {
if (!city.trim()) return
setLoading(true)
setError(null)
try {
const res = await fetch(
`${BASE_URL}/weather?q=${encodeURIComponent(city)}&units=metric&appid=${API_KEY}`
)
if (!res.ok) {
throw new Error(res.status === 404 ? 'City not found' : 'Failed to fetch weather')
}
const data = await res.json()
setWeather(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [])
return { weather, loading, error, fetchWeather }
}
The custom hook encapsulates fetch state and the fetch function. useCallback with an empty dependency array ensures fetchWeather is stable — it won’t cause re-renders when passed as a prop. encodeURIComponent handles city names with spaces or special characters safely.
Building the Search Component
Let users type a city name and trigger the fetch.
// WeatherSearch.jsx
import { useState } from 'react'
export function WeatherSearch({ onSearch }) {
const [city, setCity] = useState('')
function handleSubmit(e) {
e.preventDefault()
onSearch(city)
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Enter city name"
aria-label="City name"
/>
<button type="submit">Search</button>
</form>
)
}
Controlled inputs keep the form value in React state. Submitting via onSubmit means the form works with both button clicks and pressing Enter, which is better accessibility than an onClick handler on the button.
Displaying Weather Data
Render the weather result with conditional handling.
// WeatherCard.jsx
export function WeatherCard({ weather }) {
if (!weather) return null
const { name, main, weather: conditions, wind } = weather
const icon = conditions[0].icon
const description = conditions[0].description
return (
<div className="weather-card">
<h2>{name}</h2>
<img
src={`https://openweathermap.org/img/wn/${icon}@2x.png`}
alt={description}
/>
<p className="temp">{Math.round(main.temp)}°C</p>
<p className="description">{description}</p>
<p>Feels like: {Math.round(main.feels_like)}°C</p>
<p>Humidity: {main.humidity}%</p>
<p>Wind: {Math.round(wind.speed)} m/s</p>
</div>
)
}
Destructuring the API response at the top of the component makes the template readable. Math.round avoids showing fractional temperatures like 18.7°C. Always provide an alt attribute on the weather icon for accessibility.
Composing the App
Wire the hook and components together in the root component.
// App.jsx
import { useWeather } from './useWeather'
import { WeatherSearch } from './WeatherSearch'
import { WeatherCard } from './WeatherCard'
export default function App() {
const { weather, loading, error, fetchWeather } = useWeather()
return (
<div className="app">
<h1>Weather App</h1>
<WeatherSearch onSearch={fetchWeather} />
{loading && <p>Loading...</p>}
{error && <p className="error">{error}</p>}
<WeatherCard weather={weather} />
</div>
)
}
The root component manages no data logic — it just connects the hook to the components. This makes each piece independently testable: the hook can be tested with a mocked fetch, the search with a mocked callback, and the card with static props.
Best Practice Note
This is the same component decomposition pattern we follow in CoreUI React templates — hooks own logic, components own rendering. For a production weather app, add debouncing to the search input to avoid firing a request on every keystroke, and cache responses by city name to avoid redundant API calls. If you want a richer UI, CoreUI’s card and badge components integrate well with this kind of data display.



