Should Promises be avoided in React components? - reactjs

I've recently came across this error in React:
warning.js:36 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. Please check the code for the
BillingDetails component.
After digging I found out that this is caused because I do setState in unmounted component like this:
componentWillMount() {
this.fetchBillings(this.props.userType);
}
componentWillReceiveProps({ userType }) {
if (this.props.userType !== userType) {
this.fetchBillings(userType);
}
}
fetchBillings = userType => {
switch (userType) {
case USER_TYPE.BRAND:
this.props.fetchBrandBillings()
.then(() => this.setState({ isLoading: false }));
return;
default:
}
};
fetchBillings is a redux-axios action creator which returns a promise
export const fetchBrandBillings = () => ({
type: FETCH_BRAND_BILLINGS,
payload: {
request: {
method: 'GET',
url: Endpoints.FETCH_BRAND_BILLINGS,
},
},
});
The problem is that when user moves fast on site, component can be unmounted at the time promise resolves.
I found out lot of places around the project where I do something like this:
componentWillMount() {
const { router, getOrder, params } = this.props;
getOrder(params.orderId).then(action => {
if (action.type.endsWith('FAILURE')) {
router.push(`/dashboard/campaign/${params.campaignId}`);
}
})
}
and now I begin to think that using Promises in components could be anti-pattern as component can be unmounted at any time...

The problem is that when user moves fast on site, component can be unmounted at the time promise resolves.
Since native promises are not interruptible, this is completely natural and should be expected at all times. You can overcome this in various ways, but you will ultimately need to track whether the component is still mounted, one way or another, and just don't do anything when the promise resolves/rejects if it's not.
Also, from the docs regarding componentWillMount:
Avoid introducing any side-effects or subscriptions in this method.
Considering this, I'd suggest using componentDidMount for initiating your fetch instead. Overall:
componentDidMount() {
this._isMounted = true;
this.fetchBillings(this.props.userType);
}
componentWillReceiveProps({ userType }) {
if (this.props.userType !== userType) {
this.fetchBillings(userType);
}
}
componentWillUnmount() {
this._isMounted = false;
}
fetchBillings = userType => {
switch (userType) {
case USER_TYPE.BRAND:
this.props.fetchBrandBillings().then(() => {
if (this._isMounted) {
this.setState({ isLoading: false });
}
});
return;
default:
}
};
Additionally, although this is not directly related to your question, you will need to consider that you will have multiple parallel fetch calls running in parallel, leading to a data race. That is, the following is just waiting to happen at any time:
start fetch0
start fetch1
finish fetch1 -> update
...
finish fetch0 -> update
To avoid this, you can track your requests with a timestamp.

Related

React + Redux: proper way to handle after-dispatch logic

I have a component with some internal state (e.g. isLoading) which has access to redux data. In this component I'd like to dispatch a thunk action (api request) resulting in redux data change. After the thunk is completed I need to change the state of my component. As I see it, there are two ways to do so:
Use the promise return by the thunk and do all I need there, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id).then(() => this.setState({ isLoading: false }));
};
Pass a callback to the thunk and fire it from the thunk itself, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id, this.onSaveSuccess);
};
Which one is the correct way to do so?
The safer way is to use the promise implementation, as you'll be sure that function will only run after the promise has been resolved. The second implementation has no inherent flaws, but if anything in your thunk is async, then it will not work correctly since it'll run once the code is reached, not when the code above it finishes executing. When handling anything that can be async (server requests/loading data/submitting data), it's always safer to use Promise implementations.
Probably the best practice for updating component level state or running a callback function based on changing redux state (or any props/state changes for that matter) is to use componentDidUpdate or useEffect:
componentDidUpdate(prevProps, prevState){
if(prevProps.someReduxState !== this.props.someReduxState && this.state.isLoading){
setState({isLoading:false})
}
}
With useEffect:
useEffect(()=>{
if(!props.someReduxState){
setLoading(true)
} else {
setLoading(false)
}
},[props.someReduxState])
However, I might recommend a different approach (depending on the goal especially on initial data fetching) that manages the loading of state within redux:
Initialize your redux state with a loading value instead:
export default someReduxState = (state = {notLoaded:true}, action) => {
switch (action.type) {
case actions.FETCH_SOME_REDUX_STATE:
return action.payload;
default:
return state;
}
}
then in your component you can check:
if (this.props.someReduxState.notLoaded ){
// do something or return loading components
} else {
// do something else or return loaded components
}

When using componentDidUpdate() how to avoid infinite loop when you're state is an array of objects?

I'm creating a react-native app and I need one of my components to use a axios get request when I do an action on another component. But the problem is that my component that I need an axios get request from is not being passed any props and the current state and new state is an array of 20+ objects with each at least 10 key value pairs. So I would need a component did update with a good if statement to not go into an infinite loop. I can't do an if statement with prevState to compare with current state because there is only a minor change happening in state. So I need to know how to stop the component Did Update from having an infinite loop.
state = {
favouriteData: []
}
componentDidMount () {
this.getFavouriteData()
}
componentDidUpdate (prevProps, prevState) {
if (this.state.favouriteData !== prevState.favouriteData){
this.getFavouriteData()
}
}
getFavouriteData = () => {
axios.get('http://5f46425d.ngrok.io')`enter code here`
.then(response => {
const data = response.data.filter(item => item.favourite === true)
this.setState({
favouriteData: data
})
})
}
The issue is that you are trying to compare 2 object references by doing the following. It will always return since the references are always different.
if (this.state.favouriteData !== prevState.favouriteData) {...}
To make life easier, we can use Lodash(_.isEqual) to deal with deep comparison of objects.
state = {
favouriteData: []
}
componentDidMount () {
this.getFavouriteData()
}
componentDidUpdate (prevProps, prevState) {
this.getFavouriteData(prevState.favouriteData)
}
getFavouriteData = (prevData) => {
axios.get('http://5f46425d.ngrok.io')
.then(response => {
const data = response.data.filter(item => item.favourite === true);
// compare favouriteData and make setState conditional
if (!prevState || !_.isEqual(prevData, data)) {
this.setState({
favouriteData: data
})
}
})
}
You should use react-redux to avoid this kind of issues. Assuming you are not using flux architecture, you can pass this.getFavouriteData() as props to the other component like:
<YourComponent triggerFavouriteData = {this.getFavouriteData}/>

Issues with state-changes in submit method of AtlasKit Form

In the submit method of an Atlaskit Form, I want to change a value of a state property that results in the form being hidden:
<Form onSubmit={data => {
return new Promise(resolve => {
setShowForm(false);
resolve();
})
}}>
</Form>
However, this results in a React error:
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in the
componentWillUnmount method.
The error disappears when i set that value a little later:
setTimeout(() => setShowForm(false));
So apparently the form is still unmounting while i change state (although i don't know why that should affect on the form, but i am not too familiar with React yet). What is the approach i should be taking here?
This is because you made an asynchronous request to an API, the request (e.g. Promise) isn’t resolved yet, but you unmount the component.
You can resolve this issue by maintaining a flag say _isMounted to see if component is unmounted or not and change the flag value based on promise resolution.
// Example code
class Form extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this._isMounted = true;
axios
.get('my_api_url')
.then(result => {
if (this._isMounted) {
this.setState({
data: result.data.data,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}

Does React batch props updates in some cases?

I'm curious whether React batches updates to props in some rare cases? There is no mention of this in the docs, but I couldn't come up with any other explanation of the following situation.
I have an equivalent to the following code:
// Connected component
class MyComponent extends React.Component {
state = {
shouldDisplayError: false,
};
componentDidUpdate(prevProps) {
console.log("componentDidUpdate: " + this.props.dataState);
if (
prevProps.dataState === "FETCHING" &&
this.props.dataState === "FETCH_FAILED"
) {
this.setState(() => ({ shouldDisplayError: true }));
}
}
render() {
return this.state.shouldDisplayError && <p>Awesome error message!</p>;
}
}
const mapStateToProps = (state) => {
const dataState = getMyDataStateFromState(state);
// dataState can be "NOT_INITIALIZED" (default), "FETCHING", "FETCH_SUCCEEDED" or "FETCH_FAILED"
console.log("mapStateToProps: " + dataState);
return {
dataState,
};
};
export default connect(mapStateToProps)(MyComponent);
// A thunk triggered by a click in another component:
export async const myThunk = () => (dispatch) => {
dispatch({ type: "FETCHING_DATA" });
let result;
try {
result = await API.getData(); // an error thrown immediately inside of here
} catch (error) {
dispatch({ type: "FETCHING_DATA_FAILED" });
return;
}
dispatch({type: "FETCHING_DATA_SUCCEEDED", data: result.data});
}
// Let's say this is the API:
export const API = {
getData: () => {
console.log("> api call here <");
throw "Some error"; // in a real API module, there's a check that would throw in some cases - this is the equivalent for the unhappy path observed
// here would be the fetch call
},
}
What I would expect to see in the console after triggering the API call (which immediately fails), is the following:
mapStateToProps: FETCHING
componentDidUpdate: FETCHING
> api call here <
mapStateToProps: FETCH_FAILED
componentDidUpdate: FETCH_FAILED
However, I can see the following instead:
mapStateToProps: FETCHING
> api call here <
mapStateToProps: FETCH_FAILED
componentDidUpdate: FETCH_FAILED
So the MyComponent component never received the "FETCHING" dataState, although it has been seen in the mapStateToProps function. And thus never displayed the error message. Why? Is it because such fast updates to a component's props are batched by React (like calls to this.setState() in some cases)???
Basically, the question is: If I dispatch two actions, really quickly after each other, triggering a component's props updates, does React batch them, effectively ignoring the first one?
The first time, a component is rendered, componentDidUpdate is NOT called. Instead, componentDidMount is called. Log to console in componentDidMount as well to see the message.

Endless loop after changing state

I've created the component, which passes the function to change its state to the child.
//parent component
setSubject = (id) => {
this.setState({
currentSubject: id
});
}
<Subjects authToken = {this.state.authToken} subjects = {this.state.subjects} setSubject = {this.setSubject} />
//child component
<li onClick={() => this.props.setSubject(subject.id)}>Egzamino programa</li>
That state is passed to another component.
<Sections authToken = {this.state.authToken} subject = {this.state.currentSubject} />
From there I am using componentDidUpdate() method to handle this change:
componentDidUpdate() {
if (this.props.subject) {
axios.get(`http://localhost:3000/api/subjects/${this.props.subject}/sections?access_token=${this.props.authToken}`)
.then(response => {
this.setState({
sections: response.data
})
}).catch(err => {
console.log(err)
})
}
}
Everything works as expected, BUT when I try to console.log something in Sections component after I've set currentSubject through Subjects component, that console.log executes endless number of times (so is get request, i guess...) It is not goot, is it? And I cannot understand why this happens..
The bug is in your componentDidUpdate method.
You are updating the state with
this.setState({
sections: response.data
})
When you do that, the componentDidUpdate life-cycle method will be called and there you have the endless loop.
You could make a quick fix by using a lock to avoid this issue. But there might be a better design to solve your issue.
The quick fix example:
if (this.props.subject && !this.state.sectionsRequested) {
this.setState({
sectionsRequested: true,
});
axios.get(`http://localhost:3000/api/subjects/${this.props.subject}/sections?access_token=${this.props.authToken}`)
.then(response => {
this.setState({
sections: response.data,
});
})
.catch(err => {
console.log(err);
});
}
It might better to use componentWillReceiveProps for your case.
You are interested in getting data based on your this.props.subject value. I can see that because you're using it as part of your url query.
You might be interested in using componentWillReceiveProps and componentDidMount instead of componentDidUpdate
componentDidMount(){
if(this.props.subject){
/* code from componentDidUpdate */
}
}
componentWillReceiveProps(nextProps){
if(nextProps.subject && this.props.subject !== nextProps.subject){
/* code from componentDidUpdate */
}
}

Resources