Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cancel a promise when a component is unmounted in ReactJS

I've a component named "Item" which creates and calls a promise when it has been mounted.

class Item extends React.Component{
    constructor(props){
        super(props)
        this.onClick = this.onClick.bind(this)

        this.prom = new Promise((resolve, reject) => {
            setTimeout(() => resolve("PROMISE COMPLETED "+this.props.id),6000)
        })
    }

    componentDidMount(){
        this.prom.then((success) => {
            console.log(success)
        })
    }

    componentWillUnmount(){
       console.log("unmounted")
    }

    onClick(e){
        e.preventDefault()
        this.props.remove(this.props.id)
    }

    render(){
        return (
            <h1>Item {this.props.id} - <a href="#" onClick={this.onClick}>Remove</a></h1>
        )
    }
}

As you can see, the promise calls the resolve 6 seconds after it has been called.

There is another component named "List" that is responsible for showing those items on the screen. The "List" is the parent of the "Item" component.

class List extends React.Component{
    constructor(props){
        super(props)
        this.state = {
            items : [1,2,3]
        }

        this.handleRemove = this.handleRemove.bind(this)
    }

    handleRemove(id){
        this.setState((prevState, props) => ({
            items : prevState.items.filter((cId) => cId != id)
        }));
    }

    render(){
        return (
            <div>
            {this.state.items.map((item) => (
                <Item key={item} id={item} remove={this.handleRemove}  />
            ))
            }
            </div>
        )
    }
}

ReactDOM.render(<List />,root)

On the example above, it shows three Item on the screen.

enter image description here

If I remove any of those components, componentWillUnmount() is called but also the promise which has been created in the removed component is run.

For example, I can see the promise of the second item is run even if I remove the second item.

unmounted 
PROMISE COMPLETED 1 
PROMISE COMPLETED 2 
PROMISE COMPLETED 3

I have to cancel the promise when a component is unmounted.

like image 364
amone Avatar asked Jun 09 '17 12:06

amone


People also ask

How do I cancel promise React?

Promises once fired, cannot be cancelled. So cancelling the promises in our current context means to ignore the result of the promise. When an api call is fired inside a react component, any state update after the component is destroyed (inside the then block of the promise), will cause error.

How do I cancel a promise in useEffect?

And call controller. abort() to cancel one or more number of promises where you included the event listener when the component unmounts. When abort() is called, the promise rejects with an AbortError , so you can listen to that and handle aborted rejects differently.

When a component is unmounted React?

To check if the React component is unmounted, we can set a state in the callback that's returned in the useEffect hook callback. We create the isMounted state with the useState hook. Then we call the useEffect hook with a callback that calls setIsMounted to set isMounted to true .

Can perform a React state update on an unmounted component?

In the happy path you won't have any issues — setState will execute, and your state will successfully update. But if you invoke setState within the context of an asynchronous operation, then you might run into the React warning “Can't perform a React state update on an unmounted component”.


4 Answers

A variation of this https://hshno.de/BJ46Xb_r7 seemed to work for me. I made an HOC with the mounted instance variable and wrapped all async components in it.

Below is what my code roughly loks like.

export function makeMountAware(Component) {
    return class MountAwareComponent extends React.Component {
        mounted = false;
        componentDidMount() {
            this.mounted = true;
        }
        componentWillUnmount() {
            this.mounted = false;
        }
        return (
            <Component 
                mounted = {this.mounted}
                {...this.props}
                {...this.state}
            />
        );
    }
}

class AsyncComponent extends React.Component {
    componentDidMount() {
        fetchAsyncData()
            .then(data => {
                this.props.mounted && this.setState(prevState => ({
                    ...prevState,
                    data
                }));
            });
    }
}
export default makeMountAware(AsyncComponent);
like image 183
Rohan Bagchi Avatar answered Oct 21 '22 01:10

Rohan Bagchi


You can't cancel native ES6 promises. Read more at https://medium.com/@benlesh/promise-cancellation-is-dead-long-live-promise-cancellation-c6601f1f5082

What you can do, however, is maybe use non-native promise libraries like Bluebird or Q, that give you promises that can be cancelled.

like image 21
Chitharanjan Das Avatar answered Oct 21 '22 01:10

Chitharanjan Das


There are various things you can do. The simplest is to reject the promise:

this.prom = new Promise((resolve, reject) => {
     this.rejectProm = reject;
     ...
});

and then

componentWillUnmount(){
   if (this.rejectProm) {
      this.rejectProm();
      this.rejectProm = nil;
   }

   console.log("unmounted")
}
like image 24
Sulthan Avatar answered Oct 20 '22 23:10

Sulthan


Since you are using a timeout in this example you should clear it when unmounting.

class Item extends React.Component{
    constructor(props){
        super(props)
        this.onClick = this.onClick.bind(this)

        // attribute for the timeout
        this.timeout = null;

        this.prom = new Promise((resolve, reject) => {
          // assign timeout
          this.timeout = setTimeout(() => resolve("PROMISE COMPLETED "+this.props.id),6000)
        })
    }

    componentDidMount(){
        this.prom.then((success) => {
            console.log(success)
        })
    }

    componentWillUnmount(){
       // clear timeout
       clearTimeout(this.timeout);
       console.log("unmounted")
    }

My guess is this will result in a rejection and you won't see that console log.

like image 1
valem Avatar answered Oct 20 '22 23:10

valem