0

I have a recoil state that is an object that is structured like this:

const Proposal = atom({
  key: "PROPOSAL",
  scopes: [{
    assemblies: [{
      items: [{}]
    }]
  }]
})

When theres updates to an item in the UI I am updating the atom by mapping through scopes, assemblies and items. When I find the correct item I am updating I am logging the current item, then logging the updated item. These logs are correct so I can see the value being updated. But when I get to the third log it does not show the updated value.

const [proposal, setProposal] = useRecoilState(Proposal)

const applyUpdatesToProposalObj = useCallback(_.debounce(params => {setProposal(proposal => {
  setProposal(proposal => {
    let mutable = Object.assign({}, proposal);

    for(let i = 0; i < mutable.scopes.length; i++) {
      let scope = mutable.scopes[i]

      for(let j = 0; j < scope.assemblies.length; j++) {
        let assembly = scope.assemblies[j]

        for(let k = 0; k < assembly.items.length; k++) {
          let item = assembly.items[k]

          if(item._id === id) {
            console.log('1', item)
            item = {
              ...item,
              ...params
            }
            console.log('2', item)
          }
        }
      }
    }

    console.log('3', mutable)

    return mutable
 })
}, 350), [])

The output of the logs look like this:

1 { ...item data, taxable: false }
2 { ...item data, taxable: true }
3 { scopes: [{ assemblies: [{ items: [{ ...item data, taxable: false }] }] }] } 

I setup a sandbox where you can view the behavior https://codesandbox.io/s/young-dew-w0ick?file=/src/App.js

Another odd thing is I have an object inside the proposal called changes. I setup an atom for the changes object and update it in a similar fashion and that is working as expected.

Ryne
  • 1,195
  • 2
  • 14
  • 32
  • `item = { ... }` creates a new item, but its never placed back into the nested object/array. You may want to have that looked at. – Caramiriel Jan 22 '22 at 16:37
  • @Caramiriel I had a similar map but instead of setting new variables i mapped throguh like for(let k = 0; k < mutable.scopes[i].assemblies[j].items.length; k++) mutable.scopes[i].assemblies[j].items[k] = { ...mutable.scopes[i].assemblies[j].items[k], ...params } and I was getting an error saying cannot write to object. Weird since I assigned the proposal to a new object write – Ryne Jan 22 '22 at 16:40
  • Hmm, could you see if it helps cloning the object you mutate along the way? `Object.assign({}, proposal)` only clones the object at the proposal level, but there's no deep copy going on, so underlying arrays/object are visible in both objects even though the intent is to only make it visible in `mutable`. – Caramiriel Jan 22 '22 at 16:43
  • I had tried doing that to each object like let scope = Object.assign({}, mutable.scopes[i], let assembly = Object.assign({}, scope.assemblies[j]), let item = Object.assign({}, assembly.items[k]) and i stopped getting the write to object error, but got the same behavior as posted in my question – Ryne Jan 22 '22 at 16:47
  • May we see what happens if you change: `item = { ...item, ...params }` to `assembly.items[k] = {...item, ...params}`? Any improvement/s - or same behaviour? – jsN00b Jan 22 '22 at 17:00

1 Answers1

1

Extending on the little conversation we had in the comments: you'd have to clone the entire tree of objects you want to mutate, cloning every level along the way. This is quite tedious to do in plain javascript, so at the cost of a little performance (this example is updating all arrays along the way), this can be optimized to somewhat more readable:

setProposal((proposal) => {
    let mutable = proposal;
    console.log("before", mutable);

    mutable = {
      ...mutable,
      scopes: mutable.scopes.map((scope) => ({
        ...scope,
        assemblies: scope.assemblies.map((assembly) => ({
          ...assembly,
          items: assembly.items.map((item) => {
            if (item.id === id) {
              return {
                ...item,
                ...params
              };
            } else {
              return item;
            }
          })
        }))
      }))
    };

    console.log("after", mutable);

    return mutable;
  });
Caramiriel
  • 7,029
  • 3
  • 30
  • 50