Search code examples
javascriptreactjsreact-reduxinfinite-looplifecycle-hook

How can I call `dispatch` from within `componentDidUpdate` without ending up in a infinite loop?


I'm making a very simple React component which shows data obtained from a remote source. Before fetching the data, it has to wait for the user to be correctly logged in. Here is how I am trying to do this:

class MyComponent extends React.Component {
  componentDidUpdate () {
    if (this.props.loggedIn) {
      api.getData()
        .then(() => {
          this.props.dispatch({
            type: 'gotData'
          })
        }, (reason) => {
          this.props.dispatch({
            type: 'getDataFailed'
          })
        })
    }
  }
}

In my own words, every time a part of the state relevant to this component (in this case, the loggedIn prop) is updated, componentDidUpdate is called and I can get the data if I see the user is logged in.

This actually works very well, except for one major problem: It seems that calling dispatch eventually triggers componentDidUpdate again, so I end up in an infinite loop. I don't even have to be listening for these dispatch events anywhere, the simple fact of using the dispatch function is enough to trigger componentDidUpdate again.

My guess is that dispatch executes my root reducer which internally uses setState, thereby triggering the whole update lifecycle chain again.

How is this kind of thing usually done? How can I call dispatch from within componentDidUpdate without ending up in an infinite loop?


Solution

  • One easy way to solve this is to add an additional flag, for example loggingIn, that you set (by dispatching) before the async action and you reset after. You also need to keep track of when the login failed to differentiate this situation from when login process wasn't started (unless you want to restart the process automatically on fail):

    class MyComponent extends React.Component {
      componentDidUpdate () {
        const { loggedIn, loggingIn, loginFailed, dispatch } = this.props;
        if (!loggingIn && !loggedIn && ! loginFailed) {
          dispatch({
            type: 'gettingData' // this one sets loggingIn to true, loggedIn to false, loginFailed to false
          });
          api.getData()
            .then(() => {
              dispatch({
                type: 'gotData' // this one sets loggingIn to false, loggedIn to true, loginFailed to false
              })
            }, (reason) => {
              dispatch({
                type: 'getDataFailed' // this one sets loggingIn to false, loggedIn to false, loginFailed to true
              })
            })
        }
      }
    }
    

    If you do not wish to set this state in redux, you can also have this control exist within your component itself:

    class MyComponent extends React.Component {
      componentDidUpdate () {
        const { loggedIn, dispatch } = this.props;
        const { loggingIn, loginFailed } = this.state;
    
        if (!loggingIn && !loggedIn && !loginFailed) {
          this.setState({
            loggingIn: true
          })
          api.getData()
            .then(() => {
              this.setState({
                loggingIn: false,
                loginFailed: false
              });
              dispatch({
                type: 'gotData'
              })
            }, (reason) => {
              this.setState({
                loggingIn: false,
                loginFailed: true
              });
              dispatch({
                type: 'getDataFailed'
              })
            })
        }
      }
    }
    

    You could use the loggingIn flag to display a spinner to your user, or the loginFailed flag to display a message, if that fits your UX.