My Authors
Read all threads
Is it possible to build a custom React renderer in a Twitter thread? I don't know, let's see! 👇🏿

This thread is not going to be 100% technically accurate with the right terminology and all. I'm going to describe things as I understood them when I learned this stuff myself.
Think of React as a library that adds, updates and deletes components. That's it. It's only responsible for managing, but not rendering them.

That's what a renderer is for. React notifies renderer about any component changes so that it knows when/how to update the UI.
Examples of such renderers are:
- React DOM (renders to HTML)
- React Native (renders to mobile UI)
- Ink (renders to terminal)

React can work with any custom renderer, so theoretically it can be used to build a UI for any platform.
Let's build a simple clone of React DOM to render components in the browser. Clone a starter code at gist.github.com/vadimdemedes/d… which has:
- index.html (#root - target div to render our app)
- app.js (app itself + custom render fn)
- reconciler.js (implementation of our renderer)
You might be wondering why I'm calling renderer a "reconciler". I don't have a definitive answer to that, but my thinking is this. Reconciler exposes pre-defined methods for a custom renderer to fill out with code that updates UI to match (reconcile with) React's internal state.
In reconciler.js I've added the base for our renderer. createReconciler() creates a reconciler and accepts an object of props that define the behavior & callbacks which React calls after something happens. For example, resetAfterCommit() is called after each update to components.
Ok, enough theory, we're going to render <h1>Hello World</h1> with our renderer (see app.js). Run `npm run dev` and open http://localhost:1234. You should see the error in console, because we haven't taught our custom reconciler how to work with DOM elements or text yet.
To create "Hello World" text, React expects to find createTextInstance() in the reconciler. Let's add it and use createTextNode() API to create a text node with value stored in `text` arg ("Hello World", in this case). Now it exists in memory, but it will be added to <h1> later.
After we told React how to render text, there's a new error! React tries to render <h1>, but our reconciler doesn't know how to deal with HTML tags yet. For this, there's a createInstance() reconciler function that React uses to render all HTML elements (e.g. <h1>, <div>, <p>).
Here, createInstance() uses document.createElement() API to create a new element. It knows that <h1> needs to be created by checking the `type` argument. If we were to render <div>Hello World</div>, `type` would equal "div" instead and so on.
Now why is this function named createInstance(), if I'm returning an HTML element? Because React itself doesn't know anything about the environment we want to render our UI in. It can be a browser, native app, VR app, etc. React uses an abstract terminology to avoid assumptions.
So after we've added functions to create text and HTML elements, we need to let React insert that text into elements. That's why you'll see the following error if you refresh the page.

appendInitialChild() in reconciler is how React adds elements inside other elements.
The implementation of that function simply uses browser's appendChild() API to add a child node into another node. Note that I've added appendChildToContainer() too, which does the same thing. I still don't know the difference between them to be honest.
Voilà! Since we told React how to create text, HTML element and how to add text into that element, it has everything it needs to render <h1>Hello World</h1>!

Now let's update createInstance() to handle props and make our text colorful.
All props that are passed to HTML element are included in `props` argument inside createInstance(). Here we can iterate through all of them and if we encounter `style` prop, assign its value to instance's `style` prop. Browser will apply styles from it when rendering our <h1>.
I'm obviously simplifying things a lot here, but this would also be the place to handle other props and manage event listeners, like `onClick`.

Let's also update the code in app.js to add `lightcoral` color to <h1>. You should now see that "Hello World" just became much cuter!
If we wanted to write "raw" JavaScript code to do the same thing using just browser APIs and no React, this is how it would look.

Note how the sequence of reconciler functions is similar to what we would write without React. createTextInstance -> createInstance -> appendChild.
Let's dive deeper. Imagine that we want to apply `lightcoral` color later. We'd use a useState hook to store the value of current color and useEffect hook to set up a 1s timeout, which will trigger the color change. Update app.js with these changes as in the screenshot below.
Any state update triggers a mutation in React and we need to teach our custom reconciler how to handle them.

To update an element we need to add prepareUpdate() and commitUpdate(). prepareUpdate() calculates the diff between old and new props and commitUpdate() applies it.
As you can see, commitUpdate() is similar to createInstance(), except for two things:

1. commitUpdate() changes an existing instance, received via `instance` argument.
2. `updatedProps` is an object we've returned from prepareUpdate().

Now you can see how text color changes!
Let's also make our demo app a little more complicated by changing who we are greeting after 1s. We're going to add a `name` state with a default value "..." (like it's a loading indicator) and change it to "Jane" at the same time we're changing the text color.
We'll also need to update the reconciler to handle updates to text nodes by adding a commitTextUpdate() function.

It receives the text node we need to update via `textInstance` and new text via `newText` arguments. To update the value of a text node, we reassign `nodeValue`.
Save changes to reconciler.js and app.js files, refresh the page and you should see how "Hello, ..." text is changed to "Hello, Jane" text with a coral color. Congrats, you just implemented basic mutations in your custom React renderer!
At this point, we have a functioning custom renderer that can create any HTML element, apply styles to it and insert text inside.

FYI, we haven't implemented all of the reconciler functions here, like for removing components or inserting new ones in between other components.
I hope this thread was interesting and gave you some understanding on how you can leverage React to build component-based UIs for any platform.

When I was building Ink there wasn't a lot of docs on this stuff, so I figured I'd share what I've learned by trial & error.
If you found this valuable, I can go deeper and cover mutations for removal/inserts, support for Suspense, adding support for Devtools for your custom renderer or other things you may have wondered about.

Thanks for reading this far!
Missing some Tweet in this thread? You can try to force a refresh.

Keep Current with Vadim Demedes

Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

Twitter may remove this content at anytime, convert it as a PDF, save and print for later use!

Try unrolling a thread yourself!

how to unroll video

1) Follow Thread Reader App on Twitter so you can easily mention us!

2) Go to a Twitter thread (series of Tweets by the same owner) and mention us with a keyword "unroll" @threadreaderapp unroll

You can practice here first or read more on our help page!

Follow Us on Twitter!

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just two indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3.00/month or $30.00/year) and get exclusive features!

Become Premium

Too expensive? Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal Become our Patreon

Thank you for your support!