State variable not updating in useEffect callback?
evolutionxbox opened this issue Β· 27 comments
Do you want to request a feature or report a bug?
Bug, maybe? Although thinking about it more makes me think I've misunderstood something.
What is the current behavior?
scroll
state variable updates in rendered output but not inside handleScroll
event callback.
I reckon this might be due to the fact that when handleScroll
is defined scroll
is 0
, but scroll
is defined in the scope above and should be updated when the component is re-rendered.
import React, { useState, useEffect } from "react";
const Scroller = () => {
const [scroll, setScroll] = useState(window.scrollY);
const handleScroll = () => {
console.log(scroll); // scroll is always 0
setScroll(window.scrollY);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []); // runs once
return <div id="scroll">{scroll}</div>; // scroll is correct
};
export default Scroller;
What is the expected behavior?
scroll
to be updated inside handleScroll
event callback and in render output.
Which versions of React, and which browser / OS are affected by this issue?
chrome 70.0.3538.77
react 16.7.0-alpha.0 - next
react-dom 16.7.0-alpha.0 - next
It is not a bug, but the normal behavior from the doc. You can check this issue for more detail #14042
In short, if you are using the second parameter []
then you need to put all the variable (state or not) that you rely on.
So in your case, if you want to have access to the scroll
state variable then you need to put [scroll]
:)
Obviously you maybe don't want to do that, because you don't want to create / remove event listener everytimes.
So the other way is to use an other effect, that will be responsible to modifying or getting the value. useReducer
is one way. (see the other issue for code example)
I hope it is clear :)
Yep that's right. We might offer a more convenient way in the future.
The rule of thumb is that if you use a variable in useEffect
, you must declare it in the useEffect
dependency array (or omit the array entirely). So in your example handleScroll
should be in the array.
It's true that this would cause it to re-subscribe more often. There is a way to avoid it but it's not very convenient and might cause other issues in the future. We plan to add a better way later but it will take some time to implement.
I had same kind of problem when trying to add an eventListener to a component. For now, could be a better approach to stay with classes in these circumstances?
@gpietro It seems like it. Current behavior just feels broken and the "workarounds" to achieve something as simple as this are just not worth it.
I'm having the same problem so I won't open a new issue for it. Hopefully someone here can help me out with it.
Here's the code:
let labelWidth = 0
React.useEffect(() => {
labelWidth = labelRef.current!.offsetWidth;
console.log("After mount:", labelWidth);
}, [labelWidth]);
console.log("Current value:", labelWidth);
And the console output on initial render of the whole app:
Current value: 0
After mount: 120
Then when I open the component that uses the labelWidth value the entire console becomes:
Current value: 0
After mount: 120
Current value: 0
useEffect isn't updating the labelWidth property after mount. I've followed the instructions above so I'm not sure what the issue is. Any help?
I think you want to use useState if you want it to be consistent.
Something like:
const [labelWidth, setLabelWidth] = useState(0);
React.useEffect(() => {
setLabelWidth(labelRef.current!.offsetWidth);
console.log("After mount:", labelWidth);
}, [labelWidth]);
console.log("Current value:", labelWidth);
I'm having the same problem so I won't open a new issue for it. Hopefully someone here can help me out with it.
Here's the code:
let labelWidth = 0 React.useEffect(() => { labelWidth = labelRef.current!.offsetWidth; console.log("After mount:", labelWidth); }, [labelWidth]); console.log("Current value:", labelWidth);
And the console output on initial render of the whole app:
Current value: 0 After mount: 120
Then when I open the component that uses the labelWidth value the entire console becomes:
Current value: 0 After mount: 120 Current value: 0
useEffect isn't updating the labelWidth property after mount. I've followed the instructions above so I'm not sure what the issue is. Any help?
@TidyIQ : had a similar issue recently, but using two hooks that shared a global and changing the value of it in each one. The value change/assignment is not persisted across useEffect invocations (there's even a warning that says that), probably related to memoization or caching/storing of the initial variable value when the hook function is created. The warning suggested using useRef to store the value used across useEffect calls but couldn't get it to work. Ended up ditching useEffect completely, used plain functions to set input/component values on every render.
Given useEffect fires after the render, my issue was related to a slider component onChange firing to adjust for the accepted value range, setting the wrong state and ruining the workflow by (unwanted) firing another re-render.
Is there a recommended way to update state or globals, using hooks, before the render happens?
if you need a real unmount, not the crapy fake one that you find in all the examples and a one that has the correct state ... add this to your hook function :D 100% fix :D
In you hook function:
return (
<div>
<RunOnUnmount onUnmount={__your_unmount_function__} />
</div>
);
class RunOnUnmount extends React.Component {
componentWillUnmount() {
this.props.onUnmount();
}
render() {
return null;
}
}
almost same problem.
I am updating the global state using context api, but when I am calling global state in useEffect()
it still retreive the initial value, not updated value. here I used [], as second parameter for useEffect function. when I remove it, it execute all code withing useEffect function as unlimited.
Do you want to request a feature or report a bug?
Bug, maybe? Although thinking about it more makes me think I've misunderstood something.What is the current behavior?
scroll
state variable updates in rendered output but not insidehandleScroll
event callback.I reckon this might be due to the fact that when
handleScroll
is definedscroll
is0
, butscroll
is defined in the scope above and should be updated when the component is re-rendered.import React, { useState, useEffect } from "react"; const Scroller = () => { const [scroll, setScroll] = useState(window.scrollY); const handleScroll = () => { console.log(scroll); // scroll is always 0 setScroll(window.scrollY); }; useEffect(() => { window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, []); // runs once return <div id="scroll">{scroll}</div>; // scroll is correct }; export default Scroller;
What is the expected behavior?
scroll
to be updated insidehandleScroll
event callback and in render output.Which versions of React, and which browser / OS are affected by this issue?
chrome 70.0.3538.77 react 16.7.0-alpha.0 - next react-dom 16.7.0-alpha.0 - next
I had the exact same problem in my branch. I solved it similar to this, but I don't think it is nice at all. Looking forward to the next update.
const handleScroll = () => {
setScroll((previousScroll) => {
console.log(previousScroll);
return window.scrollY;
});
};
I'm having a similar issue
import { useState, useEffect } from "react";
const counter = ({ count, speed }) => {
const [inc, setInc] = useState(0);
useEffect(() => {
const counterInterval = setInterval(() => {
if(inc < count){
setInc(inc + 1);
}else{
clearInterval(counterInterval);
}
}, speed);
}, [count]);
return inc;
}
export default counter;`
In the above code, I want to call useEffect callback only when count (coming from props) updates and I'm increasing inc in setInterval callback till it becomes equal to count.
Now the issue is setInc is incrementing the value of inc but in useEffect's and setInterval's callback, I'm always getting 0, which I think should get updated as inc is in the closure these callbacks
Youβre not including all dependencies in the array. Try putting inc
in the useEffect array.
Youβre not including all dependencies in the array. Try putting
inc
in the useEffect array.
I can't do that, as I've setInterval in useEffect, if I put inc in array it will be infinite loop of setIntervals
You might need to rethink what it does then. If you donβt include inc
in the array it wonβt update.
Why not use a setTimeout
instead?
Also Iβm not sure itβs worth putting a prop as a dependency of the useEffect.
It's a counter, it takes a value (max value) in props, then initializes inc with 0 and keeps increment inc till inc become equals to count, so I can't use setTimeout ( as it will execute call back first time only ) in my case
I've put count in array because count value is coming from API call in parent component so count is initially 0, which I can solve by loading counter component only after getting data from server, So count in array is not an issue I can remove its dependency, my issue is why inc is not getting update in setInterval's callback
Like if I use class component I can access updated inc from this.state.inc and will get the updated value but in case of functional component I'm not able to achieve the same
Itβs likely that when you wrote the class component it had a bug in it which you didnβt notice.
The functional hook version is forcing you to face that bug first, not later.
Try using a normal variable inside the useEffect rather than using state?
Here is the class version
class Counter extends React.Component{
constructor(props){
super(props);
this.state = {
inc: 0
}
}
componentDidMount(){
const { count, speed = 100 } = this.props;
const counterInterval = setInterval(() => {
let { inc } = this.state;
if(inc < count){
this.setState({ inc: inc + 1 });
}else{
clearInterval(counterInterval);
}
}, speed)
}
render(){
return this.state.inc;
}
}
Like above in componentDidMount I'm able to get updated value of inc, I want the same thing using functional component, can you help to achieve the same using functional component
I can't use a normal variable, as react is not going to render Counter component on it's update
@gaearon any comments?
include inc
in array and use React.useRef to check if count has changed, if it has, destroy the previous interval and recreate it...?
@AyWa 's solution worked for me (i.e using a reducer)
However I was able to solve a similar issue (subscribing once in useEffect and updating state in callback) while still using useState(), alongside with the "function updater" option of the state update function
as mentioned here:
https://reactjs.org/docs/hooks-reference.html#functional-updates
Swapping setInterval
for setTimeout
did the job. I'm also using the "clean up" function in the useEffect
to remove the timeout when the component updates.
import { useState, useEffect } from "react";
const counter = ({ count, speed }) => {
const [inc, setInc] = useState(0);
useEffect(() => {
const counterTimeout = setTimeout(() => {
if (inc < count) {
setInc(inc + 1);
}
}, speed);
return () => clearTimeout(counterTimeout);
}, [inc]);
return inc;
};
export default counter;
See live demo: https://codesandbox.io/s/brave-kalam-j7xkb
In case if you see stale or older values of state or props in useEffect callback or in event handlers, React suggests you to use refs keep the update values. This behavior is not due to React but its due to what values of a variable a javascript function receive at run-time (lexical scope and closures). React devs have tried to address this issue here: https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function
My opinion, the approach in the above doc solves the issue but seems a bit of a life hack. There are more quirks like this with hooks, so I still would prefer a class-based components for writing components with complex state and business logic.
To anyone coming to this conversation late (like me). I ended up using useRef
as suggested above and making a little hook to autosave form data on unmount:
import { useEffect, useRef } from 'react';
import type { FormFields } from './useForm';
type FormState = Readonly<FormFields>;
export type TOptions = {
formState: FormState;
isDirty: boolean;
handleSubmit: (shouldValidate: boolean, formState: FormState) => void;
};
const useAutosave = (options: TOptions): void => {
const formStateRef = useRef(options.formState);
const isDirtyRef = useRef(options.isDirty);
useEffect(() => {
formStateRef.current = options.formState;
}, [options.formState]);
useEffect(() => {
isDirtyRef.current = options.isDirty;
}, [options.isDirty]);
useEffect(
() => () => {
if (isDirtyRef.current) options.handleSubmit(false, formStateRef.current);
},
[],
); // If form is dirty, then save data before tearing down
};
export default useAutosave;
It can then be used in my form component:
useAutosave({ formState, isDirty, handleSubmit });
The key is to useRef
and update the ref.current
with a useEffect every time the values are updated. This gives you a current representation of the state when the cleanup callback is fired.
We've submitted a proposal to solve this. Would appreciate your input!
const element = document.getElementById(
'myId',
) as HTMLInputElement | null;
if (element) {
element.scrollIntoView({
behavior: 'smooth',
});
}