Search code examples
javascriptreactjsrace-conditiondeep-copy

React switching lists race condition?


I am trying to implement a queue wherein the user can switch items from one list to another. i.e. from "available" to "with client", where the state of the queue is held in the root React component like so:

this.state = {
  queue: {
    available: [{ name: "example", id: 1 }, ...],
    withClient: [],
    unavailable: []   
  }
};

However my move function is broken:

move(id, from, to) {
  let newQueue = deepCopy(this.state.queue);
  console.log("NEW QUEUE FROM MOVE", newQueue); // { [from]: [], [to]: [undefined] }
  console.log("QUEUE IN STATE FROM MOVE", this.state.queue); // { [from]: [{...}], [to]: [] }
  let temp = newQueue[from].find(x => x.id === id);
  newQueue[from] = this.state.queue[from].filter(x =>
    x.id !== id
  );
  newQueue[to] = this.state.queue[to].concat(temp);
  this.setState({
    queue: newQueue
  });
}

I am expecting the 2 console.logs to be the same. There seems to be some sort of race condition happening here that I am not understanding. It results in getting an error Cannot read property 'id' of undefined

Right now the only way the movement is triggered from a HelpedButton component contained within each item in the "Available" list which gets passed props:

class HelpedButton extends React.Component {
  constructor() {
    super();
    this.clickWrapper = this.clickWrapper.bind(this);
  }

  clickWrapper() {
    console.log("I have id right?", this.props.id); //outputs as expected
    this.props.move(this.props.id, "available", "withClient");
  }

  render() {
    return (
      <span style={this.props.style}>
        <button onClick={this.clickWrapper}>
          <strong>Helped A Customer</strong>
        </button>
      </span>
    );
  }
}

export default HelpedButton;

I don't think there's anything wrong with the deepCopy function, but here it is, imported file from node_modules:

"use strict";

function deepCopy(obj) {
  return JSON.parse(JSON.stringify(obj));
}

module.exports = deepCopy;

Solution

  • The recommended way by react documentation to make setState which depends on previous state is to use the updater form which looks like this setState((prevState,prevProp)=>{}) . With this method your move function will look like this.

    move(id, from, to) {
    
      let temp = newQueue[from].find(x => x === x.id);
      newQueue[from] = this.state.queue[from].filter(x =>
        x.id !== id
      );
      newQueue[to] = this.state.queue[to].concat(temp);
      this.setState((prevState)=>{
        queue: {
          ...prevState.queue,
          [from]: prevState.queue[from](o => x.id !== id),
          [to]: [from]: prevState.queue[from].concat(prevState.queue[from].find(x => x === x.id))
        }
      });
    }
    

    I believe the reason why you see different outputs is console.log outputs a live object which means if you run console.log(obj) and later change obj param the displayed params are changes. Try to debug with console.log("obj: " + JSON.strignify(obj)).
    Here is more about why you should call the async method of setState when relying on previous state in react docs