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
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!
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.
I've been using the following pattern to test conditions after state updates in my components using react.
await expect(new Promise((resolve) => {
let intervalId = setInterval(() => {
component.update();
if (myCondition) {
clearInterval(intervalId);
resolve(true);
}
}, 25);
})).resolves.toBe(true);
This works well and is guaranteed to work but it's a pain to write and quite verbose.
I've been looking into possibly using setImmediate rather than setInterval. This would prevent the polling (and allows me to test negative assertions which currently aren't possible without introducing another level of verbosity with a try/catch), but is it guranteed to work with react async mechanisms such as setState?
For example what happens if react decides to batch try to batch some setState events together or something along those lines and setImmediate gets put in the event loop queue before react dispatches the state update actions?
I don't want to introduce flakiness to my tests.
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.
In the react docs it recommends making initial network requests in the componentDidMount method:
componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request. Setting state in this method will trigger a re-rendering.
If componentWillMount is called before rendering the component, isn't it better to make the request and set the state here? If I do so in componentDidMount, the component is rendered, the request is made, the state is changed, then the component is re-rendered. Why isn't it better to make the request before anything is rendered?
You should do requests in componentDidMount.
If componentWillMount is called before rendering the component, isn't it better to make the request and set the state here?
No because the request won’t finish by the time the component is rendered anyway.
If I do so in componentDidMount, the component is rendered, the request is made, the state is changed, then the component is re-rendered. Why isn't it better to make the request before anything is rendered?
Because any network request is asynchronous. You can't avoid a second render anyway unless you cached the data (and in this case you wouldn't need to fire the request at all). You can’t avoid a second render by firing it earlier. It won’t help.
In future versions of React we expect that componentWillMount will fire more than once in some cases, so you should use componentDidMount for network requests.
You should use componentDidMount.
Why isn't it better to make the request before anything is rendered?
Because:
Your request will almost certainly not finish before the component is rendered (unless rendering large amounts of markup, or you are on a zero latency quantum entanglement connection), and the component will ultimately need to re-render again, most of the time
componentWillMount is also called during server-side rendering (if applicable)
However, if you were to ask, isn't it better to initiate a request in componentWillMount (without actually handling it in place), I would definitely say yes (ES6), and I do this myself to occasionally cut a few milliseconds from load times:
componentWillMount() {
// if window && window.XMLHttpRequest
if (!this.requestPromise) {
this.requestPromise = new Promise(resolve => {
// ... perform request here, then call resolve() when done.
});
}
}
componentDidMount() {
this.requestPromise.then(data => ...);
}
This will start preloading your request during componentWillMount, but the request is only handled in componentDidMount, whether it is already finished by then or still in progress.
You should make the request in componentDidMount as no side-effects requests should be made in componentWillMount. It's fine to setState in componentWillMount, if you setState in componentDidMount you will immediately trigger a second re-render.
You will read that it's an anti-pattern (UGHHH) and some linters have it prohibited (eslint-react-plugin), but I wouldn't pay huge attention to that as sometimes it's the only way to interact with the DOM. You can set your default state either in willMount or as a method property ( state = { } ), if you're using the associated babel stage
As you say the component will be rendered already once, but this is good because you can display some kind of Loader or any other form of information that a resource is loading.
class MyComp extends Component {
// What I do with stage 0
state = { mystate: 1 }
// What you might want to do if you're not
// on the experimental stage, no need to do
// the whole constructor boilerplate
componentWillMount() {
this.setState({ mystate: 1 });
}
componentDidMount() {
dispatch(yourAction());
// It's fine to setState here if you need to access
// the rendered DOM, or alternatively you can use the ref
// functions
}
render() {
if (!this.props.myCollection) return <Loader />
return (
<div> // your data are loaded </div>
)
}
}
The real reason to avoid fetches in lifecycle hooks before the render method is because the React community is planning to make the render method calls asynchronous.
Check the response from gaeron here : https://github.com/reactjs/reactjs.org/issues/302
Now this means placing an asynchronous action like fetch(or any asynchronous operation for that matter) in any of the lifecycle methods before render, will interfere with the rendering process.
So unlike the synchronous execution that you would imagine today :
1. constructor << imagine firing fetch() here >> => 2. getDerivedStateFromProps << fetch completes, callback added to callback queue by event loop >> => 3. render => 4. componentDidMount => 5. callbacks executed in the order that they were added so fetch callback runs now.
it would instead be like this :
1. constructor << imagine firing fetch() here >> => 2.
getDerivedStateFromProps << in the meantime, fetch completes and callback gets queued >> << imagine firing async render() here. its callback too gets queued >> => callbacks get executed in the order that they were added 3. so fetch callback runs first => 4. render callback runs => 5. componentDidMount
This interference may result in state changes getting reverted because render may apply an earlier state that overrides the changes made by fetch.
Another other reason being that fact that it is the componentDidMount lifecycle that guarantees the presence of the corresponding component on DOM and if fetch tries to manipulate the DOM even before its available or is updated, it could result in faulty application display.