Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does React discard the entire DOM subtree and recreate it from scratch?

There are two helpers that can be used to add content while rendering:

  ...

  const DisplayA = () => <div className={'containerA'}>
    <button onClick={handleToggleA}>{"A toggled: " + toggledA.toString()}</button>
  </div>

  const displayB = () => <div className={'containerB'}>
    <button onClick={handleToggleB}>{"B toggled: " + toggledB.toString()}</button>
  </div>

  return (
    <>
      <DisplayA />
      { displayB() }
    </>
  );

  ...

The problem is that in the first helper, React always discards the entire subtree and creates it again from scratch, as can be seen here:

subtree is discarded and recreated

The demo

I know, the first way is syntax sugar for React.createElement so a new component is created each render. However, the second way, a distinct arrow function is created each render too.

  1. Why does React not know how to reuse the subtree the first way but does know the second way? What is happening under the hood?

  2. How can we spot when the DOM subtree is discarded and recreated each render? Is it enough to assume that one should not create inline components, and use inline functions only?

Notice, that helpers can come from props, for example (render props pattern).

like image 216
Daniel Avatar asked Oct 22 '19 15:10

Daniel


People also ask

Does React change the DOM?

When the state of a component changes, React updates the virtual DOM tree. Once the virtual DOM has been updated, React then compares the current version of the virtual DOM with the previous version of the virtual DOM. This process is called “diffing”.

How does React render the DOM?

React uses virtual DOM to enhance its performance. It uses the observable to detect state and prop changes. React uses an efficient diff algorithm to compare the versions of virtual DOM. It then makes sure that batched updates are sent to the real DOM for repainting or re-rendering of the UI.

When a component is being removed from the DOM?

Unmounting. The next phase in the lifecycle is when a component is removed from the DOM, or unmounting as React likes to call it. React has only one built-in method that gets called when a component is unmounted: componentWillUnmount()

What is reconciliation in ReactJS?

React provides a declarative API so that you don't have to worry about exactly what changes on every update. This makes writing applications a lot easier, but it might not be obvious how this is implemented within React.


2 Answers

This will depend in which scope DisplayA is defined. Functional components should usually be defined at the top level of a file. In your demo DisplayA is a component that is created inside the render of App, so every time App renders a new functional component is created, not a new invocation of the same component.

To resolve this make DisplayA top level in the file and pass props through to it.

const DisplayA = ({handleToggle, toggled}) => <div className={'containerA'}>
  <button onClick={handleToggle}>{"A toggled: " + toggled.toString()} </button>
</div>

const App = () => {
  ...
  return <>
    <DisplayA handleToggle={() => {...}} toggle={...} />
  </>
}

The second approach does not create a component which is passed to react for reconciliation, but is a function which is invoked while rendering and puts the contained elements into the rendering of this component.

like image 172
ggovan Avatar answered Oct 23 '22 06:10

ggovan


In the first case you are not calling DisplayA. Instead, you are letting react decide when to render it. Notice how when transpiled, React.createElement(DisplayA) does not invoke this function. When react renders the subtree, it decides what needs to be re-rendered.

The process of handling tree changes/updates is called reconciliation. In react docs it says that same types will try to maintain state, whereas different component types will always perform a tear down on the DOM tree.

The latter happens with your DisplayA component because it's a different value on every render. Although the component renders the same view, React can't be sure that this is the same component because the value DisplayA points to a different component reference each time. In this case you are using function components, therefore the value is a reference to a new function. The variable name just happens to be the same - it does not have any importance at run time.

In the second case with displayB you are explicitly calling the function and rendering its result. Because the function is pure, this is equivalent to pulling its return value and inlining it inside the parent component:

return (
  <>
    <DisplayA />
    { 
      <div className={'containerB'}>
        <button onClick={handleToggleB}>{"B toggled: " + toggledB.toString()}</button>
      </div>
    }
  </>
)

Notice how the second child of this fragment is now a div. Because divs are primitive elements, they are represented by the literal string 'div' and not a reference to a component. React knows between renders that this is the same tree and as such does not destroy it. This would also work if you had any external component with a stable reference - it will be considered an element of the same type.

like image 41
Edon Avatar answered Oct 23 '22 05:10

Edon