1

I am working on an app in react that helps users new to a certain schema create queries interactively. The functionality I am currently working on is the following: anytime a relationship is deleted from the query, and fields that were accessed from that relationship should be removed as well. Assuming some users may run queries several "layers" deep, a delete on the top of the list will remove a significant amount of children, who may also have their own children. Therefore, I call the delete function recursively, checking for children, calling delete on their children first, etc etc until we return completely. The function works and hits all proper nodes (I can verify this through console logs), the issue I am having is that due to setState being asynchronous, the state only updates on the final call, and only the top node is ever actually filtered from the list. The code I am using is :

cascadeDeleteQueryFields(id) {
    //loop through the array checking for anyone with parent of id^
    this.state.queryFields.forEach((item) => {
      if (item.parent === id) {
        console.log("calling for item " + item.id);
        this.cascadeDeleteQueryFields(item.id);
      }
    });
    console.log("filtering ID : " + id);
    const queryFields = this.state.queryFields.filter((c) => c.id !== id);
    this.setState({ queryFields });
  }

(logs currently in just for debugging purposes)

Can anyone with a little more experience with react recommend a better way to update my state so as to catch every change in the recursive call? I have looked over other questions but none of them are in a recursive function like mine and so the solutions seem like they will not work properly or be horribly inefficient.

Devon F
  • 63
  • 1
  • 7
  • 1
    Can you write a recursive function `getDescendants` which collects the ids of the node, all its children, grandchildren, etc., then do something like `this.setState(this.states.filter(c => descendentIds.includes(c.id)))`? That is, run all the recursive calculations first, and only then do the `setState` call. – Scott Sauyet Jul 07 '20 at 16:07
  • 1
    There is a lot sad about state in react being immutable, that is to say you have to make deep copy of the object each time you set state, to prevent unexpected behavior. However if you choose to disregard you may consider following article:https://stackoverflow.com/questions/50837670/reactjs-setstate-previous-state-is-the-first-argument-props-as-the-second-argum/50837784#:~:text=If%20you%20are%20going%20to,state%2C%20like%20the%20example%20below You would still stack setState calls, but base each new in stack based on the result of previous iteration. – Dmitriy Godlevski Jul 07 '20 at 16:23
  • @ScottSauyet I was considering doing this, I wasn't aware that the javascript array prototype had an includes function, my fault for not checking the doc first (this is my first project with js). I see that being the best solution here (except given filters functionality shouldn't it be !descendentIds.includes(c.id)? ) – Devon F Jul 07 '20 at 16:57
  • 1
    Yes, of course. Typing code in these tiny boxes makes it easy to miss things! – Scott Sauyet Jul 07 '20 at 17:09

1 Answers1

1

This is the approach I suggested in the comments. descendants is a pure function which given a collection and an id returns that id and the ids of the (recursive) descendants of the element with that id, as denoted by parentId fields of some of the objects.

The deleteQueryFields method calls descendants once and then calls setState once with the result of filtering the nodes not included in the result.

const descendants = (xs) => (id) => [
  id, 
  ... xs .filter (({parentId}) => parentId == id) 
         .map (o => o .id) 
         .flatMap (descendants (xs))
]

class FakeReactComponent {
  constructor (state) {
    this .state = state
  }

  setState (newState) {
    console.log ('setState called') // should only happen once
    this .state = Object .assign ({}, this .state, newState)
  }

  deleteQueryFields (id) {
    const toRemove = descendants (this .state .queryFields) (id)
    this.setState ({
      queryFields: this .state .queryFields .filter (({id}) => !toRemove .includes (id))
    })
  }
}

const component = new FakeReactComponent ({
  queryFields: [{id: 1}, {id: 2}, {id: 3, parentId: 2}, {id: 4, parentId: 2}, 
                {id: 5, parentId: 1}, {id: 6, parentId: 5}, {id: 7, parentId: 6}, 
                {id: 8}, {id: 9, parentId: 4}, {id: 10, parentId: 8}]
})

component .deleteQueryFields (2)

console.log (component.state)
.as-console-wrapper {min-height: 100% !important; top: 0}

You should see that setState is only called once, but the element with id 2 has been removed along with all its descendants.


You say you're new to JS. If any of that syntax is confusing, feel free to ask about it.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103