Skip to main content

Avoid unnecessary remounting of DOM elements in React

I ran into a strange problem while trying to use React’s built-in animation API to fade in/out DOM elements when they enter/exit the page.

Here I present a minimal, complete, and verifiable example of the problem that I ran into. I have a Child element that renders to an HTML button (styled by Bootstrap for ease of viewing) with a numeric label for demo purposes. I also have a Container component that, on launch, will render the first child component View1 1. View1 simply wraps two Child elements with ids 1 and 2:

2 <Child id={1}/>
3 <Child id={2}/>

Then after one second (to simulate a user-triggered animation), Container will render the second child component View21, which is structurally identical to View1. I also strategically added log messages to a few lifecycle methods of both components to help you follow along.

This was my first implementation:

Because the “virtual DOM” structure is the same between the two views:

2 <Child id={1}/>
3 <Child id={2}/>

I expected React to not unmount and then remount the two DOM nodes corresponding to the two buttons when switching from View1 to View2. However, as the console.log messages2 show:

1"Child 1 has been mounted"
2"Child 2 has been mounted"
3"Container mounted"
4"Will change viewIndex to 2 in one second."
5"Child 1 has been unmounted"
6"Child 2 has been unmounted"
7"Child 1 has been mounted"
8"Child 2 has been mounted"
9"Container's state has been updated to 2 and finished re-render."

React did unmount the DOM nodes in the switch between the views. In this first implementation, I did all I could to tell React that the elements between the two views are the same by using as many keys as I could. However, that was of no use.

I scratched my heads and tried lots of things for a few hours and came up with this very slightly different implementation that does what I want:

as shown by the console log:

1"Child 1 has been mounted"
2"Child 2 has been mounted"
3"Container mounted"
4"Will change viewIndex to 2 in one second."
5"Container's state has been updated to 2 and finished re-render."

There’s no remounting of components between views! Voila!

And what have I changed? Just two lines! I just changed how the JSX is generated. Here’s the diff going from the first to the second implementation:

1@@ -62,9 +62,9 @@
2 render() {
3 const {viewIndex} = this.state;
4 if (viewIndex === 1) {
5- return <View1 key='index'/>
6+ return View1()
7 } else {
8- return <View2 key='index'/>
9+ return View2()
10 }
11 }
12 }

The reason for the different outcomes is a heuristic that React uses when performing reconcilliation i.e. the process making the real DOM match the virtual DOM using the smallest number of DOM mutations. As stated in the official documentation , React assumes that the internal of two components with different displayNames are wildly different. As such, it doesn’t waste its time diffing their subtrees but rather re-renders the entire subtree even though those two subtrees might just be identical like in my example.

Basically, in the first implementation, from React’s point of view, it has to reconcile this virtual DOM tree:

2 <View1 key='view'/>

with this tree:

2 <View2 key='view'/>

As soon as Reacat sees the change from View1 to View2, it unmounts all DOM nodes inside View1 and mounts those inside View2.

On the other hand, in the second implementation, React has to reconcile this:

2 <div>
3 <Child id={1} key='child-1'/>
4 <Child id={2} key='child-2'/>
5 </div>

with this:

2 <div>
3 <Child id={1} key='child-1'/>
4 <Child id={2} key='child-2'/>
5 </div>

Expressed this way, it’s clear that in the second case, no DOM remounting is needed.

This whole example might seem contrived but has significant consequence for animation with React’s transition API. Suppose in View1, two buttons are spaced some distance apart and in View2, they move closer together through a CSS transform transition3.

1function View1() {
2 return (
3 <div>
4 <Child id={1} key='child-1' x={20} y={0}/>
5 <Child id={2} key='child-2' x={200} y={0}/>
6 </div>
7 )
9function View2() {
10 return (
11 <div>
12 <Child id={1} key='child-1' x={60} y ={0}/>
13 <Child id={2} key='child-2' x={120} y={0}/>
14 </div>
15 )

The second approach yield a pleasing transition:

while the first one doesn’t. The buttons just disappear at their original locations and re-appear at their new locations because CSS transitions are only carried out if there’s a CSS change on the same DOM node.

  1. These functions define “functional components” in React.
  2. Click here to see how to view the JavaScript console from within CodePen on the full site
  3. I used transform instead of top and left to take advantage of hardware-accelerated transitions.
© 2021 Huy Nguyen© 2021