Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactDOM.unmountComponentAtNode() in Test causes Warning

Tags:

reactjs

I built my application wir create-react-app. In the recent version of it, it adds one line in the bootstrapped test setup in Jest to unmount the component (see ReactDOM.unmountComponentAtNode(div)).

import ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  ReactDOM.unmountComponentAtNode(div);
});

It causes a warning when I run the test for my App component.

Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.

My guess: It happens because I have an asynchronous request in componentDidMount():

fetchFoo(bar) {
  fetch(SOME_URL)
    .then(response => response.json())
    .then(result => this.setState({ result }))
    .catch(error => this.setState({ error }));
}

If this is the case, how do I wait in the test for the asynchronous request to finally unmount the component again? I know that I could remove the one liner in the Jest test which causes this, but rather I would like to fix it.

  • Here you can find the issue.

SOLUOTION: Solved it and documented how to solve it over here.

like image 650
Robin Wieruch Avatar asked Feb 05 '18 13:02

Robin Wieruch


2 Answers

The easiest way to resolve this issue is to make your fetch() properly cancel itself when the component is unmounted. Having async requests in a componentDidMount() that don't cancel themselves is bad practice in React, because depending on network speed and UI interactions, they can frequently attempt to update the state of an unmounted component. Either use a cancelable Promise, or use a this.shouldCancel instance variable to indicate weather to call setState() or not, like so:

class LifecycleTest extends React.Component {
  constructor(props){
    super(props)
    this.shouldCancel = false;
    this.state = {
      result:null,
      err:null
    }
  }

  componentDidMount(){
    asyncTask()
      .then(result => !this.shouldCancel ? this.setState({result}) : null)
      .catch(err => !this.shouldCancel ? this.setState({err}) : null);
  }

  componentWillUnmount(){
    this.shouldCancel = true
  }

  render(){
    const {err, result} = this.state
    if(err){
      return <div className="err">{err}</div>
    }else if (result){
      return <div className="result">{result}</div>
    }else{
      return <div className="loading">Loading...</div>
    }        
  }
}

(view on webpackbin)

That said, if you really want to make this test pass without changing the source, you could mock fetch() to return a 'dud' Promise that never resolves. For instance, adding this should fix the error:

window.fetch = jest.fn(() => new Promise((accept, reject) => {}))

However, this is a dreadful hack. The better solution is just to properly cancel network requests on component unmount.

like image 145
wgoodall01 Avatar answered Oct 04 '22 03:10

wgoodall01


You can see in the React docs that componentDidMount() is the recommended location for handling network requests.

The reason you're getting this error, is that you're using setState in an asynchronous call that isn't bound by React's lifecycle, so you end up attempting to setState well after the component has been un-mounted.

Your opportunity to handle/correct this would be in componentWillUnmount(). This would require crafting your fetch() request to somehow be cancelled during the componentWillUnmount() method. You need to wrap your network requests in a cancellable promise to be able to do this, since fetch() doesn't natively support directly cancelling the request.

Ultimately, this would prevent the setState() from being called after you have unmounted the component.

like image 30
mootrichard Avatar answered Oct 04 '22 03:10

mootrichard