From what I understand in react versions 16 (current) and under, setState calls are batched IFF they are made in either component lifecycle events or event handlers. Otherwise, in order to batch calls there is an opt in ReactDOM.unstable_batchedUpdates can be used.
If an event handler is an async function though, the browser will fire the event handler but then a promise will be returned, thus the actual event handler Promise callback won't be run until the next microtasks are picked up in the event loop. In other words, the setState updates do not actually occur in the immediate event handler.
Does this mean that we need to opt into ReactDOM.unstable_batchedUpdates if we want setState updates to be batched in event handlers?
After researching, I believe the answer is that the initial portion of the async event handler (that ends up translating to the executor function of the Promise that is returned underneath the hood) will have setState updates batched, but not anything after any await calls.
This is because everything in the async function body before the first await is translated to the executor function, which is executed within the browser event handler for the event, but everything after ends up as a chained Promise callback for the initial executor function, and these chained callbacks are executed on the microtask queue.
This is all because async () => {} is translated to something like return new Promise().then() where each then is a callback created for code after an await statement.
const onClick = async e => {
// 1 and 2 will be batched
setState(1)
setState(2)
await apiCall()
// 3 and 4 will not be batched
setState(3)
setState(4)
}
Below call will be batched by React and will cause single re-render.
const onClick = (e) => {
setHeader('Some Header');
setTitle('Some Tooltip');
};
Without ReactDOM.unstable_batchedUpdates, React would have made 2 sync calls to re-render components. Now it will have single re-render with this API.
const onClick = (e) => {
axios.get('someurl').then(response => {
ReactDOM.unstable_batchedUpdates(() => {
setHeader('Some Header');
setTitle('Some Tooltip');
});
});
};
Additional Notes:
We used it once in our project and it worked seamless. But I personally prefer to make a state as an object and update things at once rather than this approach. But I understand it is not always possible.
Here we can see that it is in Experimental mode, so not sure if you should use it in production.
Update
Based on the OP comment, below is the version for async await in JavaScript.
const onClick = async e => {
setState(1);
setState(2);
await apiCall();
ReactDOM.unstable_batchedUpdates(() => {
setState(3);
setState(4);
});
}
The above code will trigger re-render 2 times. Once for update 1/2 and another for 3/4.
Related
I have this basic code which updates a state from a handler:
const wait = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
export default function App() {
async function handleClick() {
setData(1);
console.log("first click");
console.log(data);
await wait(1000);
setData(2);
console.log("second click");
console.log(data);
}
const [data, setData] = React.useState(0);
return (
<div>
{data}
<button onClick={() => handleClick(setData)}>Click Me</button>
</div>
);
}
I am trying to understanding the order of operations, could someone please verify or point me in the right direction of what is happening? I have researched around but haven't found conclusive sources on what I think is happening.
we click the button, triggering the handler
the handler runs setData(1), enqueuing a task to the event loop
console.log('first click') runs
we log the state (data), which is still 0, as the setter has only been enqueued
we run into the wait function, so we exit out to the synchronous code flow as we wait for the 1000ms
the sync code finishes running, so we dequeue the next task, which is the setter function, the state is now set to 1, and the view re-renders and reflects that new state
after 1 second has elapsed, we return to the code after wait function
setData(2) enqueues another task
'second click' is logged
0 is stil logged, as our local state has not changed
the sync code finishes, we dequeue the setter, re-rendering the view, causing 2 to show up
Is this all correct? Have I misunderstood anything? Thanks for any help.
Yes, you've got this down correctly, except possibly for the bit
runs setData(1), enqueuing a task to the event loop
This may or may not involve the event loop. What happens in setData is specific to react.js, and won't necessarily enqueue a task in the browser event loop. It certainly does "somehow" schedule the state update and re-rendering of the component - within react.
If I remember correctly, react.js does schedule the render caused by setData(1) for the end of the native click handler, i.e. when the call to your onClick handler returns to react code. And for setData(2), the rendering might actually happen synchronously within the setData call, as react does not control the context. This is however subject to change (the devs are talking about batching multiple updates together asynchronously) and should be considered an implementation detail. If you're curious, just place a console.log in the render function of your component, but do not write any code that relies on the order observed in this test!
I'm having a trouble wrapping my head around this problem: suppose I have a form where I want to handle onSubmit using async callback through event listener and I want to prevent the default behavior. I don't understand why this works:
form.addEventListener('submit', async(event) => {
// do stuff
event.preventDefault();
await asyncFetching(); // first await
// do more stuff
}
And this doesn't:
form.addEventListener('submit', async(event) => {
// do stuff
await asyncFetching(); // first await
event.preventDefault();
// do more stuff
}
I've come to understand from event.preventDefault in async functions that event will have happened by the time await unblocks. But I don't get it. This is what I expect:
Event is triggered once we click the button
Event handler fires up
Event handler finishes execution
Event has happened
What am I missing here?
After some time experimenting and reading up further I think I get it now.
From MDN:
The body of an async function can be thought of as being split by zero or more await expressions. Top-level code, up to and including the first await expression (if there is one), is run synchronously. In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously.
Inside the event handler everything is executed synchronously. Once first await is reached, it is the same as returning a Promise in pending state that has just executed asyncFetching() and has event.preventDefault() inside of its then() block. This return indeed signals that event handler callback has finished its execution, so by the time when asyncFetching() fails or succeeds, event.preventDefault() will still execute, but it won't have any effect because its eventPhase will be 0 meaning Event.NONE ("No event is being processed at this time").
Consider these two usages of useEffect in React:
useEffect(() => {
setSomeState(complexComputation(someDependency));
}, [someDependency]);
vs
useEffect(() => {
setTimeout(() => {
setSomeState(complexComputation(someDependency));
}, 0);
}, [someDependency]);
They effectively do the same thing, but the technical difference is that the function passed to useEffect in the first case is blocking, whereas in the second case it is asynchronous.
Do these two usages differ in any way from the perspective of the React rendering flow? Does React take care of asynchronous scheduling of effects internally, or should I do that manually for synchronous/costly effects?
To clarify the comments below: I initially made this mistake when asking the question.
Some important points that I would like to cover here before answering your question are as follows:
We can easily do asynchronous calls within useEffect() hook.
The call-back function within useEffect() cannot be async, so callback cannot use async-await, because useEffect() cannot return a promise which every async function does by default:
useEffect(async ()=>{
})
// This will return error and will not work
So we can set async function inside the callback function or outside in our component.
Now coming to your question:
useEffect(() => {
setTimeout(() => {
setSomeState(complexComputation(someDependency));
}, 0);
}, [someDependency]);
The main point here is that our useEffect() hook will run initially, and setTimeout() function is simply passed to the Web-API of Browser, Timer starts normally and only once all the code is executed on the call-stack, the callback within setTimeout gets executed.
Check running the below code.
useEffect(() => {
console.log("Line one")
setTimeout(() => {
setSomeState(complexComputation(someDependency));
console.log("line two");
}, 3000);
console.log("Line three");
}, [someDependency]);
Output will be:
Line one
Line Three
Line two // after all other synchronous consoles are executed :)
The question is not about, "what running of useEffect() asynchronously or synchronously mean", but about in what scenarios does useEffect() run first, which runs in both the scenarios.
And since in your code you as using the second argument and passing the value (a state). i-e.,
useEffect(()=>{},[someDependency])
Whenever someDependency again the component re-renders and our useEffect() is again invoked and the code (asynchronous or synchronous) gets executed.
I have seen a React setState method with a callback, which means that the callback will be executed after it is ensured that the new state has been set and the component was re-rendered, e.g this example:
this.setState({value: event.target.value}, function () {
console.log(this.state.value);
}); //new state will be logged to console after it was set and rendered
now, if I am not totally wrong, the same thing can be implemented using async
functions:
async someFunction(){
await this.setState({value: event.target.value});
console.log(this.state.value);
}
My question now is, will it impact performance if I use multiple await setState calls in one function? will it re-render after each setState call and wait for it to finish the rendering process, and then execute the net await setState call etc. and possibly create performance issues? e.g:
async someFunction(){
await this.setState({value: event.target.value});
let result = await someAPICall();
await this.setState({resultApiCall: result});
await.....
}
Yes, you are totally wrong :) setState does not return a promise, therefore you can't just await it. For sure you can wrap the callback into a promise, but you probably don't need that (as you usually don't need to wait for a rerender).
will it impact performance if I use multiple await setState calls in one function?
Sure. Calling a function twice is estimatedly twice as slow as calling it once.
will it re-render after each setState call and wait for it to finish the rendering process, and then [go on]?
Yes, if you would await a Promise, like this:
await new Promise(resolve => this.setState(/*...*/, resolve));
and possibly create performance issues?
No, probably not. Calling setState will execute very fast, you have to call it hundreds of times to impact performance.
In the documentation for setState it has this to say:
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
So I understand if you have an eventhandler like:
handleClick() {
this.stuff1();
this.stuff2();
}
stuff1() {
this.setState({...});
}
stuff2() {
doSomethingWithState(this.state);
this.setState({...});
}
then in the stuff2 method you might not (or is this guaranteed you won't..) see the updates to state from stuff1. React provides a way of 'safely' accessing the previous state by supplying a function to setState() instead of an object. But presumably you could work around this by keeping track of the state yourself as you handle the event and call setState() once at the end of your method. So maybe there is another reason option for supplying a function to setState(). What if other changes to the state could have been batched before the handleClick method is called?
For example if handleClick() is called twice then am I guaranteed to see the state changes from the first call to handleClick() in the second call to handleClick()? or are there other ways the state could be dirty before my event handler has been called?
Anytime you have a state that is derived from the previous state you can run into an issue of them being out of sync if you are not using the callback function vs passing in an object. In your last example you would only be guaranteed if you used the function instead. If you are relying on this.state you could easily run into a situation where handleClick is called once, the state transition gets queued up to be resolved later, you the have handleClick called again and the first state change is still pending and in queue so when you call this.state it will have the same state available to it as the first handleClick had which wouldn't be what you want.
this would be an example of using the callback function, assuming that doSomethingWithState returns the updated state object and is non mutative of course.
stuff2() {
this.setState((state) => {
const updatedState = doSomethingWithState(state);
return state;
})
}
this is a great article on using the function vs setState and also includes a codepen example demonstrating the problem. https://medium.com/#shopsifter/using-a-function-in-setstate-instead-of-an-object-1f5cfd6e55d1#.gm2t01g70
If i understood you right, then the answer is no, your not guaranteed, as setState is an asynchronous operation - so it's kinda equivalent to the first 'problem' you mentioned. You can provide a callback function to the setState method that will be fired as the setState method finishes. Then, in the callback scope, you are guaranteed that the current state is updated and also provided with the old state for comparisons and other stuff....
You can provide callback as said. Or since you want first function to get executed in handleClick(), what you can do is you can call the second function after setting the state in the first function. i.e.,
handleClick() {
this.stuff1();
}
stuff1() {
this.setState({...},
() => {
this.stuff2();
});
}
stuff2() {
doSomethingWithState(this.state);
this.setState({...});
}
I think this will provide the solution for the problem which is raised. Since we can't guarantee synchronous operation of calls to setState as it is an asynchronous operation.