Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

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.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
8 Best Free Bootstrap Admin Templates for 2026 - Comparison & Reviews
8 Best Free Bootstrap Admin Templates for 2026 - Comparison & Reviews

How to check if an element is visible in JavaScript
How to check if an element is visible in JavaScript

How to Achieve Perfectly Rounded Corners in CSS
How to Achieve Perfectly Rounded Corners in CSS

How to loop inside React JSX
How to loop inside React JSX

Answers by CoreUI Core Team