Search code examples
reactjsfetchsetstate

Why are two network calls being made, when fetch in setState?


When I use fetch in setState the function makes two network requests, but I expect one request.

Why is this happening and how to prevent it?

import React from 'react';

class TestFetch extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {

    this.setState(() => {

      fetch('http://example.com/', {
        mode: 'no-cors'
      })
        .then(data => {
          console.log(data)
        });
      });
  }

  render() {
    return (
      <button onClick={this.handleClick}> Test </button>
    )
  }
}

export default TestFetch

Another version with setState in the fetch. Now I have one network call, but two values in my state after one click:

import React from 'react';

class TestFetch extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      'newItems': []
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {

    fetch('http://example.com/', {
      mode: 'no-cors'
    })
      .then(data => {

        this.setState((state) => {
          state.newItems.push("value")
        })

        console.log(this.state)
      });
  }

  render() {
    return (
      <button onClick={this.handleClick}> Test </button>
    )
  }
}

export default TestFetch

Ok, basically it has this effect in this example as well:

import React from 'react';

class TestFetch extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      'newItems': []
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(state => {
      state.newItems.push("value")
    })
    console.log(this.state);
  }

  render() {
    return (
      <button onClick={this.handleClick}> Test </button>
    )
  }
}

export default TestFetch

Solution

  • Why is this happening...

    My guess would be you are rendering your app into a React.StrictMode component. See Detecting unintentional side-effects

    Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

    • Class component constructor, render, and shouldComponentUpdate methods
    • Class component static getDerivedStateFromProps method
    • Function component bodies
    • State updater functions (the first argument to setState)
    • Functions passed to useState, useMemo, or useReducer

    In other words, the setState is called twice by React to help you find unintentional side-effects, like the double fetching.

    ...and how to prevent it?

    Just don't do side-effects in the setState callback function. You likely meant to do the fetch and in the Promise chain update state.

    handleClick() {
      fetch('http://example.com/', {
        mode: 'no-cors'
      })
        .then(data => {
          console.log(data);
          this.setState( ......); // <-- update state from response data
        });
    }
    

    Update

    Another version with setState in the fetch. Now I have one network call, but two values in my state after one click:

    In your updated code you are mutating the state object. Array.prototype.push updates the array by adding the new element to the end of the array and returns the new length of the array.

    Array.prototype.push

    this.setState(state => {
      state.newItems.push("value") // <-- mutates the state object
    })
    

    I believe you see 2 new items added for the same reason as above. When updating arrays in state you need to return a new array reference.

    You can use Array.prototype.concat to add the new value and return a new array:

    this.setState(prevState => {
      newItems: prevState.newItems.concat("value"),
    });
    

    Another common pattern is to shallow copy the previous state array into a new array and append the new value:

    this.setState(prevState => {
      newItems: [...prevState.newItems, "value"],
    });
    

    Additionally, once you sort out your state updates, the console log of the state won't work because React state updates are asynchronously processed. Log the updated state from the componentDidUpdate lifecycle method.

    componentDidUpdate(prevProps, prevState) {
      if (prevState !== this.state) {
        console.log(this.state);
      }
    }