7hibault
7hibault

Reputation: 2459

How can I wait for or cancel a data update in componentDidUpdate when a new update was triggered?

My issue

I'm using componentDidUpdate to fetch data and update the component state if props have changed. This is what the React docs suggest:

This is also a good place to do network requests as long as you compare the current props to previous props

However, if I trigger a network request with an update that happens to get a faster response than one triggered by a previous update, the state will be updated with the new request and then overwritten with the older request. Which is not what I want: I want the state to be updated with the data from the request triggered by the latest update.

How can I wait for or cancel a data update in componentDidUpdate when a new update was triggered?

Reproducing the issue

To make it more clear, I've written a small example to simulate the situation, you can try it out on code sandbox. There are two buttons, one changes the state immediately and the other one after 2 seconds.

Try the following behavior

  1. Click Fetch B (nothing happens)
  2. Quickly click Fetch A(data from A appears)
  3. Wait
  4. (data from B appears)

A user however would expect Data from A to remain, as it is the last button that was clicked.

The example code that was uploaded on CodeSandbox, in case the link breaks

import React, { Component } from "react";
import ReactDOM from "react-dom";

class Display extends Component {
  constructor(props) {
    super(props);
    this.state = {
      show: ""
    };
  }
  componentDidUpdate(prevProps) {
    if (this.props.show !== prevProps.show) {
      const timeout = this.props.show === "A" ? 0 : 2000;
      const show = this.props.show === "A" ? "Data from A" : "Data from B";
      setTimeout(() => {
        this.setState({ show });
      }, timeout);
    }
  }

  render() {
    return <div>{this.state.show}</div>;
  }
}

class ChangeInput extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: ""
    };
  }
  handleEventA = () => {
    this.setState({ selected: "A" });
  };
  handleEventB = () => {
    this.setState({ selected: "B" });
  };

  render() {
    return (
      <div>
        <button type="button" onClick={this.handleEventA}>
          Fetch A (fast)
        </button>
        <button type="button" onClick={this.handleEventB}>
          Fetch B (slow)
        </button>
        <Display show={this.state.selected} />
      </div>
    );
  }
}

export default ChangeInput;

const rootElement = document.getElementById("root");
ReactDOM.render(<ChangeInput />, rootElement);

A diagram to illustrate the issue

I've created a diagram that shows what I suspect is happening

enter image description here


Upvotes: 1

Views: 409

Answers (1)

Mohamed Ramrami
Mohamed Ramrami

Reputation: 12711

You can use an instance variable to save a request ID, and before updating the state check if the request ID is last one. It's more clear with code :

  constructor(props) {
    super(props);
    this.state = {
      show: ""
    };

    this.lastRequestId = null;
  }

  componentDidUpdate(prevProps) {
    if (this.props.show !== prevProps.show) {
      const timeout = this.props.show === "A" ? 0 : 2000;
      const show = this.props.show === "A" ? "Data from A" : "Data from B";

      const requestId = `REQUEST-${Date.now()}`;
      this.lastRequestId = requestId;

      setTimeout(() => {
        if (this.lastRequestId !== requestId) {
          console.log('Request canceled ...');
          return;
        }
        this.setState({ show });
      }, timeout);
    }
  }

Complete code : https://codesandbox.io/s/componentdidupdate-conflict-t4rfj

Upvotes: 1

Related Questions