13
Shallow vs. Deep merge in React.js
**Prerequisites **for this article – fundamentals of React.js
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 :
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 & 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