Search code examples
javascriptreactjsfetchopenweathermap

cannot access deep nested objects in fetch response in react


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 the console.log

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.


Solution

  • 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:

    1. A conditional render that renders a "loading data..." etc if the fetch has not been completed.

    2. 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.

    3. 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);