The two big phases of React
Reconciler
What is reconciliation?
Reconciliation is the algorithm React uses to diff one tree with another to determine which parts need to be changed.
Reconciliation is the algorithm behind what is popularly understood as the "virtual DOM." A high-level description goes something like this: when you render a React application, a tree of nodes that describes the app is generated and saved in memory.
We are encouraged to not use the term "virtual DOM" because it is a bit of a misnomer in host environments that are not concerned with the DOM
This tree is then flushed to the rendering environment — for example, in the case of a browser application, it's translated to a set of DOM operations
Why is reconciliation necessary?
In short,
Re-rendering the entire app on each change is prohibitively costly in terms of performance. React has optimizations which create the appearance of whole app re-rendering while maintaining great performance. The bulk of these optimizations are part of a process called reconciliation.
Renderer
What is a renderer?
React was originally created for the DOM but it was later adapted to also support native platforms with React Native. This introduced the concept of “renderers” to React internals.
The common renderers are:
- react-dom https://github.com/facebook/react/tree/main/packages/react-dom
- react-native-renderer https://github.com/facebook/react/tree/main/packages/react-native-renderer
- react-test-renderer https://github.com/facebook/react/tree/main/packages/react-test-renderer
Why is a renderer necessary?
Renderers manage how a React tree turns into the underlying platform calls.
- What is meant by a host environment(underlying platform)?
- e.g. Native vs Web
- Why would a different host environment matter?
- Different host environments have different host(built-in) components
- Host components for web:
div
,span
,img
etc - Host components for native:
View
,Text
,Image
etc
- What is the main purpose of a renderer
- To act as a bridge between the reconciler and a host environment
Reconciliation versus rendering
- Renders and reconcilers are completely different libraries.
- React is designed so that reconciliation and rendering are separate phases.
- The reconciler does the work of computing which parts of a tree have changed
- The renderer then uses that information to actually update the rendered app.
- This separation means that React DOM and React Native can use their own renderers while sharing the same reconciler, provided by React core.
Diving into a renderer for the web
Understanding the web render process
Assuming a web application, the render process happens in 4 steps:
- A render is triggered
- Rendering a component(render phase)
- Committing to the DOM(commit phase)
- Browser repaints
People often say that updates happen in the reconciler in two phases: render and commit phase
Triggering a render
There are two reasons for a component to render:
- It’s the component’s initial render.
- The component’s (or one of its ancestors’) state has been updated.
Rendering a component
“Rendering” is React calling your components.
- On initial render, React will call the root component.
- During the initial render, React will create the DOM nodes
- For subsequent renders, React will call the function component whose state update triggered the render.
- During a re-render, React will calculate which of their properties, if any, have changed since the previous render. It won’t do anything with that information until the next step, the commit phase.
Committing to the DOM
After rendering (calling) your components, React will modify the DOM.
- For the initial render, React will use the
appendChild()
DOM API to put all the DOM nodes it has created on screen.
- For re-renders, React will apply the minimal necessary operations (calculated while rendering!) to make the DOM match the latest rendering output.
React only changes the DOM nodes if there’s a difference between renders.
Browser repaints
After rendering is done and React updates the DOM, the browser will repaint the screen.
Understanding a renderer(by building one)
Full disclosure: the following content is heavily referenced from a talk by Sophie Alpert, React Core Team's Engineering Manager.
Reconciliation is the algorithm React uses to diff one tree with another to determine which parts need to be changed.
Renderers manage how a React tree turns into the underlying platform calls.
Abstractly speaking, the output of reconciliation is a minimal list of commands that needs to happen. This could be things like
- Update view X to be red
- Create a new view
- Add view X and a child of view Y
A renderer, which is the bridge between the reconciler and the host environment, is then likely responsible for implementing how the commands work
- For a browser, this means writing implementation with DOM operations
View [app.js]
import React from 'react'; import logo from './logo.svg'; import './App.css'; let colors = ['red', 'green', 'blue']; let i = 0; function App() { let [showLogo, setShowLogo] = React.useState(true); let [color, setColor] = React.useState('red'); return ( <div className="App" > <header className="App-header"> {showLogo && <img src={logo} className="App-logo" alt="logo"/>} <button onClick={() =>setShowLogo(show => !show)}>Toggle logo visibility</button> <p bgColor={color} onClick={() => { i++; setColor(colors[i % 3]); }}> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App;
Entry point [index.js]
import React from 'react'; import ReactDOMMini from './ReactDOMMini'; import './index.css'; import App from './App'; ReactDOMMini.render(<App />, document.getElementById('root'));
Renderer [ReactDOMMini.js]
We'll call our renderer
ReactDOMMini
and start implementation from scratchReactDOMMini
- Must implement a
render
method render
method must somehow communicate with the reconciler- We know that the renderer should write implementations with DOM APIs
import ReactReconciler from 'react-reconciler'; let reconciler = ReactReconciler({...}); let ReactDOMMini = { render(whatToRender, div) { // the reconciler has its own notion of a container, so we have to call createContainer() // the other arguments are to disable concurrent mode or server side hydration let container = reconciler.createContainer(div, false, false); reconciler.updateContainer(whatToRender, container, null, null); }, }; export default ReactDOMMini;
- Reconciler
- As part of the configurations, a renderer must provide its own implementation of a set of methods(interface)
Accepts configurations as props
{ supportsMutation: true, // Called whenever a non-text host component is created createInstance( type, props, rootContainerInstance, hostContext, internalInstanceHandle, ) {}, // Called whenever a text host component is created createTextInstance( text, rootContainerInstance, hostContext, internalInstanceHandle, ) {}, appendChildToContainer(container, child) {}, appendChild(parent, child) {}, appendInitialChild(parent, child) {}, removeChildFromContainer(container, child) {}, removeChild(parent, child) {}, insertInContainerBefore(container, child, before) {}, insertBefore(parent, child, before) {}, prepareUpdate( instance, type, oldProps, newProps, rootContainerInstance, currentHostContext, ) {}, commitUpdate( instance, updatePayload, type, oldProps, newProps, finishedWork, ) {}, finalizeInitialChildren() {}, getChildHostContext() {}, getPublicInstance() {}, getRootHostContext() {}, prepareForCommit() {}, resetAfterCommit() {}, shouldSetTextContent() { return false; }, }
createInstance
- From the logs, notice that we only deal with host components, i.e.
<App />
is ignored
createInstance( type, props, rootContainerInstance, hostContext, internalInstanceHandle, ) { console.log('createInstance:', { type, props }); let el = document.createElement(type); // set DOM attributes from react props ['alt', 'className', 'href', 'rel', 'src', 'target'].forEach(k => { if (props[k]) el[k] = props[k]; }); // add event listeners if (props.onClick) { el.addEventListener('click', props.onClick); } // other self-defined props if (props.bgColor) { el.style.backgroundColor = props.bgColor; } return el; },
createTextInstance
createTextInstance( text, rootContainerInstance, hostContext, internalInstanceHandle, ) { return document.createTextNode(text); },
appendChild
and its variants- Generally called when React wants to add an element into its component hierarchy
- The three variants are called in specific situations that we don't really care about
// Called when React wants to add an element into its component hierarchy // container/parent: DOM element // child: DOM element or node returned from createInstance or createTextInstance appendChildToContainer(container, child) { container.appendChild(child); }, appendChild(parent, child) { parent.appendChild(child); }, appendInitialChild(parent, child) { parent.appendChild(child); },
removeChild
/insertBefore
and their variants
// used in removing an element removeChildFromContainer(container, child) { container.removeChild(child); }, removeChild(parent, child) { parent.removeChild(child); }, // used when adding a element in the middle of a list insertInContainerBefore(container, child, before) { container.insertBefore(child, before); }, insertBefore(parent, child, before) { parent.insertBefore(child, before); },
- Updates
// Called when a component is updated(render phase) // returns an update payload prepareUpdate( instance, type, oldProps, newProps, rootContainerInstance, currentHostContext, ) { let payload; if (oldProps.bgColor !== newProps.bgColor) { payload = {newBgColor: newProps.bgColor}; } return payload; }, // Called when a component is updated(commit phase) commitUpdate( instance, updatePayload, type, oldProps, newProps, finishedWork, ) { if (updatePayload.newBgColor) { instance.style.backgroundColor = updatePayload.newBgColor; } },
Demo: making observations on state changes
createInstance
is called on initial render
appendChildToContainer
is necessary for.render()
- Toggling logo will call
createInstance
andremoveChild
- Handling customProps
- Handling state updates
What is missing
- Handling all props, besides bgColor
- Handling uncontrolled components
- Handling refs
- Etc etc
Next steps
As of React v16, the internals of the reconciler were completely rewritten with the main change being the introduction of React Fiber
Fiber reimplements the reconciler. It is not principally concerned with rendering, though renderers will need to change to support (and take advantage of) the new architecture.
This supports more advanced(experimental) features such as suspense, concurrent mode and incremental rendering.
- Understanding React Fiber
- Understanding concurrent renderers(v18)
References
- legacy.reactjs.org
- https://github.com/acdlite/react-fiber-architecture
- https://sophiebits.com/2019/10/24/building-a-custom-react-renderer