Build a React Custom Renderer

October 10, 2022

Intro

React knows nothing about the dom, it's just a JavaScript library to manage components and states, and returns VDOM. And this is the idea behind React, learn once, write anywhere. ReactDOM generates the html on the screen from the VDOM

The difference betweeen React and ReactDOM

That explains a lot, if you take a look at a react native, it uses the same react library we use for web, just a different renderer, which is react-native. Also if you know @react-three/fiber it’s a React renderer for Three,js

The VDOM is a javascript object like this

{
	type: “div”,
	props: {
		className: ‘container’,
		children:... other elements”
		… etc
	}
}

React Reconciler

It's a library build by React team to make the process of creating custom react renderer easier! It doesn't have a lot of docs and it's not stable but I will cover the basics in this article. For more info check the repo on Github.

Rendering mode

The reconciler has 2 different rendering modes Mutation mode Persistent mode

Mutation mode is used when the target environment UI is mutable like DOM where we can change a specific property of a specific element without rerender the whole page, persistent mode is when the target environment creates a new version of the UI with any updates. It's more efficient when you're dealing with mobile platforms for example.

Let’s build our custom renderer

Let’s create the render function for our renderer, and create a new file, for example custom-renderer.js:

import ReactReconciler from "react-reconciler";

const reconciler = ReactReconciler({
  // ... configuration options ...
});

const render = (element, container) => {
  // ... implementation ...
};

export default render;

Let’s explain them one by one, a good way to understand the ReactReconciler() by starting with these empty configuration functions like this:

const reconciler = ReactReconciler({
  // ... configuration options ...
  supportsMutation: true,
  createInstance(type, props) {
    console.log({ type: type, props: props });
  },
  createTextInstance: () => null,
  getRootHostContext: () => null,
  getChildHostContext: () => null,
  shouldSetTextContent: () => null,
  prepareForCommit: () => null,
  clearContainer: () => null,
  resetAfterCommit: () => null,
  appendInitialChild: () => null,
  appendChildToContainer: () => null,
  finalizeInitialChildren: () => null,
  removeChildFromContainer: () => null,
});

We will explore what each option does in this article supportsMutation is to enable the mutation mode, and we gonna just print what’s in the VDom in the console in createInstance

For the render function:

const render = (element, container) => {
  // ... implementation ...
  let root = reconciler.createContainer(container, false, false);
  reconciler.updateContainer(element, root, null, null);
};

First we created the root and we update the root when the reconciler gets changes from react

Note: The false, false and null, null is for concurrent mode and server-side hydration

In the main.js or index.js we gonna replace the ReactDOM with our render function

Use our custom render function instead.

And now we can see the vdom in the console

The vdom in the console

Render the VDOM on the screen

Let’s remove the console.log on createInstance method and render something:

createInstance(type, props) {
   const element = document.createElement(type);
   Object.keys(props).forEach((prop) => {
     // Filter out non-HTML attributes like:
     if (!["children", "onClick", "key"].includes(prop)) {
       // Appends each html attribute to the element
       element[prop] = props[prop];
     }
   });
   // return the HTML element
   return element;
 },

createTextInstance method

React calls this method from the renderer when there is a raw text in the JSX like “Hello world” in the example:

<div className="App">
  <h1>Hello world</h1>
</div>

For our simple DOM renderer, we don’t need so much, just append this text in the DOM

createTextInstance: (text) => document.createTextNode(text);

For now, we created the instances but we didn’t attach them to any container or parent in the DOM

Append children to the DOM

There are 3 methos that react calls in different situations to append children in the DOM appendChild, appendInitialChild and appendChildToCotainer and they’re pretty similar

{
    // ...
    appendChild: (parent, child) => parent.appendChild(child),
    appendInitialChild: (parent, child) => parent.appendChild(child),
    appendChildToContainer: (parent, child) => parent.appendChild(child),
    // ...
}

Append children to the DOM

Custom elements

We are controlling the render function for react now and we can create our own elements now, for example I wanna create a <redHeading> element and it returns a red h1 element

<redHeading>Hello world from our Custom Renderer</redHeading>

And we can resolve this in our createInstance method in the reconciler

createInstance(type, props) {
   if(type === "redHeading") {
     const element = document.createElement("h1");
     element.style.color = "red";
     return element;
   }
 // …etc
}

Custom elements

Woohoo 🎉

Stay tuned for the next part where we gonna add events and more features to our custom renderer🔥