Set loading state before and after an action in a React class component
ReactjsJsxReactjs Problem Overview
I have function which dispatched an action. I would like to display a loader before and after the action. I know that react composing the object passed to setState
. the question is how can I update the property in async way:
handleChange(input) {
this.setState({ load: true })
this.props.actions.getItemsFromThirtParty(input)
this.setState({ load: false })
}
Basically, it all worked great if I put this property as part of the application state (using Redux), but I really prefer to bring this property to the component-state only.
Reactjs Solutions
Solution 1 - Reactjs
you can wrap the setState in a Promise and use async/await as below
setStateAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}
async handleChange(input) {
await this.setStateAsync({ load: true });
this.props.actions.getItemsFromThirtParty(input);
await this.setStateAsync({ load: false })
}
Source: ASYNC AWAIT With REACT
Solution 2 - Reactjs
Wrap the rest of your code in the callback of the first setState
:
handleChange(input) {
this.setState({
load: true
}, () => {
this.props.actions.getItemsFromThirtParty(input)
this.setState({ load: false })
})
}
With this, your load
is guaranteed to be set to true
before getItemsFromThirtParty
is called and the load
is set back to false
.
This assumes your getItemsFromThirtParty
function is synchronous. If it isn't, turn it into a promise and then call the final setState
within a chained then()
method:
handleChange(input) {
this.setState({
load: true
}, () => {
this.props.actions.getItemsFromThirtParty(input)
.then(() => {
this.setState({ load: false })
})
})
}
Solution 3 - Reactjs
Here's a typescript implementation of an "async-await" setState:
async function setStateAsync<P, S, K extends keyof S>(
component: Component<P, S>,
state:
((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) |
Pick<S, K> |
S |
null
) {
return new Promise(resolve => component.setState(state, resolve));
}
Solution 4 - Reactjs
The previous answers don't work for Hooks. In this case you get the following error when passing a second argument to setState
>Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
As the error message says, you need to use useEffect
instead in this case (see also this discussion for more detailed information)
Solution 5 - Reactjs
Here's what you can do...
-
Change your action to take in a
onFetchComplete
callback, along with theinput
. -
Change your handleChange to -
handleChange(input) { this.setState({ load: true }, ()=> this.props.actions.getItemsFromThirtParty(input, ()=>this.setState({ load: false })) ); }
This will ensure the action processor code can invoke back your state change callback even if it's not written in a promise based fashion.
Solution 6 - Reactjs
A small update- using promises for the action creators and async/await works great, and it makes the code even cleaner, compared to the "then" chaining:
(async () => {
try {
await this.props.actions.async1(this.state.data1);
await this.props.actions.async2(this.state.data2)
this.setState({ load: false );
} catch (e) {
this.setState({load: false, notify: "error"});
}
})();
Of course it is a matter of taste.
EDIT : Added missing bracket
Solution 7 - Reactjs
I know this is about class components... But in functional components I do this to synchronously set the state:
const handleUpdateCountry(newCountry) {
setLoad(() => true);
setCompanyLocation(() => newCountry);
setLoad(() => false);
}
Just worked for my automatic country detection that should set the form to dirty.
Solution 8 - Reactjs
One approach not mentioned in any of the other answer is wrapping the operation in a setTimeout. This will also work if you use hooks.
Eg.:
handleChange(input) {
this.setState({ load: true })
setTimeout(() => {
this.props.actions.getItemsFromThirtParty(input).finally(() => {
this.setState({ load: false })
});
});
}