So i stumbled across a lot of examples on Stack and the web, where accessing deep nested objects from a async fetch call return undefined. A lot of answers are resolved to other articles resulting in the way objects and arrays are accessed. Others claim that it's the reason of async operations. I just can't really get my head around it though.
this is the data:
base: "stations"
clouds: {all: 1}
cod: 200
coord: {lon: -81.38, lat: 28.54}
dt: 1607561322
id: 4167147
main: {temp: 51.28, feels_like: 45.97, temp_min: 48.99, temp_max: 53.6, pressure: 1021, …}
name: "Orlando"
sys: {type: 1, id: 5234, country: "US", sunrise: 1607515628, sunset: 1607552963}
timezone: -18000
visibility: 10000
weather: [{…}]
wind: {speed: 4.52, deg: 271}
this is the code, notice the setWeatherData in the useEffect callback
function WeatherDisplay(props){
const [weatherData, setWeatherData] = useState({})
useEffect(() => {
fetch(`http://api.openweathermap.org/data/2.5/weather?q=Orlando&units=imperial&appid=3abd9c2df6a249e8abcf4f812de0a627`)
.then(res => res.json())
.then(data => {
setWeatherData({
data,
name: data.name,
main: data.main,
...data.main
})
})
.catch(err => console.log(err))
}, [])
return(
<>
<Media style={{backgroundColor: "gray", borderRadius: "5px", height: "5.5rem"}}>
<IconContext.Provider value={{size: "5rem", style:{fontWeight: "200"}, className:"align-self-center"}}>
<TiWeatherPartlySunny />
</IconContext.Provider>
{console.log(weatherData)}
{console.log(weatherData.name)}
{console.log(weatherData.main)}
{console.log(weatherData.temp)}
</Media>
</>
)
}
this is how is started first I just setWeatherData to data, for that I could render weatherData.name (Orlando) but for some reason weatherData.main.temp came back with an error because weatherData.main results in undefined. I asked around and they say it's because promises and it's async, so I have to use a conditional blah blah, but that still doesn't explain how it was able to access weatherData.name. It's the same with weatherData.clouds and .sys
I went ahead and just spread the values of the object I wanted, but I can think of easy situations where my workaround won't be useful.
I wondered if it was the way react saved the value of a deeply nested object, but I don't really know.
Yeah this is because of asynchronous behavior. The weatherData
object will always exist (as {} initially, then with the fetched data once the request finishes).
In the initial render, weatherData.name
will equal {}.name
which will return undefined, but will not cause an error.
However when you run weatherData.main.temp
it will equal {}.main.temp
, meaning you are effectively calling undefined.temp
which will throw an error. This error will only be throwed as long as the main
prop is undefined, meaning it should work correctly once the data is actually fetched.
You can solve this in a few different ways, I recommend one of the following two:
A conditional render that renders a "loading data..." etc if the fetch has not been completed.
Conditional accessors. Try accessing weatherData.main?.temp
instead of weatherData.main.temp
. Not sure if it will work inside JSX, but it should work outside assuming you are using a recent version of babel that will transpile conditional branching correctly.
You could also create an initial weatherData
object with the expected interface defined, but all the values set to undefined
.
In other words,
const initialWeatherData = {
name: undefined,
main: {
temp: undefined,
}
}
// etc, leaving out most of the object as I'm sure you get what I'm trying to do
// just make sure you define the entire interface
const [weatherData, setWeatherData] = useState(initalWeatherData);