Categories
Hard Skills

Infinite API Call Problem Using React, Redux, and Thunk.

Why does the app send the same API request over and over again? It should do it only once. You use Redux, so as soon as it receives the data, it should be all done?

There are cases when your application won’t behave the way you would expect. That is normal, but in that ticket, something is off. Why does the app send the same API request over and over again? It should do it only once. You use Redux, so as soon as it receives the data, it should be all done?

You also made sure that your React component asks for data in the componentDidMount lifecycle method. Because that function should be called only once in a component’s lifecycle, the repeated call should be impossible. You have no idea what is going on?

There is always an answer, and no, React is behaving as it should. The problem is somewhere else. Let’s find out what is the real issue here.

Jira Ticket
WUCOVID-19-T-01
(Written Under Corona Virus 2019, Ticket 01)

Symptoms

  • The app always triggers the same API repeatedly, behaving like an infinite loop.
  • The Spinner component reloads and keeps going on forever.
  • The app is unusable.

Triggering Behaviour:

  • The API returns a truthy false value. In that case an empty array: “[]”

Main Components

  • API to get Data to store in Redux and any middleware to handle async calls, in my case, Thunk.
  • Parent Component that can render 2 child components, one at a time:
    • Child Component
    • Spinner Component

Note: You can find the commented version of these code-snippets at the end of this article.

function getSomeOptions(maxAmount) {
  return function(dispatch) {
    return fetchSomeOptions(maxAmount).then(
      (newOptions) => dispatch({type: "[FETCH_SOMEARRAY] SUCCESS", newOptions}),
      (error) => dispatch({type: "[FETCH_SOMEARRAY] ERROR", newOptions: [], error})
    );
  }
}
class Parent extends React.Component {
  render () {
    return this.props.isLoading ? <Spinner /> : <ConnectedChild />
  }
}

function mapParentStateToProps(state) {
  return {
    isLoading: state.isLoading
  }
}

export const ConnectedParent = connect(mapParentStateToProps, Parent);
class Child extends React.Component {

  componentDidMount() {
    this.props.getSomeOptions(3);
  }

  render() {
    return (
      <ul>
        {this.props.options.map(item => <li>{item}</li>)}
      </ul>
    )
  }
}

function mapChildStateToProps(state) {
  return {
    options: state.options
  }
}

mapChildDispatchToProps = {
  getSomeOptions
}

export const ConnectedChild = connect(mapChildStateToProps, mapChildDispatchToProps, Child);

Breaking down the steps

…by (Pseudo) steps

  • 1. The app starts, and mounts the Parent component.
  • 2. The Parent component gets the isLoading value from the Redux Store.
    • 2.a. When there is an API call in progress, the isLoading value will be true.
  • 3. The Parent component render function returns two different component based on the isLoading value:
    • 3.a. If isLoading is true, the Parent component will render the Spinner component.
      • 3.a.1. It will display a spinner gif.
      • 3.a.2. React will dismount the Child component
    • 3.b. If it is false, it will render the Child component.
      • 3.b.1. React will then mount the Child component and will call the component’s componentDidMount function.
      • 3.b.2. The componentDidMount will call the getSomeOptions function
      • 3.b.3. The getSomeOptions function will set the isLoading value to true.
      • 3.b.4. Redux will update all the components to let them know, the isLoading value changed
      • 3.b.5. Step 3.a. will activate (Parent renders the Spinner component and dismounts this Child component)

… explained

When you get data from the Redux store that does not satisfy one of the child components, the child component goes ahead and triggers another call to the Redux store (in componentDidMount) hoping for a better result. When a Redux call is in progress though, the Parent displays the Spinner component instead of the child component, meaning that it destroys the Child component that triggered a Redux call itself in componentDidMount.

When the API returns, Redux updates the Parent component with the new data, then the Parent displays the Child component again. But, probably your API returned a data again that does not satisfy the child component, so the child goes ahead and triggers the API call from componentDidMount that triggers the whole cycle again and keeps repeating itself indefinitely.

React will not complain, since regarding JavaScript everything behaves as expected, there is not a JS infinite-loop, nor calls components in a limited number depth. It just kicks a process in again and again.

Whereas a user only notice there is a problem, seeing that the Spinner starts over and over again, with a slight flickering, and as a developer you can see in Chromes network tab, that the same API is getting called over and over again, that always returns the same data.

Separation of concerns

Mixed components

 When your component handles both the business logic and the view at the same time. It calls Thunk actions that trigger API calls and that updates the Redux Store. It also handles the components inner state for component-specific behavior.

Smart and Dumb components

Or you could call them Container and Presentational components.

It is when you separate the logic from the view. Instead of having one big component that handles API calls, state, redux updates and logically changes what to display, we make a series of components.

One (smart/container component) for handling the logic, owns a state and can manage redux store. And one or more (dumb/presentational) view components, that are only responsible for displaying data received through props and calling functions that were passed down from the parent component.

Solution(s)

If you face a similar problem I would advise to use “Smart and Dumb” option, meaning, separate the logic from the representation and that way you are avoiding scenarios like these. Do not cause a state update circumventing the parents job, if it can destroy the child component that needs the data and tries to get it itself.

If you use the Mixed Components option make sure, none of the parent components are trying to replace one component with another. If you need to hide something, pass down a prop asking the child component to do it itself. That way your component can stay alive during the apps lifecycle and all your lifecycle methods will work as expected. The child componentDidMount will be called once, and you can listen to changes in componentDidUpdate as you would expect.

One more thing

When you experience this problem you most likely will try to log out the change in componentDidUpdate, because you have this.props.yourValue and prevProps.yourValue, that can tell you a lot about how data is traveling through your component.

Let’s suppose it is an integer for the sake of this example. When you have a value of 50 and you change it to 200, it is expected that the prevProps.yourValue will contain the value 50 and the this.props.yourValue will contain the new value, 200. You write an if to compare equality. The strange thing will happen if you console.log out the 2 values. The prevProps.yourValue will not have the value 50 in it, it will have the new 200 value as well.

In short, both the previous and current values will contain the new value, so you will not be able to catch the state change in an if statement as you would normally do.

And you might think that there is a problem within React, since by the documentation it would not be possible.

But if you ask yourself, what is the lifecycle method that can run and steal the state when prevProps.yourValue owns the value 50, before componentDidUpdate, you can realize, it is the componentDidMount lifecycle method only.

If you try to log out the this.props.yourValue in componentDidMount as well, when you change that value, you will see, not one, but 2 logs. One for the componentDidMount, with the value 50, and a componentDidUpdate, with two 200 values, one for the previous and one for the current props.

This means, your parent component kills and recreates the child component due to redux store updates for each data change. It is a big problem because your child component that has to work with the data, cannot depend on the lifecycle methods, its inner state, since it gets destroyed all the time.

It is also a big performance hit for your application.

Here is the promised commented code

with some tips:

/**
 * Imagine that in our scenario, the empty array means, "we need some data, fetch it now"
 * But, to open the door for the undesired behaviour, when an error occurs we reset it to default empty array.
 * In that way we introduced enough possibilities for confusion, that can cause a problem described in that article.
 */
function getSomeOptions(maxAmount) {

  // Thunk function to save data in Redux Store.
  return function(dispatch) {
    // get data (the options that our child component will need) from server
    // here we have 2 cases for error:
    // 1. We get a success response with an empty array (with no options to use later)
    // 2. We get an error, we reset the options to the default empty array.
    return fetchSomeOptions(maxAmount).then(
      (newOptions) => dispatch({type: "[FETCH_SOMEARRAY] SUCCESS", newOptions}),
      (error) => dispatch({type: "[FETCH_SOMEARRAY] ERROR", newOptions: [], error})
    );
  }
}
/**
 * The Parent Component manages business logic, as well as handling the loading state.
 */
class Parent extends React.Component {

  /* ... your business logic comes here ... */

  // imagine that isLoading is set to true whenever an API call started and set to false when it returned with a response.
  render () {
    return this.props.isLoading ? <Spinner /> : <ConnectedChild />
  }
}

// map the isLoading property from the Redux Store
function mapParentStateToProps(state) {
  return {
    isLoading: state.isLoading
  }
}

// connect to Redux
export const ConnectedParent = connect(mapParentStateToProps, Parent);
/**
 * The Child Component has its own data management.
 * When it is mounted (it happens once in a component's lifecycle) it 
 */
class Child extends React.Component {

  componentDidMount() {
    this.props.getSomeOptions(3);
  }

  render() {
    // in this example it doesn't matter what we try to display
    // but we could further complicate the situation
    // if it would be a form that listens to the user input
    // and it would communicate with a server through API calls.
    return (
      <ul>
        {this.props.options.map(item => <li>{item}</li>)}
      </ul>
    )
  }
}

// map the options property from the Redux Store
function mapChildStateToProps(state) {
  return {
    options: state.options
  }
}

// map the Redux getSomeOptions action creator function to getSomeOptions property
mapChildDispatchToProps = {
  getSomeOptions
}

// connect to Redux
export const ConnectedChild = connect(mapChildStateToProps, mapChildDispatchToProps, Child);

By Botond Bertalan

I am the founder of www.botond.dev
I started my programming studies in 2010 and started to work professionally in 2012 before graduation.
I love programming and architecting code that solves real business problems and gives value for the end-user.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.