1

So I’ve spent dozens of hours wondering why this is going on at this point, and I’ve looked into a few different solutions.

I’m pretty sure it doesn’t have to do with any lifecycle methods or asynchronous issues of React’s setState function, though I've investigated thoroughly.

I’m pretty sure it doesn’t have to do with value/reference issues of tempState actually mutating state, because I’m using lodash to deepClone the state object, though this was a previous issue.

I suspect it has to do with setState’s shallow merge, but I cannot for the life of me figure out why.

Essentially, I thought that this code should only change the typeAheadOptions array to change the hostshow values to Various Shows, but it is mutating the state as well, and not even in the same way. The state isn't even the same shape as the the typeAheadOptions-- it is deeper with a couple other layers in it.

At this point, I might rebuild this with Redux and ImmutableJS, but now I really need to understand why this is happening, because it is making me slightly crazy. And now I've read suggestions to keep your state objects as flat as possible, and I think I understand why now.

Here’s the snipped state with just the relevant bits:

this.state = {
  showByName: false,
  showByShow: true,
  resources: [
    {
      show: "TED Radio Hour",
      showurl: TEDRadioHour,
      hosts: [
        {
          firstName: "Guy",
          lastName: "Raz",
          personurl: GuyRaz,
          hostshow: "TED Radio Hour"
        }
      ]
    },
    {
      show: "Radiolab",
      showurl: Radiolab,
      hosts: [
        {
          firstName: "Jad",
          lastName: "Abumrad",
          personurl: JadAbumrad,
          hostshow: "Radiolab"
        },
        {
          firstName: "Robert",
          lastName: "Krulwich",
          personurl: RobertKrulwich,
          hostshow: "Radiolab"
        }
      ]
    },
    {
      show: "How I Built This",
      showurl: HowIBuiltThis,
      hosts: [
        {
          firstName: "Guy",
          lastName: "Raz",
          personurl: GuyRaz,
          hostshow: "How I Built This"
        }
      ]
    },
    {
      show: "Radiolab Presents: More Perfect",
      showurl: RadiolabPresentsMorePerfect,
      hosts: [
        {
          firstName: "Jad",
          lastName: "Abumrad",
          personurl: JadAbumrad,
          hostshow: "Radiolab Presents: More Perfect"
        }
      ]
    }
  ],
  contentToRender: [],
  typeAheadOptions: [],
  selected: [],
  duplicateHostIndices: []
}
}

And here are the functions:

showByName() {
 const tempState = _.cloneDeep(this.state);
 tempState.showByName = true;
 tempState.showByShow = false;
 tempState.selected.length = 0;
 this.alphabetizeHostList(tempState);
}

alphabetizeHostList(tempState) { // sorting state so that results are alphabetical
 tempState.typeAheadOptions.length = 0;  // clear typeAhead options
 tempState.resources.forEach((resource, index) => {
   resource.hosts.forEach((host, index) => {
     tempState.typeAheadOptions.push(host);
     tempState.typeAheadOptions.sort(function(a, b) {  // sorting function
      var nameA = a.firstName.toUpperCase(); // ignore upper and lowercase
      var nameB = b.firstName.toUpperCase(); // ignore upper and lowercase
      if (nameA < nameB) {
        return -1;
      }
      if (nameA > nameB) {
        return 1;
      }
      // names must be equal
      return 0;
     });
   })
 })
 this.populateDuplicateHostIndices(tempState);
}

populateDuplicateHostIndices(tempState) { // removes duplicates by first and last name for instances like Guy Raz and Jad Abumrad
 let duplicateHostIndices = tempState.duplicateHostIndices;
 duplicateHostIndices.length = 0;
 let options = tempState.typeAheadOptions;
 let length = options.length;
 let i = 1;
 if (length > 1 && tempState.showByName === true) {
  for (i; i < length; i++) {  // iterates over the hosts and finds duplicates by first and last name
     if ((options[i - 1].firstName === options[i].firstName) && (options[i - 1].lastName === options[i].lastName)) {
      duplicateHostIndices.push(i);
     }
   }
 }
 this.removeDuplicateHosts(tempState, duplicateHostIndices);
}

 removeDuplicateHosts(tempState, duplicateHostIndices) {
  if (duplicateHostIndices.length > 0) {
   duplicateHostIndices.sort().reverse();  // if we didn't sort and reverse, we would remove the 1st host and the index of the rest would be off and we would remove them
   duplicateHostIndices.forEach((element) => {
    const previousElement = (element - 1);
    tempState.typeAheadOptions[(previousElement)].hostshow = "Various Shows";
    tempState.typeAheadOptions.splice(element, 1);
   });
 }
 this.pullContentToRenderFromTypeAheadList(tempState);
}

pullContentToRenderFromTypeAheadList(tempState) {
 tempState.contentToRender = _.cloneDeep(tempState.typeAheadOptions); // separates out content that renders from list that TypeAhead pulls from
 this.setState(tempState);
}
TomRod
  • 311
  • 2
  • 5
  • You are correct; This should answer your question: https://stackoverflow.com/questions/43040721/how-to-update-nested-state-properties-in-react/43041334 Let me know if you still have any questions, happy to help answer – Magnum Nov 28 '18 at 19:44

1 Answers1

0

Someone on Reactiflux kindly pointed out that by iterating over my state object with forEach() and running push() to form a new tempState, I was in fact mutating my original state directly, not a new tempState like I had previously assumed.

It was not a setState shallow merge issue, but a value/reference issue like I previously thought I had solved.

I fixed this by only calling setState with partial state, so there was never an opportunity for my original source of truth to be overwritten.

Now to do some refactoring around an overly-nested state object and refactoring this code to be more concise.

TomRod
  • 311
  • 2
  • 5