Cleaning up asynchronous calls

If your asynchronous code tries to set the state of a component that has been unmounted, nothing will happen. A warning will be logged, and the state isn't set. It's actually very important that this warning is logged; otherwise, you would have a hard time trying to solve subtle race condition bugs.

The correct approach is to create cancellable asynchronous actions. Here's a modified version of the users() API function that you implemented earlier in the chapter:

// Adapted from:
// https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html
function cancellable(promise) {
let cancelled = false;

// Creates a wrapper promise to return. This wrapper is
// resolved or rejected based on the wrapped promise, and
// on the "cancelled" value.
const promiseWrapper = new Promise((resolve, reject) => {
promise.then(
value => {
return cancelled ? reject({ cancelled: true }) : resolve(value);
},
error => {
return cancelled
? reject({ cancelled: true })
: reject(error);
}
);
});

// Adds a "cancel()" method to the promise, for
// use by the React component in "componentWillUnmount()".
promiseWrapper.cancel = function cancel() {
cancelled = true;
};

return promiseWrapper;
}

export function users(fail) {
// Make sure that the returned promise is "cancellable", by
// wrapping it with "cancellable()".
return cancellable(
new Promise((resolve, reject) => {
setTimeout(() => {
if (fail) {
reject(fail);
} else {
resolve({
users: [
{ id: 0, name: 'First' },
{ id: 1, name: 'Second' },
{ id: 2, name: 'Third' }
]
});
}
}, 4000);
})
);
}

The trick is the cancellable() function, which wraps a promise with a new promise. The new promise has a cancel() method, which rejects the promise if called. It doesn't alter the actual asynchronous behavior that the promise is synchronizing. However, it does provide a generic and consistent interface for use within React components.

Now let's take a look at a container component that has the ability to cancel asynchronous behavior:

import React, { Component } from 'react';
import { fromJS } from 'immutable';
import { render } from 'react-dom';

import { users } from './api';
import UserList from './UserList';

// When the "cancel" link is clicked, we want to render
// a new element in "#app". This will unmount the
// "<UserListContainer>" component.
const onClickCancel = e => {
e.preventDefault();

render(<p>Cancelled</p>, document.getElementById('root'));
};

export default class UserListContainer extends Component {
state = {
data: fromJS({
error: null,
loading: 'loading...',
users: []
})
};

// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}

// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}

componentDidMount() {
// We have to store a reference to any async promises,
// so that we can cancel them later when the component
// is unmounted.
this.job = users();

this.job.then(
result => {
this.data = this.data
.set('loading', null)
.set('error', null)
.set('users', fromJS(result.users));
},

// The "job" promise is rejected when it's cancelled.
// This means that we need to check for the "cancelled"
// property, because if it's true, this is normal
// behavior.
error => {
if (!error.cancelled) {
this.data = this.data
.set('loading', null)
.set('error', error);
}
}
);
}

// This method is called right before the component
// is unmounted. It is here, that we want to make sure
// that any asynchronous behavior is cleaned up so that
// it doesn't try to interact with an unmounted component.
componentWillUnmount() {
this.job.cancel();
}

render() {
return (
<UserList onClickCancel={onClickCancel} {...this.data.toJS()} />
);
}
}

The onClickCancel() handler actually replaces the user list. This calls the componentWillUnmount() method, where you can cancel this.job. It's also worth noting that when the API call is made in componentDidMount(), a reference to the promise is stored in the component. This is necessary otherwise you would have no way to cancel the async call.

Here's what the component looks like when rendered during a pending API call:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.151.141