Effects
The three shapes of the dependency array
useEffect takes a function, and an optional second argument that controls when that function runs.
// 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 timecitychanges 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
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
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
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.
finallyruns whether thetrysucceeds or thecatchfires, sosetLoading(false)always happens, exactly once, no matter which path the request took.- Rendering follows the same three states directly:
{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
const geoResponse = await fetch(...)
if (!geoResponse.ok) {
throw new Error("Failed to find city")
}fetchonly rejects, triggering thecatch, on a real network failure. A 404 or 500 response still counts as a "successful" fetch as far asfetchitself is concerned,response.okis what actually tells you whether the server said yes.throw new Error(...)inside atryblock is what routes control into thecatch.error.messageis exactly the string passed in here.
Chaining two dependent requests
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
async function searchWeather(searchCity) {
const cityToSearch = searchCity ?? city
...
}??only falls back when the left side isnullorundefined, not for any other falsy value like"". CallingsearchWeather("Kolkata")from the effect uses"Kolkata"directly. CallingsearchWeather()from the form, no argument, sosearchCityisundefined, falls back to whatever's currently in thecitystate.- 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.