Reputation: 97
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
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.
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]);
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
.
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?
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.
!!
and conditional renderTo 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