I'am puzzled by the setState() accepted object.The code link is here https://codepen.io/DRL9/pen/jadbWq and the code is as follow:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
intervalCount: 1,
buttonCount: 1
};
this.increment = 1;
this.intervalId = null;
}
tick() {
this.setState({
intervalCount: this.state.intervalCount + this.increment
});
this.setState({
intervalCount: this.state.intervalCount + this.increment
});
}
onClick() {
this.setState({
buttonCount: this.state.buttonCount + this.increment
});
this.setState({
buttonCount: this.state.buttonCount + this.increment
});
}
componentDidMount() {
this.intervalId = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
return <div>
<div>
interval counter: {this.state.intervalCount}
</div>
<button onClick={this.onClick.bind(this)}>increment</button>
<div>
button counter: {this.state.buttonCount}
</div>
</div>;
}
}
I expect that intervalCount will increment 1 like the behavior when I click increment button. However, it increment 2 each tick.
The only different is that one updated is in setInterval function and the other updated is in onClick function.
Why are their behavior different?
We can't talk in absolutes regarding the timing of setState as it is, by definition, unpredictable. The state changes may be delayed to some time in the future, and this behavior may be different depending on the version of React that you are using.
In the example provided, React is delaying state updates until the onClick handler has finished running. React knows when this handler is finished running because we are passing the handler through JSX's onClick (which is then processed internally by React):
// React processes the onClick handler below
<button id="btn" onClick={this.onClick.bind(this)}>increment</button>
If we were to instrument the onClick logic ourselves, by manually grabbing the button element from the DOM and adding a click event listener that calls our onClick handler, the button updates identically to the setInterval (React doesn't know that we are updating state within a click handler, so it chooses not to make the optimization of batching the calls to setState).
See this codepen, where the button counter has a click handler manually added to it in the componentDidMount function as opposed to using JSX's onClick. Notice that the button counter now increments in intervals 2 instead of 1.
I want to stress that this behavior is not deterministic and you should never use this.state within your setState function. Instead, you want to use the variation of setState that accepts an updater function that contains the previous state. Then, build your new state from the state passed to the updater:
this.setState(state => ({
buttonCount: state.buttonCount + this.increment
}));
See this codepen, which uses an updater to update the button counter, producing the expected effect of updating the button counter in intervals of 2.
For more info on setState see the official documentation.
From the documentation for setState:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
This is saying that when you reference state data (this.state.buttonCount or this.state.intervalCount) immediately after you've changed it using setState (as you do in both functions on the second setState command) the behavior will be unpredictable. Maybe setState immediately updates the state data, as it seems to be doing with intervalCount, and maybe setState waits to update the state data so it can batch it later, as it seems to be doing with buttonCount. As a developer we should avoid exposing ourselves to such unpredictable behavior by using other variables when we want to modify the state multiple times during the same event.
As to why intervalCount is fairly consistently being updated immediately (and thus incrementing the second time) and buttonCount is consistently being batched (and only incrementing one time for the two calls to setState) my guess is this: onClick is triggered by a user interaction so the React engine probably guesses that during user interactions a lot of state may be changing, so it batches the calls to setState, maybe until the event fully propagates. tick, on the other hand, is triggered by an internal callback without any user interaction being processed, so the React engine probably guesses it's safe to update state right away without batching.
Related
I'm a beginner to React JS.
I faced this weird situation,
const [counter, setCounter] = useState(0);
const incrementCounter = () => {
setCounter(counter + 1);
console.log(counter, "Log inside the function");
};
useEffect(() => {
console.log(counter, "Log inside the useEffect");
}, [counter]);
return (
<div>
<h1>{counter}</h1>
<button onClick={incrementCounter}>Increment</button>
</div>
);
So when incrementCounter function is triggered counter value will be increased by 1 and right after that I'm logging the value in the console. But it displays the value as 0. When I use the useEffect hook to check the changes of count state it I'm getting the correct value as 1 when I log the count value inside the useEffect scope. What is the reason for not displaying in incrementCounter function and the reason for displaying the correct value inside the useEffect hook.
according to React docs
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. In the rare case that you need to force the DOM update to be applied synchronously, you may wrap it in flushSync, but this may hurt performance.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
States value will change only after the component has rendered.
In this case the log inside the function will keep the state value even after the SetCounter, once it completes the component will render again.
The useEffect is triggered when the state has changed on such render so it shows the new value.
A new incrementCounter function object is being redefined with every render. Each time it is defined, its own internal counter is based on the value of counter state at that point. The counter within that particular incrementCounter function object is stale and doesn't get updated anymore.
This is because the setCounter is asynchronous.
If you use setTimeOut you can clearly observe it
setTimeout(() => {
console.log(counter, "Log inside the function");
}, 2000);
I have a react antd component that uses the onMouseEnter prop that calls a query to my api. I want to make it so that when onMouseEnter first calls the api its works fine...then if the user somehow keeps moving the mouse over the component, not to call the api that many times. Maybe a timer between consecutive calls to the api so the network doesnt get flooded with calls.
This is the antd component very simple:
<Select onMouseEnter={handleMouseEnter} />
This is the function executed everytime the onMouseEnter is called:
const handleMouseEnter = () => {
refetchQueries();
}
Basically dont do consectuive calls if the user accidentally enters the component 5 times a couple seconds. And Im not sure if debounce would work here because from what I understood debounce calls the function x many seconds after the last time it was invoked.
You can create a new boolean state and set this state when onMouseEnter triggered and with setTimeout you can reset this state to initial condition and then you can use this state like condition to trigger onMouseEnter event like this
<Select onMouseEnter={ () => { this.state.isNotActive && {
setState({isNotActive: false});
handleMouseEnter;
setTimeout(() => { setState({isNotActive: true})}, 5000);
} } } />
setState updates state asynchronously. It's my understanding that, when using a class component, you can do something like this to ensure certain code is executed after one setState updates state:
setState({color: red}, callbackThatExecutesAfterStateIsChanged);
I'm using a functional component & hooks. I'm aware, here, useEffect()'s callback will execute everytime after color state changes and on initial execution.
useEffect(callback, [color]);
How can I replicate similar behaviour as the class component example - that is, to execute a chunk of code once after one setState() successfully changes state and not on initial execution?
If you ask me, there is no safe way to do this with hooks.
The problem is that you both have to read and set an initialized state in order to ignore the first update:
const takeFirstUpdate = (callback, deps) => {
const [initialized, setInitialized] = useState(false);
const [wasTriggered, setWasTriggered] = useState(false);
useEffect(() => {
if (!initialized) {
setInitialized(true);
return;
}
if (wasTriggered) {
return;
}
callback();
setWasTriggered(true);
}, [initialized, wasTriggered]);
};
While the hook looks like it works, it will trigger itself again by calling setInitialized(true) in the beginning, thus also triggering the callback.
You could remove the initialized value from the deps array and the hook would work for now - however this would cause an exhaustive-deps linting error. The hook might break in the future as it is not an "official" usage of the hooks api, e.g. with updates on the concurrent rendering feature that the React team is working on.
The solution below feels hacky. If there's no better alternative, I'm tempted to refactor my component into a class component to make use of the easy way class components allow you to execute code once state has been updated.
Anyway, my current solution is:
The useRef(arg) hook returns an object who's .current property is set to the value of arg. This object persists throughout the React lifecycle. (Docs). With this, we can record how many times the useEffect's callback has executed and use this info to stop code inside the callback from executing on initial execution and for a second time. For example:
initialExecution = useRef(true);
[color, setColor] = useState("red");
useEffect(() => {
setColor("blue");
});
useEffect(() => {
if (initialExecution.current) {
initialExecution.current = false;
return;
}
//code that executes when color is updated.
}, [color]);
I am creating a component with an animation that occurs with a css class toggle. Sandbox of the example here.
The css class is applied conditionaly against the transitioned field, so we should get an animation when the transtioned field goes form false to true.
Problem:
The animation doesn't happen in the case where the state if modified like this :
animateWithoutST = () => {
this.setState({transitioned: false},
() => this.setState({transitioned: true}))
}
But it works if it the second setState is called within a setTimeout callback like this:
animateWithST = () => {
this.setState({ transitioned: false },
() => {
setTimeout(() => this.setState({ transitioned: true }))
}
)
}
Why isn't animateWithoutST working as expected although my component is rendering in the right order ?
This looked definitely bizarre and I had to dig into it until I understood what is happening.
So, yeah without the setTimeout it doesn't work, not even in the componentDidUpdate method and there is an explanation: you are changing the state, and it is updated, and render is called twice BUT because of browser optimization we don't see the animation happening: "browsers are not re-rendering stuff that changed in the same animation frame".
When you use the setTimeout you are forcing the 2nd state update to go into the next animation frame and voila you see the animation. Even with the current timeout value set to 0, it might not work on some browsers, but if you set the value to > 16ms aprox, it prob will work always (you need a value greater than an animation frame).
You can also use requestAnimationFrame, twice to assure your both state updates fall into different animation frames, check it here.
I found all this info in this article, check it because it is extremely well explained. Does now make sense to you?
You should be using componentDidUpdate instead:
componentDidUpdate(prevProps, prevState) {
if(!this.state.transitioned) {
this.setState({
transitioned: true
});
}
}
This makes sure that your second setState gets called AFTER the component has updated.
setState is asyncronous which means calls do not happen back to back. So multiple calls to setstate might not happen as you expect them to. Read this to know more about setstate()
I have a function stopRecording() that I'd like to be called when a timer runs out, or when someone presses a stop button. The problem is that when it is called when the timer runs out (the first half of the render function) it is called continuously, despite me bracketing it in an if clause. When it is called as a button event (in the return half of the render function) then it works fine.
Note my console logs. When I open the console in Chrome and let the timer run out, the console logs I marked as successful in my code body runs, but NOT ones that I commented with //!!!. I also get the following error continuously: Invariant Violation: setState(...): Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
//...
stopRecording: function() {
if (this.state.recording){
console.log("this will log as expected")
this.setState({recordingStatus:"",
recording:false})
console.log("this will NOT log as expected'") //!!!
this.props.emit("recEmit")
}
}
render: function() {
var timeBar;
var countdown = "0";
var timeBarFill = "#FF9090"
if (this.state.recording){
countdown = new Date()-this.state.startTime
timeBarFill = "#FF3830";
if (countdown > this.state.maxRecLength){
console.log('this will log as expected')
countdown=0
this.stopRecording()
console.log('this will NOT log as expected') //!!!
};
}
//...
return(
//...
<button type="button" id="button" onClick={this.stopRecording}><b>Stop</b></button>
//...
)
You should never call setState inside render(): https://github.com/facebook/react/issues/5591#issuecomment-161678219
As render should be a pure function of the component's props and state, which means that it should not have any side effects (like changing its own state).
Also, you can't guarantee that React will call your component's render() method when your countdown is about to expire. Consider using setTimeout in component's life cycle methods.
I think that this is due to how states work in react. This article explains it pretty well. I suggest to read it but I can some it up for you:
setState is usually called asynchronously.
if setState is not triggered by an event that React can keep track of, such as onClick, it is called synchronously.
This means that when you are using onClick everything goes fine because your call of setState in stopRecording does not block and the function finishes before a re render is called. When a timer triggers it this happens synchronously, the state changes and render is called again.
Now, I still do not understand how it can run continuously, since it should have set the state.recording variable to false and I don't see anything that turns it back to true.
Also, be careful to use states just for variables that are truly states: change with time. The maxRecordinLength does not seem to be a state variable, and same for startTime.
EDIT:
after I saw the update I realized that the main issue here is changing a state inside of the render method. I posted this link in a comment here but I think it is worth explaining.
Basically, you can solve your issue by calling a setTimer function in the componentDidMount function of react-- more on this here.
Something like:
componentDidMount: function(){
setTimer(this.myFunction, this.props.maxRecLength);
},
And you myFunction would look like this:
myFunction: function(){
this.setState({timeElapsed: true});
},
Then you can use this.state.timeElapsed in your render function, and whatever is in there will be displayed after the maxRecLength is reached.