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&current_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/await with proper try/catch/finally around 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.