setState
is called on unmounted component- race conditions among multiple in-flight requests (responses come back out of order)
Make the promise cancellable
function makeCancellable = promise => {
let isCancelled = false
promise.cancel = () => isCancelled = true
return promise.then(
val => isCancelled ? Promise.reject({isCancelled}) : val,
err => isCancelled ? Promise.reject({isCancelled}) : Promise.reject(err)
)
}
componentDidMount() {
this.cancellable = makeCancellable(fetchDataById(id))
.then(data => this.setState(...))
.catch(({isCancelled, ...err}) => console.log('request is cancelled:', isCancelled))
}
componentWillUnmount() {
this.cancellable.cancel()
}
Or with hooks,
React.useEffect(() => {
let isSubscribed = true
fetchDataById(id).then(data => {
if (isSubscribed) {
setData(data)
}
})
return () => isSubscribed = false
}, [id]);
Step further, even more opinionated, creating a custom hook
// Definition
const usePromise = fn => {
const [state, setState] = React.useState({
isLoading: false,
error: null,
data: null
});
React.useEffect(() => {
let isSubscribed = true;
setState({ isLoading: true });
fn()
.then(data =>
isSubscribed ? setState({ data, isLoading: false }) : null
)
.catch(error =>
isSubscribed ? setState({ error, isLoading: false }) : null
);
return () => (isSubscribed = false);
}, [fn]);
return state;
};
// Usage
const { data, error, isLoading } = usePromise(React.useCallback(() => callApi(id), [id])))