lowcrawler
lowcrawler

Reputation: 7599

Waiting for setState to finish

I see from a million other questions that there is a callback that can be used after setState is called. In fact, I use it in my code example here in my questionChangeSystemCallback function.

I'm unsure how to take advantage of this in my situation. Here is my code (stripped for simplicity sake)

The main flow works like this: A question changes, it calls it's callback, questionChangeSystemCallback. From there it updates it's value in state. When done updating it's value, it checks for additional things to do and calls actionExecuter as needed.

//note, the part that matters is effectively the forEach loop at the bottom that's within the setState questionsData callback.

questionChangeSystemCallback(Q) {
    // updates current state of questionsData, checks for action string, executes any actions

    if (Q == null) {
        console.log("questionChangeSystemCallback required field, question, is null");
    }

    let updatedQuestionData = this.getQuestionDataWithUpdatedValue(Q);

    this.setState({ questionsData: updatedQuestionData }, () => {
    // after state is set, check if there are additional actions needed based on the actionOptions

        if (Q.props.actions) {
            let { actions } = Q.props;
            let qval = Q.state.value;
            let commandString = actions[qval];
            if (commandString) {
                let actionsToDo = commandString.split('&');
                actionsToDo.forEach((action) => {
                    this.actionExecuter(action);
                });
            }
        }
    });
}

actionExecuter does this... basically just a switch statement to call showTab with a true or fall element:

actionExecuter = (actionString) => {
    let splitActionString = actionString.split('::');
    if (splitActionString.length !== 2) {
        //TODO: Throw error
    }
    switch (splitActionString[0]) {
        case "ShowTab":
            this.showTab(splitActionString[1], true);
            break;
        case "HideTab":
            this.showTab(splitActionString[1], false);
            break;
        default:
            console.log("Requested action '" + splitActionString[0] + "' not recognized");
    }
}

showTab looks like this, effectively adding tabName to this.state.hiddenTabs if toShow is true and removing tabName from this.state.hiddenTabs if it's false... and then setting the state.hiddenTabs to the new array.

showTab(tabName, toShow) {

    let newHiddenTabs = this.state.hiddenTabs.slice();
    console.log("After copy: ", newHiddenTabs); 
    let cleanTabName = tabName.replace(/ /g, '');
    if (toShow) {
        // remove all instances from newHiddenTabs
        while (newHiddenTabs.includes(cleanTabName)) {
            let index = newHiddenTabs.indexOf(cleanTabName);
            if (index > -1) {
                newHiddenTabs.splice(index, 1);
            }
        }
        console.log("After removal: ", newHiddenTabs);
    } else {
        // add tabName to newHiddenTabs
        newHiddenTabs.push(cleanTabName);
        console.log("After addition: ", newHiddenTabs);

    }
    console.log("Before setting state: ", newHiddenTabs);
    this.setState({ hiddenTabs: newHiddenTabs }, ()=> {
        console.log("STATE after setting state: ", this.state.hiddenTabs);
    }
    );
}

Using that host of console logs, I'm learning 1) the logic here works and 2) that if I have more than one 'action', and thus showTab gets called twice... only the data from the SECOND call ends up in state. Further, the render method does not get called afterward.

As an example:

initial this.state.hiddenTabs = ["WaterQuality","FieldForm","EWI","EDI"] I have added a console.log("RENDER") to the top of my render function. I run, as actions, a ShowTab(EDI, true) and a ShowTab(EWI, false).

The following is the output:

After copy:  (4) ["WaterQuality", "FieldForm", "EWI", "EDI"]   
**(correct)**

After removal:  (3) ["WaterQuality", "FieldForm", "EWI"]   
**(correct)**

Before setting state:  (3) ["WaterQuality", "FieldForm", "EWI"]   
**(correct)**

After copy:  (4) ["WaterQuality", "FieldForm", "EWI", "EDI"]  
**(nope - same initial state as first time through)**

After addition:  (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"]  
**(given it's input, correct, but overall wrong)**

Before setting state:  (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"] 
**(given it's input, correct, but overall wrong)**

RENDER  
**(why are we rendering now... and why only once)**

STATE after setting state:  (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"]  
**(this is the (erroneous) value from the second time through)**

STATE after setting state:  (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"] 
**(this is the (erroneous) value from the second time through)**

Upvotes: 1

Views: 5350

Answers (1)

Alexandre Wiechers Vaz
Alexandre Wiechers Vaz

Reputation: 1617

Your setState calls are getting batched. Depending on where you're calling setState, React will batch them automatically and only perform a render, once the whole batch is finished.

Problem in your case is probably here: let newHiddenTabs = this.state.hiddenTabs.slice();

When you have multiple actions, this function gets called multiple times and react is batching setState. Since the updates where not flushed yet, when it performs this action again, the state isn't updated yet!

My suggestion: Extract this to another function and use the other setState signature, which takes a function with prevState and props as argument. It'd look somewhat like this:

showTab(tabName, toShow) {
  const processTabs = (hiddenTabs) => {
      let cleanTabName = tabName.replace(/ /g, '');
      if (toShow) {
        hiddenTabs = hiddenTabs.filter((tab) => tab !== cleanTabName)
      } else {
        hiddenTabs.push(cleanTabName)
      }
      return hiddenTabs;
  }

  this.setState((prevState, props) => ({ hiddenTabs: processTabs([...prevState.hiddenTabs])}), () => {
    console.log("STATE after setting state: ", this.state.hiddenTabs);
  })
}

Edit: Sorry, accidentally sent the answer incomplete before D:

Upvotes: 5

Related Questions