The todo app kept everything local. This project connects to the outside world: a small weather dashboard where a user types a city name and gets back live weather data from a real API, complete with loading and error states that a production app actually needs.
1. The plan
The component needs: a search input, a search button, and three possible states for the result area — loading, error, or the actual weather data. This "three-state" pattern (loading / error / data) comes up in practically every real screen that talks to a network.
2. State setup
import { useState } from "react";
function WeatherApp() {
const [city, setCity] = useState("");
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// ...
}3. Fetching on demand, not on mount
Unlike the user list example earlier, this fetch should not run automatically when the component mounts — it should run when the user searches. That means the fetch lives inside an event handler, not inside useEffect:
async function searchWeather() {
if (!city.trim()) return;
setLoading(true);
setError(null);
setWeather(null);
try {
const res = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=0&longitude=0¤t_weather=true`
);
if (!res.ok) throw new Error("Weather service unavailable");
const data = await res.json();
setWeather(data.current_weather);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}In a real build you would first geocode the typed city name to latitude and longitude with a geocoding API, then plug those coordinates into the forecast request. The important part for this tutorial is the pattern: an async function, wrapped in try/catch/finally, driving three pieces of state in a predictable order — clear old results, mark loading, then land in either the success or the error branch.
4. Rendering the three states
return (
<div className="weather-app">
<h1>Weather Check</h1>
<input
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Enter a city"
/>
<button onClick={searchWeather} disabled={loading}>
{loading ? "Searching..." : "Search"}
</button>
{error && <p className="error">{error}</p>}
{weather && (
<div className="result">
<p>Temperature: {weather.temperature}°C</p>
<p>Wind speed: {weather.windspeed} km/h</p>
</div>
)}
</div>
);Two things worth pointing out. First, disabled={loading} on the button stops a user from firing off five requests by mashing search while one is already in flight — a small touch, but it is exactly the kind of detail that separates a demo from something that feels finished. Second, the {error && ...} and {weather && ...} pattern is the standard React way to conditionally render something only when a value is truthy, without needing a full if/else.
5. Handling Enter as well as the button
<input
value={city}
onChange={(e) => setCity(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && searchWeather()}
placeholder="Enter a city"
/>Small usability details like this are cheap to add once you are comfortable with events, and they are exactly what users expect from a real form.
What this project adds on top of the todo app
- Fetching in response to a user action, not just on mount.
async/awaitwith propertry/catch/finallyaround a network call.- Three-state UI: loading, error, success — the shape of almost every real data-fetching screen.
- Small UX details: disabling a button while busy, supporting the Enter key.
Between the todo app and this dashboard you have now built the two most common shapes of a real React screen: one driven entirely by local state, and one driven by a live external API. From here, the natural next steps are React Router for multi-page apps and a state management library once a single app's state gets too big to lift by hand.