Shallow vs. Deep merge in React.js

**Prerequisites **for this article – fundamentals of React.js

Intro

When it's required to keep only single state as primitive of string , number , or any relevant , there is no need to use deep merge approach , conversely – make use of shallow merge . However for more complex scenarios , when previous state mostly required to be preserved i.e. not overwritten – as in case of shallow merge which overwrites , the following i.e. deep merge stays per recommendation and even requirement to use . Just like in functional component good practice is too copy a state and only then modify it (indirect mutation a.k.a. deep merge) rather than passing value instead function as direct mutation (a.k.a. shallow merge) , same logic applies when objects used for initial render and , of course , for subsequent re-render Compared with containers (objects, arrays) , primitives are relatively insensitive to direct mutations . Let's examine a couple of examples conceptually without digging too much into React.js code theory itself . First examples shows classic approach : the argument passed as value rather than function callback , next example will consider the opposite . Keep in mind both examples pivots around primitive value being modified for the imaginative Counter component . Consider the initial state for both Examples 1 & Example 2 utilizing useState hook as follows :

Let's practice

const [counter, setCounter] = useState(0);

Example 1

setCounter(counter + 1) // shallow (not recommended)

Example 2

setCounter(prevCounterState => prevCounterState + 1) // deep (recommended) ...
// .., especially when heavy computations might be foreseen)

Note : Although Example 2 takes precedence over Example 1 as recommendation , both examples technically would work 'cause hereby primitive over data structure (container) used

Now as we revised the shallow vs. deep merge for primitive such as number , let's consider similar logic applied with an object supplied as value with some sub-values (properties) within . Let's consider the following Example 3 :

Example 3

import {useState} from 'react'

const App = () => {
    const [clicks, setClicks] = useState( {
      left: 0, right: 0,
    } )

    const handleLeftClick = ()=> {
      /* clicks commented out to produce uncommon behaviour */
      setClicks( {/* ...clicks,  */left: clicks.left + 1} )
    }

    const handleRightClick = ()=> {
      /* (...object spread operator used to copy last value [deep merge]) */
      setClicks( {...clicks , right: clicks.right + 1} )
    }

    return (
      <div>
        <button onClick={handleLeftClick}>Left click {clicks.left}</button>
        <button onClick={handleRightClick}>Right click {clicks.right}</button>
      </div>
    )
}

export default App;

Example 3 in detail:


Now what happens if e.g. Left click pressed a few times first : within a first few click Left click gets incremented by 1, 2, 3,.. etc. , from which the very first click of Left click changes the Right click value from 0 to undefined (more precisely number zero just disappears) . Why this happens , is because if Left click pressed it does not copy path properties of left & right , accessing and modifying only left : from that moment no more property of right : value exists as was set initial . This demonstrates that sharing properties misbehaviour might be atomic – no way to get back and fix it . Of course this is mocked scenario as I commented ...click's spread operator value out on purpose ! Within this moment technically we can continue incrementing Left click by + 1s , but if Right click pressed for the first time ever , it gets NaN : from JS perspective everything seems to be clear i.e. undefined + 1 gives you NaN ( just the way it is ) , but what is happening in terms of perspective of React.js which recommends (more precisely requires pureness within state change) says about it ? Because technically initially both object spread operators evaluated and assigned for each handleLeftClick &amp; handleRightClick , from which the following i.e.handleRightClick expects both object values (left and right) even if only one of those will be modified within respective handler , so what happens next when we press Right click for state to change, the Right click expects property of right: 0 to be accessible which due to to initial click on the Left click did disappear . What does it tell us about overall ? It tries to tell that direct (a.k.a. shallow merge) which overwrites the very last state , is not a best practice if more than one (sub-)value within container (in this case – object) used . First direct mutable sate clicks of Left click kinda did "eliminated" the right: 0 sub-value (property) from the object which subsequently resulted into NaN as we tried to add 1 (+1) to already non existing property of the object . If this is clear enough , hopefully it is , you may ask what would happened if Right click gets clicked for the very first time instead – opposite scenario . Well pretty much interesting thing happens tho . Instead of writing one more paragraph try by oneself [source] .

The final example tries to touch same logic applied to object but with an array container , this time no NaN or similar produced , so no worries , I won't get into greater detail , rather just touch the surface of the following :

Example 4

import {useState} from 'react'

const App = () => {
    const [left, setLeft] = useState(0)
    const [right, setRight] = useState(0)
    const [allClicks, setAll] = useState([])

    const handleLeftClick = ()=> {
      setAll(allClicks.concat('L'))
      // (...array spread operator used to copy last value [deep merge])) 
      // & is equivalent to setAll([...allClicks, 'L'])
      setLeft(left + 1)
    }

    const handleRightClick = ()=> {
      setAll(allClicks.concat('R'))
      // otherwise equivalent to :..
      // setAll([...allClicks, 'R'])
      setRight(right + 1)
    }

    return (
      <div>
        <button onClick={handleLeftClick}>Left click {left}</button>
        <button onClick={handleRightClick}>Right click {right}</button>
        <p>{allClicks.join(' ')}</p>
      </div>
    )
}

export default App;

Same logic has been followed : first of all 1) copy the state , only when the state copied , 2) modify it ! In this case it's done with array-like spread operator or .concat() , keeping in mind .push() would do the job for shallow merge which mostly not a case we want to do , instead merge it deeply preserving data by modifying it indirectly ! That's all folks .

If any typos found and (or) suggestions could be made, please leave it in the comment section below . Thank you and see you in the next one !

Related references>

13