Understanding Code Flow For Setting State in React - javascript

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!

Related

Race condition being set up by React.StrictMode double firing functions

I think I understand why React.StrictMode causes functions to be called twice. However, I have a useEffect that loads data from my api:
useEffect(() => {
async function fetchData() {
const data = await getData();
setData(data);
}
fetchData();
}, []);
In my getData() function I call a maintenance script cullRecords() that cleans up my data by deleting records over a certain age before returning the data:
async function getData(){
let results = await apiCall();
cullRecords(results);
return results;
}
Here's the rub: React.StrictMode fires the getData() function twice, loading up the apiCall() twice and firing the cullRecords() twice. However, by the time the second cullRecords() subscript fires, my API throws an error because those records are already gone.
While it's not the end of the world, I'm curious if I'm doing something wrong, or if this is just a fringe case, and not to worry about it.
You can read through here also:
https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
The same issue can occur if the user leaves/visits a route quickly for example (which is what development mode is simulating here). It might not be the the best approach to call a backend maintenance script when a UI component is being rendered.
As per the race condition happening on APIs, it’s useful to implement debouncing methods as it’s explaned below.
The debounce() function forces a function to wait a certain amount of
time before running again. The function is built to limit the number
of times a function is called.
You can either use debouncing on either server-side or client side as they have pretty similar implementation.

Why does this official React testing recipe using await/act/async actually work?

I've been working with Javascript for a couple of years now, and with my current knowledge of the event loop I'm struggling to understand why this testing recipe from the React docs work. Would someone be able to break down exactly what happens in each step there? To me, it seems magical that this works in the test:
await act(async () => {
render(<User id="123" />, container);
});
// expect something
The component looks like this (copying in case that link gets deprecated):
function User(props) {
const [user, setUser] = useState(null);
async function fetchUserData(id) {
const response = await fetch("/" + id);
setUser(await response.json());
}
useEffect(() => {
fetchUserData(props.id);
}, [props.id]);
if (!user) {
return "loading...";
}
return (
<details>
<summary>{user.name}</summary>
<strong>{user.age}</strong> years old
<br />
lives in {user.address}
</details>
);
}
There's no implicit or explicit return happening on the render, so how does act know to await the async stuff happening in the component (fetching etc)?
To me, this would make more sense:
await act(async () => render(<User id="123" />, container));
or (which is the same thing):
await act(async () => {
return render(<User id="123" />, container);
});
or even:
await act(render(<User id="123" />, container));
But that doesn't seem to be how people use it, or how it was intended to be used, so I'm a bit lost. I've seen the same examples with enzymes mount.
I don't want to create a fragile test, so I really want to understand this.
Does it have something to do with the callback being async i.e. does that append something to the event loop last, making it so that the await waits for everything inside render to happen before resolving?
I'm drawing a blank here and am struggling in the react doc jungle, because everyone seems to use this pattern, but no one really explains why or how it works.
Thanks for the help in advance!
When looking closer at the source code of react-dom and react-dom/test-utils it seems like what's making this whole thing work is this setImmediate call happening after the first effect flush in recursivelyFlushAsyncActWork.
It seems like act chooses to use this recursivelyFlushAsyncActWork simply because the callback has the signature of being "thenable", i.e. a Promise. You can see this here.
This should mean that what happens is (simplified) this:
The useEffect callback is flushed (putting fetch on the event loop).
The setImmediate callback "ensures" our mock promise / fetch is resolved.
A third flush happens by a recursion inside the setImmediate callback (called by enqueueTask) making the state changes appear in the DOM.
When there's nothing left to flush it calls the outer most resolve and our act resolves.
In the code that looks kinda like this (except this is taken from an older version of react-dom from the node_modules of my React project, nowadays flushWorkAndMicroTasks seems to be called recursivelyFlushAsyncActWork):
function flushWorkAndMicroTasks(resolve) {
try {
flushWork(); // <- First effect flush (fetch will be invoked by the useEffect?)
enqueueTask(function () { // <- setImmediate is called in here (finishes the fetch)
if (flushWork()) { // <- Flush one more time and the next loop this will be false
flushWorkAndMicroTasks(resolve);
} else {
resolve(); // <- resolve is called when flushWork has nothing left to flush.
}
});
} catch (err) {
resolve(err);
}
}
Additional info (update)
Unless I'm mistaken this should mean that await act(async () => { render(...); }); "only" awaits one event loop unless a new task is added by the latest flush. I.e. it's possible to add promises recursively if there's a flush in between, also micro tasks such as promise chains will probably resolve during the first loop (since they are technically "blocking" (source)).
That means that if you add a timer or something else in your mock or React code that might naturally need multiple loops to resolve, it's not gonna be awaited because the "after" event is not captured before resolving since the then listeners are not attached / returned to the outer promise (please correct me if I'm wrong!).

Are setState updates in async event handlers batched?

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.

Does render get called immediately after setState?

I have the following function:
onSelectDepartment = (evt) => {
const department = evt.target.value;
const course = null;
this.setState({ department, course });
this.props.onChange({ name: 'department', value: department });
this.props.onChange({ name: 'course', value: course });
if (department) this.fetch(department);
};
The question is, after the setState function get called, the render function on the component will be executed immediately or after function call is finished?
render function on the component will be executed immediately or after
function call is finished?
No one can take the guarantee of when that render function will get called, because setState is async, when we call setState means we ask react to update the ui with new state values (request to call the render method), but exactly at what time that will happen, we never know.
Have a look what react doc says about setState:
setState() enqueues changes to the component state and tells React
that this component and its children need to be re-rendered with the
updated state. This is the primary method you use to update the user
interface in response to event handlers and server responses.
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.
Check this answer for more details about async behavior of setState: Why calling setState method doesn't mutate the state immediately?
If you are looking execute some piece of code only once the setState is completed you can use below format.
this.setState({
flag: true
}, ()=> {
// any code you want to execute only after the newState has taken effect.
})
This is the way to make sure your desired piece of code only runs on the new state.

Wait for a React Component to finish updating

In my tests, I would like to block my main thread until one of my components finishes going through its lifecycle methods, through componentDidUpdate(), after I trigger an event that causes it to add children components to itself. How can I do so?
Something like this:
describe('my <Component />', () => {
it('should do what I want when its button is clicked twice', () => {
const wrapper = mount(<Component />);
const button = wrapper.find('button');
// This next line has some side effects that cause <Component /> to add
// some children components to itself...
button.simulate('click', mockEvent);
// ... so I want to wait for those children to completely go through
// their lifecycle methods ...
wrapper.instance().askReactToBlockUntilTheComponentIsFinishedUpdating();
// ... so that I can be sure React is in the state I want it to be in
// when I further manipulate the <Component />
button.simulate('click', mockEvent);
expect(whatIWant()).to.be.true;
});
});
(I want to do this because, right now, I get this warning:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op.
I believe I'm getting it because my tests cause my component to change its internal state more quickly than React's internal multithreading magic can keep up with, so by the time I i.e. run button.simulate('click') the second time, React has instantiated the new child components but hasn't finished mounting them yet. I think that waiting for React to finish updating my Component and its children is the best way to solve that problem.)
Try wrapping your expect() blocks in a setImmediate() block:
describe('my <Component />', () => {
it('should do what I want when its button is clicked twice', (done) => {
const wrapper = mount(<Component />);
const button = wrapper.find('button');
button.simulate('click', mockEvent);
button.simulate('click', mockEvent);
setImmediate(() => {
expect(whatIWant()).to.be.true;
done();
});
});
});
Here's what's going on: To handle asynchronicity, Node and most browsers have an event queue behind the scenes. Whenever something, like a Promise or an IO event, needs to run asynchronously, the JS environment adds it to the end of the queue. Then, whenever a synchronous piece of code finishes running, the environment checks the queue, picks whatever is at the front of the queue, and runs that.
setImmediate() adds a function to the back of the queue. Once everything that is currently in the queue finishes running, whatever is in the function passed to setImmediate() will run. So, whatever React is doing asynchronously, wrapping your expect()s inside of a setImmediate() will cause your test to wait until React is finished with whatever asynchronous work it does behind the scenes.
Here's a great question with more information about setImmediate(): setImmediate vs. nextTick
Here's the documentation for setImmediate() in Node: https://nodejs.org/api/timers.html#timers_setimmediate_callback_args

Categories