all topics

Effects

The three shapes of the dependency array

useEffect takes a function, and an optional second argument that controls when that function runs.

jsx
// runs after every render
useEffect(() => {
    ...
})
 
// runs once, after the initial render
useEffect(() => {
    ...
}, [])
 
// runs after the initial render, and again whenever city changes
useEffect(() => {
    ...
}, [city])
  • No second argument at all → runs after every single render. The most aggressive option, and rarely what you actually want.
  • An empty array [] → runs exactly once, right after the component first mounts. Nothing in the array means nothing to watch, so it never runs again after that.
  • An array with values in it, [city] → runs after the first render, then again every time city changes between renders. React compares each dependency to its previous value and only re-runs the effect if something in the list is different.

The one-line version worth remembering: [] means "when I mount", [city] means "when city changes", no array at all means "after every render".

Fetching data when the app loads

jsx
useEffect(() => {
    setCity("Kolkata")
    searchWeather("Kolkata")
}, [])

The concrete version of "when I mount": an empty dependency array runs this exactly once, right after the app first mounts, to load a default city's weather before the user does anything.

Effects can't be async directly

jsx
async function searchWeather(searchCity) {
    ...
}
 
useEffect(() => {
    searchWeather("Kolkata")
}, [])

The function passed to useEffect can't itself be async, React expects it to return either nothing or a cleanup function, not a promise. So instead of making the effect async, the effect just calls a separately defined async function.

The loading / error / data pattern

jsx
setLoading(true)
setError("")
setWeather(null)
 
try {
    // ...fetch calls
    setWeather({ ... })
} catch (error) {
    setError(error.message)
} finally {
    setLoading(false)
}
  • Before starting a fetch, reset everything to a clean slate: loading on, error cleared, old data cleared. That stops a stale error or a stale weather card lingering on screen while a new search is in flight.
  • finally runs whether the try succeeds or the catch fires, so setLoading(false) always happens, exactly once, no matter which path the request took.
  • Rendering follows the same three states directly:
jsx
{loading && <p>Loading...</p>}
{error && <p>{error}</p>}
{weather && <WeatherCard weather={weather} />}

At most one of the three is ever true at a time.

Checking response.ok, and throwing your own errors

jsx
const geoResponse = await fetch(...)
 
if (!geoResponse.ok) {
    throw new Error("Failed to find city")
}
  • fetch only rejects, triggering the catch, on a real network failure. A 404 or 500 response still counts as a "successful" fetch as far as fetch itself is concerned, response.ok is what actually tells you whether the server said yes.
  • throw new Error(...) inside a try block is what routes control into the catch. error.message is exactly the string passed in here.

Chaining two dependent requests

jsx
const { latitude, longitude, name } = geoData.results[0]
 
const weatherResponse = await fetch(
    `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}...`
)

The second request needs data from the first, coordinates from the geocoding lookup, so it can't fire until the first await resolves. await lets this read top to bottom like ordinary synchronous code, even though both calls are asynchronous.

searchCity ?? city

jsx
async function searchWeather(searchCity) {
    const cityToSearch = searchCity ?? city
    ...
}
  • ?? only falls back when the left side is null or undefined, not for any other falsy value like "". Calling searchWeather("Kolkata") from the effect uses "Kolkata" directly. Calling searchWeather() from the form, no argument, so searchCity is undefined, falls back to whatever's currently in the city state.
  • That's what lets the same function serve two different callers: one that already knows which city it wants, and one that should just use whatever the user typed.