Reputation: 7599
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
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