GD1
GD1

Reputation: 97

Can't access state inside useEffect

It seems I can't access the state from a useEffect() hook in my project. More specifically, I'm successfully fetching some data from an API; then I save those data in the component's state; part of it gets then "published" in a Context, the remaining is passed to child components as props. The problem is that the setState() functions are not updating the state correctly; and I've noticed while debugging that if I put a watch for the state variables, they show up as null even though the promises do fulfill and JS succesfully assigns the correct data to the service variables resMainCityCall, resUrlAltCity1Call and resUrlAltCity2Call. The hook useState() assigns null as a default value to the state variables mainCityData, altCity1Data, altCity2Data, but the state setter functions fail to assign the values fetched to the state, which stay set to null.

Caller.js

import React from 'react';
import { useState, useEffect } from 'react';
import { MainCity } from './MainCity';
import { AltCity } from './AltCity';


export const MainCityContext = React.createContext(
    null // context initial value. Let's fetch weather data, making our Caller component the provider. The main city box and the other two boxes will be consumers of this context, aka the data fetched.
);


export const Caller = () =>
{
    const [mainCityData, setMainCityData] = useState(null);
    const [altCity1Data, setAltCity1Data] = useState(null);
    const [altCity2Data, setAltCity2Data] = useState(null);

    useEffect(() =>
        {
            const fetchData = async () =>
            {
                const urlMainCityBox = "https://api.openweathermap.org/data/2.5/weather?lat=45.0677551&lon=7.6824892&units=metric&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
                // const urlTodayBox = "https://pro.openweathermap.org/data/2.5/forecast/hourly?lat=45.0677551&lon=7.6824892&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
                // const urlWeekMonthBox = "https://api.openweathermap.org/data/2.5/forecast/daily?lat=45.0677551&lon=7.6824892&cnt=7&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
                const urlAltCity1 = "https://api.openweathermap.org/data/2.5/weather?lat=51.5073219&lon=-0.1276474&units=metric&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
                const urlAltCity2 = "https://api.openweathermap.org/data/2.5/weather?lat=41.8933203&lon=12.4829321&units=metric&appid=65e03b16f8eb6ba0ef7776cd809a50cd";


                let globalResponse = await Promise.all([
                    fetch(urlMainCityBox),
                    //fetch(urlTodayBox),
                    //fetch(urlWeekMonthBox),
                    fetch(urlAltCity1),
                    fetch(urlAltCity2)
                ]);

                const resMainCityCall = await globalResponse[0].json();
                // const resUrlTodayBoxCall = await globalResponse[1].json();
                // const resUrlWeekMonthBoxCall = await globalResponse[2].json();
                const resUrlAltCity1Call = await globalResponse[1].json();
                const resUrlAltCity2Call = await globalResponse[2].json();

                setMainCityData({
                    "name" : resMainCityCall.name,
                    "weather" : resMainCityCall.weather[0].main,
                    "weather_description" : resMainCityCall.weather[0].description,
                    "icon" : resMainCityCall.weather[0].icon,
                    "temperature" : resMainCityCall.weather[0].main.temp,
                    "time" : convertTimeOffsetToDate( resMainCityCall.timezone )
                    // spot for the forecasted data (paid API on OpenWeather).

                });

                setAltCity1Data({
                    "name" : resUrlAltCity1Call.name,
                    "weather" : resUrlAltCity1Call.weather[0].main,
                    "weather_description" : resUrlAltCity1Call.weather[0].description,
                    "icon" : resUrlAltCity1Call.weather[0].icon,
                    "temperature" : resUrlAltCity1Call.weather[0].main.temp,
                    "time" : convertTimeOffsetToDate( resUrlAltCity1Call.timezone )  // time attribute is type Date
                });

                setAltCity2Data({
                    "name" : resUrlAltCity2Call.name,
                    "weather" : resUrlAltCity2Call.weather[0].main,
                    "weather_description" : resUrlAltCity2Call.weather[0].description,
                    "icon" : resUrlAltCity2Call.weather[0].icon,
                    "temperature" : resUrlAltCity2Call.weather[0].main.temp,
                    "time" : convertTimeOffsetToDate( resUrlAltCity2Call.timezone )  // time attribute is type Date
                });

                console.log("Status updated.");
                console.log(mainCityData);
                console.log(altCity1Data);
                console.log(altCity2Data);

            }

            fetchData().catch((error) => { console.log("There was an error: " + error)});

        }, []); // useEffect triggers only after mounting phase for now.


        let mainCityComponent, altCity1Component, altCity2Component = null;
        // spot left for declaring the spinner and setting it on by default.
        if ((mainCityData && altCity1Data && altCity2Data) != null)
        {
            mainCityComponent = <MainCity />;  // Not passing props to it, because MainCity has nested components that need to access data. It's made available for them in an appropriate Context.
            altCity1Component = <AltCity data={ altCity1Data } />
            altCity2Component = <AltCity data={ altCity2Data } />
        }
        // spot left for setting the spinner off in case the state is not null (data is fetched).

    
    return (
        <div id="total_render">
            <MainCityContext.Provider value={ mainCityData } >
                { mainCityComponent }
            </MainCityContext.Provider>
            { altCity1Component }
            { altCity2Component }
        </div>
        
    );

}

const convertTimeOffsetToDate = (secondsFromUTC) =>
{
    let convertedDate = new Date();  // Instanciating a Date object with the current UTC time.
    convertedDate.setSeconds(convertedDate.getSeconds() + secondsFromUTC);

    return convertedDate;
}

Since the state stays null, the check

if ((mainCityData && altCity1Data && altCity2Data) != null)

doesn't pass and nothing gets rendered.

Upvotes: 0

Views: 924

Answers (1)

Il&#234; Caian
Il&#234; Caian

Reputation: 665

There are some issues with your code and probably your logic. I'll list them here and ask questions so you can think about them and answer the best way you think it makes sense.

Too much logic inside useEffect()

As advice, you could save the result from the request and keep that in a state. When that state got updated, you update the respective states. Something like this could work:

const [requestResponseValue, setRequestReponseValue] = useState([]);
const [first, setFirst] = useState();
const [second, setSecond] = useState();
const [last, setLast] = useState();

useEffect(() => {
  const initialFetch = async () => {
    const allResult = await Promise.all([
      fetchFirst(),
      fetchSecond(),
      fetchLast(),
    ]);
    setRequestReponseValue(allResult);
  }

  initialFetch();
}, []);

useEffect(() => {
  if (requestResponseValue[0] !== undefined) {
    setFirst(requestResponseValue[0]);
  }
}, [requestResponseValue, setFirst]);

useEffect(() => {
  if (requestResponseValue[1] !== undefined) {
    setSecond(requestResponseValue[1]);
  }
}, [requestResponseValue, setSecond]);

useEffect(() => {
  if (requestResponseValue[2] !== undefined) {
    setLast(requestResponseValue[2]);
  }
}, [requestResponseValue, setLast]);

Log states right after updating them

When you call the setX() function, the respective state will not change right after you called that function. This code will never log the correct value:

const [age, setAge] = useState(20);
useEffect(() => {
  setAge(30);
  console.log(age);
}, []);

It'll log 20 instead of 30.

Confusion on setting child components

This code:

let mainCityComponent, altCity1Component, altCity2Component = null;

sets the value from:

  • mainCityComponent to undefined
  • altCity1Component to undefined
  • altCity2Component to null

Is that what you want?

Confusion about checking logical operations

Also, this code:

if ((mainCityData && altCity1Data && altCity2Data) != null)

checks for (mainCityData && altCity1Data && altCity2Data) being different from null with just the != operator. If you want to check each one of those, you should write something like this:

if (
  mainCityData !== null
  && altCity1Data !== null
  && altCity2Data !== null
)

But the meaning of that is totally different from what you wrote. Also, I can't say for sure but I think those "component" variables will never be rendered as the component API is not expecting them to change and it is not "waiting" for them. Long story short: They are not mutable. (But I'm not 100% sure about that as I didn't test this, specifically)

Even with this, you could write this with a different approach.

Use !! and conditional render

To avoid this specific case, you could write your render function like this:

return (
  <div id="total_render">
    <MainCityContext.Provider value={ mainCityData } >
      {!!mainCityData ? <MainCity /> : null}
    </MainCityContext.Provider>
    {!!altCity1Data ? <AltCity alt={altCity1Data} /> : null}
    {!!altCity2Data ? <AltCity alt={altCity2Data} /> : null}
  </div>
);

With that, when the mainCityData, altCity1Data, and altCity2Data states got changed, they'll re-render for you.

Upvotes: 1

Related Questions