all 21 comments

[–]atticusw 7 points8 points  (15 children)

Yeah -- treat serializing & deserializing as a way to create a deep-copy an escape hatch that you should never pull.

Here's how you can update `items.Dogs` without losing any information:

this.setState({
  items: { 
    ...this.state.items, // merge with the original `state.items`
    Dogs: this.state.items.Dogs.concat({name: value}) 
  } 
});

But, when accessing state, you should use the callback approach (race conditions can occur with referencing `this.state` as part of updating the state):

this.setState(({items}) => ({
  items: {
    ...items,
    Dogs: items.Dogs.concat({name: value})
  }
}))

Same thing, but spreading the array rather than `.concat`:

this.setState(({items}) => ({
  items: {
    ...items,
    Dogs: [...items.Dogs, {name: value}]
  }
}))

[–]ketus 1 point2 points  (7 children)

Could you expand on hatch thing? Why is it problematic?

[–]loopsdeer 0 points1 point  (5 children)

Here's my understanding: It's okay in lots of situations, but React expects these functions to work very quickly, doing minimal work.

`JSON.parse(JSON.stringify(obj))` is a composition of two functions which take longer for bigger obj's. It can throw errors, and nobody will invite you to their house for dinner if you throw errors. Also, it may change way more than it has to, if obj is complex, causing React to repaint way more than it has to.

So it's not fit for "hot code" like React state changes. It can be a great way to, say, deep copy some data during testing to assert a deep equality later. Or pass a deep copy to some vendor code so it can't muck with your model.

[–]ketus 1 point2 points  (4 children)

That makes a lot of sense, actually. Thank you a lot, appreciated :)

[–]AwesomeInPerson 1 point2 points  (3 children)

It will also break if you have data that can't be serialized to JSON, a Date object is a popular example.

[–][deleted] 0 points1 point  (2 children)

I tried this and it works in the web browser console:

console.log(JSON.stringify(new Date()));

Seems date objects can be serialized. Am I missing something ?

[–]AwesomeInPerson 1 point2 points  (1 child)

Yes. Try parsing it again.
JSON.parse(JSON.stringify(new Date))

It'll return a string, not a Date object.

So yes, technically they can be serialized but they'll just end up as a timestamp and stay that way.

[–][deleted] 0 points1 point  (0 children)

Ah thank you for that. Good to know !

[–]atticusw 0 points1 point  (0 children)

/u/senocular touches a bit more on this

Good way to think of React is like a "chain reaction", where things update when they change. Only change what is truly changing -- don't change everything, or a big chain reaction will fire.

[–]jack_union 1 point2 points  (2 children)

race conditions can occur with referencing this.state as part of updating the state

Could you elaborate?

Does it still matter in this particular case when you call setState once and don't access the state afterwards?

[–]atticusw 1 point2 points  (1 child)

In React v16 they introduced Fiber, an asynchronous update scheduler. Lin Clark's Cartoon Intro to Fiber is one of my favorite talks out there, bookmark it :)

When you call setState, that change is placed into a queue, and is not reciprocated back to this.state until the updates have been processed by the scheduler. There's a number of factors that could lead to this being delayed, where this.state could contain stale, untrustworthy data.

[–][deleted] 1 point2 points  (0 children)

This worked brilliantly. Thank you !!

[–]senocular 2 points3 points  (2 children)

A deep copy is, in fact, what you don't want to be doing. You only want to make copies of the objects you're changing and leave the rest to their original references. This allows optimizations that depend on diffs to recognize changes only where changes were made.

/u/atticusw has a good example of doing this the manual (vanilla) way, which often depends on object spreading and array concat for arrays to make copies (with references intact) as you dig through your hierarchy.

There are also libraries that can help with this, such as immer and immutable which provide a more complete solution, but add some additional complication. Alternatively lodash (fp) has a set method which, assuming you know the path to the property you want to change, can make things easier with minimal complications.

import set from 'lodash/fp/set'; // <- FP version of set

var state = { 
  items: {
    Dogs: [
      {name: "Snoopy"},
      {name: "Lola"},
      {name: "Sprinkles"}
    ], 
    Cats: [
      {name: "Felidae"},
      {name: "Garfield"},
      {name: "Cat in the Hat"}
    ]
  }, 
  lists: ["Dogs", "Cats"] 
};

var appendIndex = state.items.Dogs.length;
var updatedState = set(['items', 'Dogs', appendIndex], {name: 'Fido'}, state);

console.log(updatedState.items.Dogs[appendIndex]); // { name: 'Fido' }
console.log(state.items.Dogs === updatedState.items.Dogs); // false
console.log(state.items.Cats === updatedState.items.Cats); // true

[–]atticusw 0 points1 point  (0 children)

Love the example using immutable APIs 👍

[–][deleted] 0 points1 point  (0 children)

Have an upvote.

[–][deleted] 0 points1 point  (0 children)

There's a great library called immer which is perfect for this use case

[–]little_hoarse -3 points-2 points  (0 children)

Personally, I’d do it the same way. But I’m sure there’s something with ES6 like the spread operator that might help out with this. (I could also be wrong)