Search code examples
reactjsrecoiljs

Why is the assigned object not being written too?


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.


Solution

  • 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;
      });