Can't access state inside useEffect - javascript

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.

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.

Related

How do I set a state to a value in localStorage?

There is a button that toggles dark and light mode, and the state of what mode the page is on is saved in localStorage. However, I cannot change the initial value of the state (dark) and I don't know why. This is done in a useEffect function but no matter what the value of dark is, it is always set to its initial value of false.
How do I set the value of the localStorage to the dark state?
function Mode() {
const [dark, setDark] = useState(false);
// localStorage.removeItem("dark");
const onClick = () => {
if (dark) {
setDark(false);
document.querySelector("body").classList.remove("dark");
} else {
setDark(true);
document.querySelector("body").classList.add("dark");
}
localStorage.setItem("dark", dark);
};
const localDark = JSON.parse(localStorage.getItem("dark"));
useEffect(() => {
if (localDark !== null) {
setDark(!JSON.parse(localStorage.getItem("dark"))); // this is what does not change the value of dark
onClick();
}
}, []);
return (
<div onClick={onClick} className="mode">
{dark ? <Light /> : <Dark />}
</div>
);
}
Directly use the value from localStorage in useState as the default. useEffect is unnecessary here.
const [dark, setDark] = useState(JSON.parse(localStorage.getItem("dark")));
document.body.classList.toggle('dark', dark);
The click event handler should set the localStorage dark value to the logical complement of the current value.
const onClick = () => {
localStorage.setItem("dark", !dark);
setDark(!dark);
};
Use a function to initialize the state from local storage. Update the storage and the body's class on init, and when dark state changes:
const getLocalDark = () => !!JSON.parse(localStorage.getItem("dark"));
function Mode() {
const [dark, setDark] = useState(getLocalDark);
const onClick = () => {
setDark(d => !d);
};
useEffect(() => {
const classList = document.querySelector("body").classList;
if (dark) classList.add("dark");
else classList.remove("dark");
localStorage.setItem("dark", dark);
}, [dark]);
return (
<div onClick={onClick} className="mode">
{dark ? <Light /> : <Dark />}
</div>
);
}
Perhaps you'd be interested in a useLocalStorage hook. Here's how that can be implemented:
export const useLocalStorage = (key, initialState) => {
const [value, setValue] = useState(() => {
// Initialize with the value in localStorage if it
// already exists there.
const data = localStorage.getItem(key);
// Otherwise, initialize using the provided initial state.
return data ? JSON.parse(data) : initialState;
});
// Each time "value" is changed using "setValue", update the
// value in localStorage to reflect these changes.
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value]);
return [value, setValue];
};
This hook syncs the value seen in localStorage with the value stored in memory under the value variable.
The usage looks like this (almost identical to regular useState):
export const Counter = () => {
const [count, setCount] = useLocalStorage('count', 0);
return (
<div>
<p>{count}</p>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}>
Increase count
</button>
</div>
);
};
However, the main caveat of this hook is that it's only really meant to be used in one component. That means, if you change the value from light to dark in one component using this hook, any other components using it won't be updated. So instead, you should look into using a similar implementation of what is demonstrated above, but using the React Context API. That way, you'll ensure your in-memory values are in sync with those stored in localStorage. Theming is one of the main uses of the Context API.
Good luck! :)

How to prevent state updates from a function, running an async call

so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.

Delay update of calculated value until some required state has been set

I am looking for some help in delaying the display of some calculated data until the state that it relies on is set in a parent component. In the below-simplified example, App has a value set to 0, then some calculations are run and the state is updated.
The problem is that when Calculate is clicked, the calculation which updates the state of the initial val hasn't yet happened, and the final value derived from calcOverUnderPayment is therefore incorrect.
It only displays the correct value on all subsequent clicks.
What could I do to fix this?
Thanks so much in advance.
function App() {
const [val, setVal] = useState(0)
const calculate = () => {
// code to run some calculations which I then set as state
setVal(100)
}
return (
<>
<button onClick={() => calculate()}>Calculate</button>
<Outcome
val={val}
/>
</>
)
}
function Outcome(val) {
const calcOverUnderPayment = (value: number, type: string) => {
if (type === 'under') {
return (value > 0) ? value : 0
} else {
return (value < 0) ? value : 0
}
}
return (
<>
Final Val is {calcOverUnderPayment(val)}
</>
)
}
I have gone through comments of yours on the other answer. You can use another state variable and useEffect hook in your Outcome component.
function App() {
// If it is possible, try to change the initial value to null so that the Outcome component can figure out when did the value change.
const [val, setVal] = useState(null)
const calculate = () => {
// code to run some calculations which I then set as state
setVal(100)
}
return (
<>
<button onClick={() => calculate()}>Calculate</button>
<Outcome
val={val}
/>
</>
)
}
function Outcome({ val }) {
// Use this state to display initial value and update this state instead of calling the function directly in the return
const [valueToDisplay, setValueToDisplay] = useState('YOUR_INITIAL_VALUE_TO_DISPLAY');
const calcOverUnderPayment = (value: number, type: string) => {
if (type === 'under') {
return (value > 0) ? value : 0
} else {
return (value < 0) ? value : 0
}
}
// use useEffect hook to run your function whenever val prop is changed
useEffect(() => {
// If the val prop is null, then it is evident that it is rendering for the first time.
// So, don't run the calcOverUnderPayment() method. This is why "null" is important to provide as your initial state for "val" in App component
if(val !== null) {
const value = calcOverUnderPayment();
// set the state variable here to display
setValueToDisplay(value);
}
},[val]) // Give the val prop as the dependency. This tells useEffect to run whenever val prop is changed.
return (
<>
{/* Instead of calling your function here, use the state variable*/}
Final Val is {valueToDisplay}
</>
)
}
This should work as you intended. Let me know if you encounter any problem or didn't understand it.
As one of the options you could just use conditional rendering, and render your child component, only when state isn't default, in your case something like this would do:
return( //your components here
{val != 0 && <Outcome val={val} />}
//rest of your component
)

Difference between usePreviousDistinct with useEffect and without

I needed a hook to get the previous distinct value of a specific state. It looks like this and it seems to work:
function usePreviousDistinct(state) {
const prevRef = useRef();
useEffect(() => {
prevRef.current = state;
}, [state]);
return prevRef.current;
}
I've also seen there is a usePreviousDistinct hook in the react-use package but the approach is different than mine.
import { useRef } from 'react';
import { useFirstMountState } from './useFirstMountState';
export type Predicate<T> = (prev: T | undefined, next: T) => boolean;
const strictEquals = <T>(prev: T | undefined, next: T) => prev === next;
export default function usePreviousDistinct<T>(value: T, compare: Predicate<T> = strictEquals): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>(value);
const isFirstMount = useFirstMountState();
if (!isFirstMount && !compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
I wonder if I have not understood something or am missing something. Is my version also correct?
In my test I could not find a difference:
https://codesandbox.io/s/distracted-mayer-zpym8?file=/src/App.js
useEffect() together with useRef() (your version) does not show the latest value.
useRef() without useEffect() gives you the correct value, because it runs synchronously, but doesn't have the advantages that come with asynchronicity.
useEffect() together with useState() gives you the correct value, but might trigger unnecessary renders (adds potentially unnecessary overhead).
Your version looks like it works as expected, because the old value that is shown is the one that you expect to see as the new value. But the actual new value is not the one you want.
Example:
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent = function(props){
const [state, setState ] = useState(0);
return <React.Fragment>
state: { state },<br />
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
<button onClick={ function(){
setState( state + 1 );
} }>
increment
</button>
</React.Fragment>;
};
const usePreviousDistinctUE = function( value ){
const prevRef = useRef();
useEffect(() => {
prevRef.current = value;
console.log('with useEffect, prev:', prevRef.current, ', current:', value);
}, [value]);
return prevRef.current;
};
const usePreviousDistinctR = function( value ){
const prevRef = useRef();
const curRef = useRef( value );
if( curRef.current !== value ){
prevRef.current = curRef.current;
curRef.current = value;
}
console.log('w/o useEffect, prev:', prevRef.current, ', current:', curRef.current);
return prevRef.current;
};
The values shown on the page are the same, but in the console they are different. That means the value in the useEffect() version is changed, it is only not yet shown on the page.
If you just add another hook that updates anything unrelated (leaving everything else unchanged), then the page (might*) magically show the updated value again, because the page is re-rendered and the previously already changed value is shown. The value is now wrong in your eyes, but it is not changed, only shown:
// ...
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
anything updated: { useAnythingUpdating( state ) },<br />
// ...
const useAnythingUpdating = function(state){
const [result, setResult ] = useState(0);
useEffect(() => {
setResult( state );
console.log('anything updated');
});
return result;
};
*But you shouldn't rely on something else triggering a re-render. I'm not even sure this would update as expected under all circumstances.
more Details:
useEffect() is triggered at some time when react decides that the prop must have been changed. The ref is changed then, but react will not 'get informed' about a ref change, so it doesn't find it necessary to re-render the page to show the changed value.
In the example without useEffect() the change happens synchronously.
React doesn't 'know' about this change either, but (if everything else runs as expected) there will be always a re-render when necessary anyway (you will have called that function from another function that is rendered at the end).
(Not informing react about the change is basically the point in using useRef(): sometimes you want just a value, under your own control, without react doing magic things with it.)

React multiple callbacks not updating the local state

I have a child component called First which is implemented below:
function First(props) {
const handleButtonClick = () => {
props.positiveCallback({key: 'positive', value: 'pos'})
props.negativeCallback({key: 'negative', value: '-100'})
}
return (
<div><button onClick={() => handleButtonClick()}>FIRST</button></div>
)
}
And I have App.js component.
function App() {
const [counter, setCounter] = useState({positive: '+', negative: '-'})
const handleCounterCallback = (obj) => {
console.log(obj)
let newCounter = {...counter}
newCounter[obj.key] = obj.value
setCounter(newCounter)
}
const handleDisplayClick = () => {
console.log(counter)
}
return (
<div className="App">
<First positiveCallback = {handleCounterCallback} negativeCallback = {handleCounterCallback} />
<Second negativeCallback = {handleCounterCallback} />
<button onClick={() => handleDisplayClick()}>Display</button>
</div>
);
}
When handleButtonClick is clicked in First component it triggers multiple callbacks but only the last callback updates the state.
In the example:
props.positiveCallback({key: 'positive', value: 'pos'}) // not updated
props.negativeCallback({key: 'negative', value: '-100'}) // updated
Any ideas?
Both are updating the state, your problem is the last one is overwriting the first when you spread the previous state (which isn't updated by the time your accessing it, so you are spreading the initial state). An easy workaround is to split counter into smaller pieces and update them individually
const [positive, setPositive] = useState('+')
const [negative, setNegative] = useState('-')
//This prevents your current code of breaking when accessing counter[key]
const counter = { positive, negative }
const handleCounterCallback = ({ key, value }) => {
key === 'positive' ? setPositive(value) : setNegative(value)
}
You can do that but useState setter is async like this.setState. If you want to base on the previous value you should use setter as function and you can store it in one state - change handleCounterCallback to
const handleCounterCallback = ({key,value}) => {
setCounter(prev=>({...prev, [key]: value}))
}
and that is all. Always if you want to base on the previous state use setter for the state as function.
I recommend you to use another hook rather than useState which is useReducer - I think it will be better for you

Categories

Resources