React prevent remounting components passed from props

To understand this issue, I think you might need to know the difference between a React component and a React element and how React reconciliation works.

React component is either a class-based or functional component. You could think of it as a function that will accept some props and eventually return a React element. And you should create a React component only once.

React element on the other hand is an object describing a component instance or DOM node and its desired properties. JSX provide the syntax for creating a React element by its React component: <Component someProps={...} />

At a single point of time, your React app is a tree of React elements. This tree is eventually converted to the actual DOM nodes which is displayed to our screen.

Everytime a state changes, React will build another whole new tree. After that, React need to figure a way to efficiently update DOM nodes based on the difference between the new tree and the last tree. This proccess is called Reconciliation. The diffing algorithm for this process is when comparing two root elements, if those two are:

  • Elements Of Different Types: React will tear down the old tree and build the new tree from scratch // this means re-mount that element (unmount and mount again).
  • DOM Elements Of The Same Type: React keeps the same underlying DOM node, and only updates the changed attributes.
  • Component Elements Of The Same Type: React updates the props of the underlying component instance to match the new element // this means keep the instance (React element) and update the props

That's a brief of the theory, let's get into pratice.

I'll make an analogy: React component is a factory and React element is a product of a particular factory. Factory should be created once.

This line of code, ChildRoutes is a factory and you are creating a new factory everytime the parent of the Component re-renders (due to how Javascript function created):

<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>

Based on the routingConfig, the MainPage created a factory to create the SecondPage. The SecondPage created a factory to create the ThirdPage. In the MainPage, when there's a state update (ex: foo got incremented):

  1. The MainPage re-renders. It use its SecondPage factory to create a SecondPage product. Since its factory didn't change, the created SecondPage product is later diffed based on "Component Elements Of The Same Type" rule.
  2. The SecondPage re-renders (due to foo props changes). Its ThirdPage factory is created again. So the newly created ThirdPage product is different than the previous ThirdPage product and is later diffed based on "Elements Of Different Types". That is what causing the ThirdPage element to be re-mounted.

To fix this issue, I'm using render props as a way to use the "created-once" factory so that its created products is later diffed by "Component Elements Of The Same Type" rule.

<Component 
    {...rest} 
    renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>

Here's the working demo: https://codesandbox.io/s/sad-microservice-k5ny0


Reference:

  • React Components, Elements, and Instances
  • Reconciliation
  • Render Props

The culprit is this line:

<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>

More specifically, the ChildRoutes prop. On each render, you are feeding it a brand new functional component, because given:

let a = props => <Routing routes={routes} {...props}/>
let b = props => <Routing routes={routes} {...props}/>

a === b would always end up false, as it's 2 distinct function objects. Since you are giving it a new function object (a new functional component) on every render, it has no choice but to remount the component subtree from this Node, because it's a new component every time.

The solution is to create this functional component once, in advance, outside your render method, like so:

const ChildRoutesWrapper = props => <Routing routes={routes} {...props} />

... and then pass this single functional component:

<Component {...rest} ChildRoutes={ChildRoutesWrapper} />