How to Build Your Own Version of React From Scratch

Introduction

React is a huge codebase and knowing where to start looking can be confusing. You may find files like this one - which is 3000 lines long - and you may decide to look the other way and keep building awesome UI’s with the library. This has been me for the most part up until recently when I decided that I was going to take a deep dive at looking at the codebase and other Virtual Dom implementations like PreactJS

We won’t cover all the features that React brings to the table. I still don’t understand everything about the codebase either, but we will look at some of the most important details that can help you understand React better, see what makes React 16+ faster than React 15 and as a bonus, you will learn 1 way of how NPM packages are created & bundled with Rollup.

Setup & Basic Overview

Create a folder with a name for your React implementation. If you want to publish it on NPM then make sure there’s no Js library with the name you have in mind. I’ll name mine tevreact .

Once we are done building we will be able to have a JSX powered React clone that reads from state and props as shown in the screenshot below.

A demo app that reads from local state and props.

JSX

JSX is XML like syntax that is written inside Javascript files and is transpiled to function calls which in turn is rendered to either the Dom thanks to React-Dom or React Native views

If you wrote

how you write your React code

You get

JSX transpiled to function calls.

createElement takes a node-type, props and finally children. If either children or props are missing we get null in the fields. Lastly, createElement returns an object {type, props} which is then used to render to the dom. The above calls will produce

for React’s case, it has other properties such as key, ref, etc, we will only be interested in type and props for now.

Let’s build our own createElement function

We’ll do things a bit differently in our implementation, we want all our objects to have the same shape. React’s children can either be an array of objects or a string. For our case, we want the children to be objects. Which means the h1 children props will be

Inside the src folder create 2 files element.js && tevreact.js(what you called your React clone .js).Inside your element.js is where we will have our createElement function.

[pastacode lang="javascript" manual="const%20TEXT_ELEMENT%20%3D%20%22TEXT%22%3B%0A%0A%2F**%0A%20*%20%40param%20%7Bstring%7D%20type%20-%20the%20node%20type%0A%20*%20%40param%20%7B%3Fobject%7D%20configObject%20-%20the%20props%0A%20*%20%40param%20%20%7B%3F...any%7D%20args%20-%20the%20children%20array%0A%20*%20%40returns%20%7Bobject%7D%20-%20to%20be%20called%20by%20tevreact.render%0A%20*%2F%0Aexport%20function%20createElement(type%2C%20configObject%2C%20...args)%20%7B%0A%20%20const%20props%20%3D%20Object.assign(%7B%7D%2C%20configObject)%3B%0A%20%20const%20hasChildren%20%3D%20args.length%20%3E%200%3B%0A%20%20const%20nodeChildren%20%3D%20hasChildren%20%3F%20%5B...args%5D%20%3A%20%5B%5D%3B%0A%20%20props.children%20%3D%20nodeChildren%0A%20%20%20%20.filter(Boolean)%0A%20%20%20%20.map(c%20%3D%3E%20(c%20instanceof%20Object%20%3F%20c%20%3A%20createTextElement(c)))%3B%0A%0A%20%20return%20%7B%20type%2C%20props%20%7D%3B%0A%7D%0A%0A%2F**%0A%20*%20%40param%20%7Bstring%7D%20nodeValue%20-%20the%20text%20of%20the%20node%0A%20*%20%40returns%20%7Bobject%7D%20-%20a%20call%20to%20createElement%0A%20*%2F%0Afunction%20createTextElement(nodeValue)%20%7B%0A%20%20return%20createElement(TEXT_ELEMENT%2C%20%7B%20nodeValue%2C%20children%3A%20%5B%5D%20%7D)%3B%0A%7D" message="" highlight="" provider="manual"/]

Moving on to the render phase

You’ve probably seen the render function from React-Dom

[pastacode lang="javascript" manual="const%20root%20%3D%20document.getElementById(%22root%22)%0AReactDom.render(%3CComponent%20%2F%3E%2C%20root)" message="" highlight="" provider="manual"/]

The render function is responsible for rendering to the real dom. It takes an element and a parentNode that the element will be appended to after being created. We still haven’t added support for custom JSX tags but we are getting there. Create 2 new files reconciler.js & dom-utils.js the renderwill be in the reconciler.js file. Firstly export the TEXT_ELEMENT in the element.js to be export const TEXT_ELEMENT = "TEXT";

The render method checks to see if the element is a string, if it is, a text node is created, if not an instance of the HTML element specified is created.

We will define updateDomProperties in a bit but it essentially takes the props provided and appends them to the element, the same process is repeated for the element’s children. The render function is called recursively on each child.

In dom-utils.js create the updateDomProperties function and make sure it is exported.

[pastacode lang="javascript" manual="import%20%7B%20TEXT_ELEMENT%20%7D%20from%20%22.%2Felement%22%3B%0A%0A%2F**%0A%20*%20%40param%20%7BHTMLElement%7D%20dom%20-%20the%20html%20element%20where%20props%20get%20applied%20to%0A%20*%20%40param%20%7Bobject%7D%20props%20-%20consists%20of%20both%20attributes%20and%20event%20listeners.%0A%20*%2F%0Aexport%20function%20updateDomProperties(dom%2C%20props)%20%7B%0A%20%20const%20isListener%20%3D%20name%20%3D%3E%20name.startsWith(%22on%22)%3B%0A%20%20Object.keys(props)%0A%20%20%20%20.filter(isListener)%0A%20%20%20%20.forEach(name%20%3D%3E%20%7B%0A%20%20%20%20%20%20const%20eventType%20%3D%20name.toLowerCase().substring(2)%3B%0A%20%20%20%20%20%20dom.addEventListener(eventType%2C%20props%5Bname%5D)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20const%20isAttribute%20%3D%20name%20%3D%3E%20!isListener(name)%20%26%26%20name%20!%3D%3D%20%22children%22%3B%0A%20%20Object.keys(props)%0A%20%20%20%20.filter(isAttribute)%0A%20%20%20%20.forEach(name%20%3D%3E%20%7B%0A%20%20%20%20%20%20dom%5Bname%5D%20%3D%20props%5Bname%5D%3B%0A%20%20%20%20%7D)%3B%0A%7D" message="" highlight="" provider="manual"/]

If the name starts with on like onClick, onSubmit then its an event listener and we need to add it as such via dom.addEventListener. the rest of the props just applied as attributes to the element.

Testing time

Let’s see what we have come up with up until this point.

Export functions to the tevreact.js file

[pastacode lang="javascript" manual="import%20%7B%20render%20%7D%20from%20%22.%2Freconciler%22%3B%0Aimport%20%7B%20createElement%20%7D%20from%20%22.%2Felement%22%3B%0A%0Aexport%20%7B%20createElement%2C%20render%20%7D%3B%0A%0Aexport%20default%20%7B%0A%20%20render%2C%0A%20%20createElement%0A%7D%3B%0Aview%20raw" message="" highlight="" provider="manual"/]

Install rollup & setup npm scripts

[pastacode lang="javascript" manual="%24%20yarn%20init%20-y%20%2F%2F%20if%20you%20have%20yarn%20installed%0A%24%20npm%20init%20-y%20%2F%2F%20if%20you%20have%20npm%0A%2F%2F%20install%20rollup%0A%24%20yarn%20add%20rollup%20%E2%80%94-dev%0A%24%20npm%20install%20rollup%20%E2%80%94-save-dev" message="" highlight="" provider="manual"/]

Setup the NPM scripts in package.json replace yarn with npm run if you don't have yarn installed. Replace the tevreact.js to the file name you chose.

[pastacode lang="javascript" manual="%22scripts%22%3A%20%7B%0A%20%20%20%20%22build%3Amodule%22%3A%20%22rollup%20src%2Ftevreact.js%20-f%20es%20--exports%20named%20-n%20tevreact%20-o%20dist%2Ftevreact.es.js%22%2C%0A%20%20%20%20%22build%3Amain%22%3A%20%22rollup%20src%2Ftevreact.js%20-f%20umd%20--exports%20named%20-n%20tevreact%20-o%20dist%2Ftevreact.umd.js%22%2C%0A%20%20%20%20%22build%3Aall%22%3A%20%22yarn%20build%3Amodule%20%26%26%20yarn%20build%3Amain%22%2C%0A%20%20%20%20%22prepublishOnly%22%3A%20%22yarn%20build%3Aall%22%20%2F%2F%20this%20command%20is%20run%20automatically%20before%20publishing%20to%20npm%0A%20%20%7D%2C" message="" highlight="" provider="manual"/]

Example repo

Run npm run build:all

Create a folder named examples in the root of the directory and a basic index.html file.

[pastacode lang="javascript" manual="%3C!DOCTYPE%20html%3E%0A%3Chtml%20lang%3D%22en%22%3E%0A%20%20%3Chead%3E%0A%20%20%20%20%3Cmeta%20charset%3D%22UTF-8%22%20%2F%3E%0A%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1.0%22%20%2F%3E%0A%20%20%20%20%3Cmeta%20http-equiv%3D%22X-UA-Compatible%22%20content%3D%22ie%3Dedge%22%20%2F%3E%0A%20%20%20%20%3Ctitle%3EDidact%3C%2Ftitle%3E%0A%20%20%3C%2Fhead%3E%0A%20%20%3Cbody%3E%0A%20%20%20%20%3Cdiv%20id%3D%22app%22%3E%3C%2Fdiv%3E%0A%20%20%20%20%3C!--%20we%20need%20the%20babel%20standalone%20transpiler%20here%20since%20this%20is%20just%20a%20basic%20html%20page%20--%3E%0A%20%20%20%20%3Cscript%20src%3D%22https%3A%2F%2Funpkg.com%2Fbabel-standalone%407.0.0-beta.3%2Fbabel.min.js%22%3E%3C%2Fscript%3E%0A%20%20%20%20%3C!--%20load%20the%20umd%20version%20because%20it%20sets%20global.tevreact%20--%3E%0A%20%20%20%20%3Cscript%20src%3D%22..%2Fdist%2Ftevreact.umd.js%22%3E%3C%2Fscript%3E%0A%20%20%20%20%3Cscript%20type%3D%22text%2Fjsx%22%3E%0A%20%20%20%20%20%20%2F**%20%40jsx%20tevreact.createElement%20*%2F%0A%20%20%20%20%20%20%2F**%20In%20the%20comment%20above%20we%20are%20telling%20babel%20which%20function%20it%20should%0A%20%20%20%20%20%20use%20the%20default%20is%20React.createElement%20and%20we%20want%20to%20use%0A%20%20%20%20%20%20our%20own%20createElement%20function*%2F%0A%20%20%20%20%20%20const%20appElement%20%3D%20(%0A%20%20%20%20%20%20%20%20%20%20%3Cdiv%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ch1%3EHello%20Tev%2C%20Have%20you%20watched%20John%20Wick%3C%2Fh1%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%20%20)%0A%20%20%20%20%20%20tevreact.render(appElement%2C%20document.getElementById(%22app%22))%3B%0A%20%20%20%20%3C%2Fscript%3E%0A%20%20%3C%2Fbody%3E%0A%3C%2Fhtml%3E" message="" highlight="" provider="manual"/]

replace tevreact with you went with. We’ll have a full example bundled with Webpack in the end

It works, You should be seeing something similar.

Updating the rendered JSX

If you’ve got time, try running the snippet below in a demo React app. We won’t have 2 app instances rendered on the dom but with our current implementation, our render method will render twice since it doesn’t know how to perform an update.

[pastacode lang="javascript" manual="const%20rootElement%20%3D%20document.getElementById(%22root%22)%0AReactDom.render(%3CComponent%20%2F%3E%2C%20rootElement)%20%2F%2F%20renders%20for%20the%20first%20time%0AReactDom.render(%3CComponent%20%2F%3E%2C%20rootElement)%20%2F%2F%20does%20the%20app%20component%20render%20twice" message="" highlight="" provider="manual"/]

We won’t have 2 app components rendered on the screen.

A duplicate call to render in your example index.html will not update the div but will append a new one.

It’s a bug that we will fix below.

In order to perform an update, we need to have a copy of the tree that has been rendered to the screen and the new tree with the updates so that we can make a comparison. We can do this by creating an object that we will call an instance.

[pastacode lang="javascript" manual="const%20instance%20%3D%20%7B%0A%20%20dom%3A%20HTMLElement%2C%20%2F%2F%20the%20rendered%20dom%20element%0A%20%20element%3A%20%7Btype%3A%20String%2C%20props%3A%20object%7D%2C%20%0A%20%20childInstances%3A%20Array%3Cinstance%3E%20%2F%2F%20array%20of%20child%20instances%0A%20%7D" message="" highlight="" provider="manual"/]

If the previous instance is null eg:(on initial render) we will create a new node

If the element.type of the previous instance is the same as the type of new instance, all we will do is just update the props of the element,

lastly, for now, If the type of the prev instance is not the same as the type of the new instance, we will replace the prev with the new instance

The above process is called reconciliation. It aims at reusing the dom nodes present as much as possible. Now that you have a grasp on the logic, keep in mind that we need to iterate the same process for the childInstances

Enough talk lets code.

We need a function inside reconciler.js that creates a new instance. It returns the instance object after an element is passed as an argument. We also need a reconcile function that will perform the reconciliation process described above. This will result in the render method offloading its functionality.

[pastacode lang="javascript" manual="import%20%7B%20updateDomProperties%20%7D%20from%20%22.%2Fdom-utils%22%3B%0Aimport%20%7B%20TEXT_ELEMENT%20%7D%20from%20%22.%2Felement%22%3B%0A%0Alet%20rootInstance%20%3D%20null%3B%20%2F%2F%20will%20keep%20the%20reference%20to%20the%20instance%20rendered%20on%20the%20dom%0A%0Aexport%20function%20render(element%2C%20parentDom)%20%7B%0A%20%20const%20prevInstance%20%3D%20rootInstance%3B%0A%20%20const%20nextInstance%20%3D%20reconcile(parentDom%2C%20prevInstance%2C%20element)%3B%0A%20%20rootInstance%20%3D%20nextInstance%3B%0A%7D%0A%0Afunction%20reconcile(parentDom%2C%20instance%2C%20element)%20%7B%0A%20%20if%20(instance%20%3D%3D%20null)%20%7B%0A%20%20%20%20%2F%2F%20initial%20render%0A%20%20%20%20const%20newInstance%20%3D%20instantiate(element)%3B%0A%20%20%20%20parentDom.appendChild(newInstance.dom)%3B%0A%20%20%20%20return%20newInstance%3B%0A%20%20%7D%20else%20if%20(element%20%3D%3D%20null)%20%7B%0A%20%20%20%20%2F**%0A%20%20%20%20%20*%20this%20section%20gets%20hit%20when%0A%20%20%20%20%20*%20a%20childElement%20was%20previously%20present%0A%20%20%20%20%20*%20but%20in%20the%20new%20element%20is%20not%20present%0A%20%20%20%20%20*%20for%20instance%20a%20todo%20item%20that%20has%20been%20deleted%0A%20%20%20%20%20*%20it%20was%20present%20at%20first%20but%20is%20now%20not%20present%0A%20%20%20%20%20*%2F%0A%20%20%20%20parentDom.removeChild(instance.dom)%3B%0A%20%20%20%20return%20null%3B%0A%20%20%7D%20else%20if%20(instance.element.type%20%3D%3D%3D%20element.type)%20%7B%0A%20%20%20%20%2F**%0A%20%20%20%20%20*%20if%20the%20types%20are%20the%20same%0A%20%20%20%20%20*%20eg%3A%20if%20prevType%20was%20%22input%22%20and%20current%20type%20is%20still%20%22input%22%0A%20%20%20%20%20*%20NB%3A%2F%2F%20we%20still%20havent%20updated%0A%20%20%20%20%20*%20the%20props%20of%20the%20node%20rendered%20in%20the%20dom%0A%20%20%20%20%20*%2F%0A%20%20%20%20instance.childInstances%20%3D%20reconcileChildren(instance%2C%20element)%3B%0A%20%20%20%20instance.element%20%3D%20element%3B%0A%20%20%20%20return%20instance%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%2F**%0A%20%20%20%20%20*%20if%20the%20type%20of%20the%20previous%20Instance%20is%20not%20the%0A%20%20%20%20%20*%20same%20as%20the%20type%20of%20the%20new%20element%0A%20%20%20%20%20*%20we%20replace%20the%20old%20with%20the%20new.%0A%20%20%20%20%20*%20eg%3A%20if%20we%20had%20an%20%22input%22%20and%20now%20have%20%22button%22%0A%20%20%20%20%20*%20we%20get%20rid%20of%20the%20input%20and%20replace%20it%20with%20the%20button%0A%20%20%20%20%20*%2F%0A%20%20%20%20const%20newInstance%20%3D%20instantiate(element)%3B%0A%20%20%20%20parentDom.replaceChild(newInstance.dom%2C%20instance.dom)%3B%0A%20%20%20%20return%20newInstance%3B%0A%20%20%7D%0A%7D%0A%0Afunction%20instantiate(element)%20%7B%0A%20%20const%20%7B%20type%2C%20props%20%7D%20%3D%20element%3B%0A%0A%20%20const%20isTextElement%20%3D%20type%20%3D%3D%3D%20TEXT_ELEMENT%3B%0A%20%20const%20dom%20%3D%20isTextElement%0A%20%20%20%20%3F%20document.createTextNode(%22%22)%0A%20%20%20%20%3A%20document.createElement(type)%3B%0A%0A%20%20updateDomProperties(dom%2C%20props)%3B%0A%0A%20%20%2F%2F%20Instantiate%20and%20append%20children%0A%20%20const%20childElements%20%3D%20props.children%20%7C%7C%20%5B%5D%3B%0A%20%20%2F%2F%20we%20are%20recursively%20calling%20instanciate%20on%20each%0A%20%20%2F%2F%20child%20element%0A%20%20const%20childInstances%20%3D%20childElements.map(instantiate)%3B%0A%20%20const%20childDoms%20%3D%20childInstances.map(childInstance%20%3D%3E%20childInstance.dom)%3B%0A%20%20childDoms.forEach(childDom%20%3D%3E%20dom.appendChild(childDom))%3B%0A%0A%20%20const%20instance%20%3D%20%7B%20dom%2C%20element%2C%20childInstances%20%7D%3B%0A%20%20return%20instance%3B%0A%7D%0A%0Afunction%20reconcileChildren(instance%2C%20element)%20%7B%0A%20%20const%20dom%20%3D%20instance.dom%3B%0A%20%20const%20childInstances%20%3D%20instance.childInstances%3B%0A%20%20const%20nextChildElements%20%3D%20element.props.children%20%7C%7C%20%5B%5D%3B%0A%20%20const%20newChildInstances%20%3D%20%5B%5D%3B%0A%20%20const%20count%20%3D%20Math.max(childInstances.length%2C%20nextChildElements.length)%3B%20%0A%20%20%0A%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20count%3B%20i%2B%2B)%20%7B%0A%20%20%20%20const%20childInstance%20%3D%20childInstances%5Bi%5D%3B%0A%20%20%20%20const%20childElement%20%3D%20nextChildElements%5Bi%5D%3B%0A%20%20%20%20%2F%2F%20the%20reconcile%20function%20has%20logic%20setup%20to%20handle%20the%20scenario%20when%20either%20%0A%20%20%20%20%2F%2F%20the%20child%20instance%20or%20the%20childElement%20is%20null%0A%20%20%20%20const%20newChildInstance%20%3D%20reconcile(dom%2C%20childInstance%2C%20childElement)%3B%0A%20%20%20%20newChildInstances.push(newChildInstance)%3B%0A%20%20%7D%0A%0A%20%20return%20newChildInstances.filter(instance%20%3D%3E%20instance%20!%3D%20null)%3B%0A%7D" message="" highlight="" provider="manual"/]

We also need to update the updateDomProperties function to remove the oldProps and apply the newProps

[pastacode lang="javascript" manual="export%20function%20updateDomProperties(dom%2C%20prevProps%2C%20nextProps)%20%7B%0A%20%20const%20isEvent%20%3D%20name%20%3D%3E%20name.startsWith(%22on%22)%3B%0A%20%20const%20isAttribute%20%3D%20name%20%3D%3E%20!isEvent(name)%20%26%26%20name%20!%3D%20%22children%22%3B%0A%0A%20%20%2F%2F%20Remove%20event%20listeners%0A%20%20Object.keys(prevProps)%0A%20%20%20%20.filter(isEvent)%0A%20%20%20%20.forEach(name%20%3D%3E%20%7B%0A%20%20%20%20%20%20const%20eventType%20%3D%20name.toLowerCase().substring(2)%3B%0A%20%20%20%20%20%20dom.removeEventListener(eventType%2C%20prevProps%5Bname%5D)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%2F%2F%20Remove%20attributes%0A%20%20Object.keys(prevProps)%0A%20%20%20%20.filter(isAttribute)%0A%20%20%20%20.forEach(name%20%3D%3E%20%7B%0A%20%20%20%20%20%20dom%5Bname%5D%20%3D%20null%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%2F%2F%20Set%20new%20attributes%0A%20%20Object.keys(nextProps)%0A%20%20%20%20.filter(isAttribute)%0A%20%20%20%20.forEach(name%20%3D%3E%20%7B%0A%20%20%20%20%20%20dom%5Bname%5D%20%3D%20nextProps%5Bname%5D%3B%0A%20%20%20%20%7D)%3B%0A%20%20%20%20%0A%20%20%2F%2F%20Set%20new%20eventListeners%0A%20%20Object.keys(nextProps)%0A%20%20%20%20.filter(isEvent)%0A%20%20%20%20.forEach(name%20%3D%3E%20%7B%0A%20%20%20%20%20%20const%20eventType%20%3D%20name.toLowerCase().substring(2)%3B%0A%20%20%20%20%20%20dom.addEventListener(eventType%2C%20nextProps%5Bname%5D)%3B%0A%20%20%20%20%7D)%3B%0A%7D%0A" message="" highlight="" provider="manual"/]

Let's update the functions that call updateDomProperties to provide the previous props and next props.

[pastacode lang="javascript" manual="function%20reconcile(parentDom%2C%20instance%2C%20element)%20%7B%0A%20%20%20%2F**%20code...%20*%2F%0A%20%20else%20if%20(instance.element.type%20%3D%3D%3D%20element.type)%20%7B%0A%20%20%20%20%2F%2F%20perform%20props%20update%20here%0A%20%20%20%20updateDomProperties(instance.dom%2C%20instance.element.props%2C%20element.props)%3B%0A%20%20%20%20instance.childInstances%20%3D%20reconcileChildren(instance%2C%20element)%3B%0A%20%20%20%20instance.element%20%3D%20element%3B%0A%20%20%20%20return%20instance%3B%0A%20%20%7D%20%0A%20%20%20%20%20%2F**%20code...%20*%2F%0A%7D%0A%0Afunction%20instantiate(element)%20%7B%0A%20%20const%20%7B%20type%2C%20props%20%7D%20%3D%20element%3B%0A%0A%20%20%2F**%20code...%20*%2F%0A%20%20const%20dom%20%3D%20isTextElement%0A%20%20%20%20%3F%20document.createTextNode(%22%22)%0A%20%20%20%20%3A%20document.createElement(type)%3B%0A%20%20%20%2F%2F%20apply%20new%20props%20and%20provide%20empty%20object%20as%20a%20prevObject%20since%20this%20is%20instanciation%0A%20%20updateDomProperties(dom%2C%20%7B%7D%2C%20props)%3B%0A%20%20%20%2F**%20code...%20*%2F%0A%20%20%0A%7D" message="" highlight="" provider="manual"/]

Build your clone again npm run build:all and reload your example app again with the 2 render calls still present.

We now have 1 app instance.

Classes and Custom JSX tags

We’ll look at class Components and custom JSX tags, we won’t be covering lifecycle methods in this tutorial.

We can do an optimization from here on out, in the previous examples, reconciliation happened for the entire virtual dom tree. With the introduction of classes, we will make reconciliation happen only for the component whose state has changed. Let's get to it. Create a component.jsfile in src

[pastacode lang="javascript" manual="export%20class%20Component%20%7B%0A%20%20constructor(props)%20%7B%0A%20%20%20%20this.props%20%3D%20props%3B%0A%20%20%20%20this.state%20%3D%20this.state%20%7C%7C%20%7B%7D%3B%0A%20%20%7D%0A%20%20setState(partialState)%20%7B%0A%20%20%20%20this.state%20%3D%20Object.assign(%7B%7D%2C%20this.state%2C%20partialState)%3B%0A%20%20%7D%0A%7D" message="" highlight="" provider="manual"/]

We need the component to maintain its own internal instance so that reconciliation can happen for this component alone.

[pastacode lang="javascript" manual="%2F%2F%20...code%0A%0Afunction%20createPublicInstance(element%2C%20internalInstance)%20%7B%0A%20%20const%20%7B%20type%2C%20props%20%7D%20%3D%20element%3B%20%0A%20%20const%20publicInstance%20%3D%20new%20type(props)%3B%20%2F%2F%20the%20type%20is%20a%20class%20so%20we%20use%20the%20*new*%20keyword%0A%20%20publicInstance.__internalInstance%20%3D%20internalInstance%3B%0A%20%20return%20publicInstance%3B%0A%7D" message="" highlight="" provider="manual"/]

A change needs to be made to the instantiate function, it needs to call createPublicInstance if the type is a class.

[pastacode lang="javascript" manual="function%20instantiate(element)%20%7B%0A%20%20const%20%7B%20type%2C%20props%20%7D%20%3D%20element%3B%0A%20%20const%20isDomElement%20%3D%20typeof%20type%20%3D%3D%3D%20%22string%22%3B%0A%0A%20%20if%20(isDomElement)%20%7B%0A%20%20%20%20%2F%2F%20Instantiate%20DOM%20element%0A%20%20%20%20const%20isTextElement%20%3D%20type%20%3D%3D%3D%20TEXT_ELEMENT%3B%0A%20%20%20%20const%20dom%20%3D%20isTextElement%0A%20%20%20%20%20%20%3F%20document.createTextNode(%22%22)%0A%20%20%20%20%20%20%3A%20document.createElement(type)%3B%0A%0A%20%20%20%20updateDomProperties(dom%2C%20%5B%5D%2C%20props)%3B%0A%0A%20%20%20%20const%20childElements%20%3D%20props.children%20%7C%7C%20%5B%5D%3B%0A%20%20%20%20const%20childInstances%20%3D%20childElements.map(instantiate)%3B%0A%20%20%20%20const%20childDoms%20%3D%20childInstances.map(childInstance%20%3D%3E%20childInstance.dom)%3B%0A%20%20%20%20childDoms.forEach(childDom%20%3D%3E%20dom.appendChild(childDom))%3B%0A%0A%20%20%20%20const%20instance%20%3D%20%7B%20dom%2C%20element%2C%20childInstances%20%7D%3B%0A%20%20%20%20return%20instance%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%2F%2F%20Instantiate%20component%20element%0A%20%20%20%20const%20instance%20%3D%20%7B%7D%3B%0A%20%20%20%20const%20publicInstance%20%3D%20createPublicInstance(element%2C%20instance)%3B%0A%20%20%20%20const%20childElement%20%3D%20publicInstance.render()%3B%20%2F%2F%20each%20class%20has%20a%20render%20method%0A%20%20%20%20%2F%2F%20if%20render%20is%20called%20it%20returns%20the%20child%0A%20%20%20%20const%20childInstance%20%3D%20instantiate(childElement)%3B%0A%20%20%20%20const%20dom%20%3D%20childInstance.dom%3B%0A%0A%20%20%20%20Object.assign(instance%2C%20%7B%20dom%2C%20element%2C%20childInstance%2C%20publicInstance%20%7D)%3B%0A%20%20%20%20return%20instance%3B%0A%20%20%7D%0A%7D" message="" highlight="" provider="manual"/]

Updating a class component in our case will happen when a call to setStateis made.

[pastacode lang="javascript" manual="import%20%7B%20reconcile%20%7D%20from%20%22.%2Freconciler%22%0A%0Aexport%20class%20Component%20%7B%0A%20%20constructor(props)%20%7B%0A%20%20%20%20this.props%20%3D%20props%3B%0A%20%20%20%20this.state%20%3D%20this.state%20%7C%7C%20%7B%7D%3B%0A%20%20%7D%0A%20%20setState(partialState)%20%7B%0A%20%20%20%20this.state%20%3D%20Object.assign(%7B%7D%2C%20this.state%2C%20partialState)%3B%0A%20%20%20%20updateInstance(this.__internalInstance)%3B%0A%20%20%7D%0A%7D%0A%0Afunction%20updateInstance(internalInstance)%20%7B%0A%20%20const%20parentDom%20%3D%20internalInstance.dom.parentNode%3B%0A%20%20const%20element%20%3D%20internalInstance.element%3B%0A%20%20reconcile(parentDom%2C%20internalInstance%2C%20element)%3B%0A%7D" message="" highlight="" provider="manual"/]

We now need to make sure that our reconcile function handles reconciliation for class components.

[pastacode lang="javascript" manual="export%20function%20reconcile(parentDom%2C%20instance%2C%20element)%20%7B%0A%20%20if%20(instance%20%3D%3D%20null)%20%7B%0A%20%20%20%20%2F%2F%20initial%20render%0A%20%20%20%20const%20newInstance%20%3D%20instantiate(element)%3B%0A%20%20%20%20parentDom.appendChild(newInstance.dom)%3B%0A%20%20%20%20return%20newInstance%3B%0A%20%20%7D%20else%20if%20(element%20%3D%3D%20null)%20%7B%0A%20%20%20%20%2F**%0A%20%20%20%20%20*%20this%20section%20gets%20hit%20when%0A%20%20%20%20%20*%20a%20childElement%20was%20previously%20present%0A%20%20%20%20%20*%20but%20in%20the%20new%20element%20is%20not%20present%0A%20%20%20%20%20*%20for%20instance%20a%20todo%20item%20that%20has%20been%20deleted%0A%20%20%20%20%20*%20it%20was%20present%20at%20first%20but%20is%20now%20not%20present%0A%20%20%20%20%20*%2F%0A%20%20%20%20parentDom.removeChild(instance.dom)%3B%0A%20%20%20%20return%20null%3B%0A%20%20%7D%20else%20if%20(instance.element.type%20!%3D%3D%20element.type)%20%7B%0A%20%20%20%20%2F**%0A%20%20%20%20%20*%20if%20the%20type%20of%20the%20previous%20Instance%20is%20not%20the%0A%20%20%20%20%20*%20same%20as%20the%20type%20of%20the%20new%20element%0A%20%20%20%20%20*%20we%20replace%20the%20old%20with%20the%20new.%0A%20%20%20%20%20*%20eg%3A%20if%20we%20had%20an%20%22input%22%20and%20now%20have%20%22button%22%0A%20%20%20%20%20*%20we%20get%20rid%20of%20the%20input%20and%20replace%20it%20with%20the%20button%0A%20%20%20%20%20*%2F%0A%20%20%20%20const%20newInstance%20%3D%20instantiate(element)%3B%0A%20%20%20%20parentDom.replaceChild(newInstance.dom%2C%20instance.dom)%3B%0A%20%20%20%20return%20newInstance%3B%0A%20%20%7D%20else%20if%20(typeof%20element.type%20%3D%3D%3D%20%22string%22)%20%7B%0A%20%20%20%20%2F**%0A%20%20%20%20%20*%20if%20the%20types%20are%20the%20same%20%26%20are%20HTMLElement%20types%0A%20%20%20%20%20*%20eg%3A%20if%20prevType%20was%20%22input%22%20and%20current%20type%20is%20still%20%22input%22%0A%20%20%20%20%20*%20NB%3A%2F%2F%20we%20still%20havent%20updated%0A%20%20%20%20%20*%20the%20props%20of%20the%20node%20rendered%20in%20the%20dom%0A%20%20%20%20%20*%2F%0A%20%20%20%20instance.childInstances%20%3D%20reconcileChildren(instance%2C%20element)%3B%0A%20%20%20%20instance.element%20%3D%20element%3B%0A%20%20%20%20return%20instance%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%2F%2FUpdate%20instance%0A%20%20%20%20instance.publicInstance.props%20%3D%20element.props%3B%0A%20%20%20%20const%20childElement%20%3D%20instance.publicInstance.render()%3B%0A%20%20%20%20const%20oldChildInstance%20%3D%20instance.childInstance%3B%0A%20%20%20%20const%20childInstance%20%3D%20reconcile(parentDom%2C%20oldChildInstance%2C%20childElement)%3B%0A%20%20%20%20instance.dom%20%3D%20childInstance.dom%3B%0A%20%20%20%20instance.childInstance%20%3D%20childInstance%3B%0A%20%20%20%20instance.element%20%3D%20element%3B%0A%20%20%20%20return%20instance%3B%0A%20%20%7D%0A%7D" message="" highlight="" provider="manual"/]

The end result means that reconciliation on class components starts at the parentNode of the child component and not the beginning of the v-domtree.

Import the Component class in tevreact just like the rest of the functions.

[pastacode lang="javascript" manual="import%20%7B%20render%20%7D%20from%20%22.%2Freconciler%22%3B%0Aimport%20%7B%20createElement%20%7D%20from%20%22.%2Felement%22%3B%0Aimport%20%7B%20Component%20%7D%20from%20%22.%2Fcomponent%22%3B%0Aexport%20%7B%20createElement%2C%20render%2C%20Component%20%7D%3B%0A%0Aexport%20default%20%7B%0A%20%20render%2C%0A%20%20createElement%2C%0A%20%20Component%0A%7D%3B" message="" highlight="" provider="manual"/]

Testing time

Run npm run build:all & create another HTML file in the examples directory class.html

[pastacode lang="javascript" manual="%3C!DOCTYPE%20html%3E%0A%3Chtml%20lang%3D%22en%22%3E%0A%20%20%3Chead%3E%0A%20%20%20%20%3Cmeta%20charset%3D%22UTF-8%22%20%2F%3E%0A%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1.0%22%20%2F%3E%0A%20%20%20%20%3Cmeta%20http-equiv%3D%22X-UA-Compatible%22%20content%3D%22ie%3Dedge%22%20%2F%3E%0A%20%20%20%20%3Ctitle%3EDidact%3C%2Ftitle%3E%0A%20%20%3C%2Fhead%3E%0A%20%20%3Cbody%3E%0A%20%20%20%20%3Cdiv%20id%3D%22app%22%3E%3C%2Fdiv%3E%0A%20%20%20%20%3C!--%20we%20need%20the%20babel%20standalone%20transpiler%20here%20since%20this%20is%20just%20a%20basic%20html%20page%20--%3E%0A%20%20%20%20%3Cscript%20src%3D%22https%3A%2F%2Funpkg.com%2Fbabel-standalone%407.0.0-beta.3%2Fbabel.min.js%22%3E%3C%2Fscript%3E%0A%20%20%20%20%3C!--%20load%20the%20umd%20version%20because%20it%20sets%20global.tevreact%20--%3E%0A%20%20%20%20%3Cscript%20src%3D%22..%2Fdist%2Ftevreact.umd.js%22%3E%3C%2Fscript%3E%0A%20%20%20%20%3C!--%20allow%20the%20react%20js%20preset%20--%3E%0A%20%20%20%20%3Cscript%20type%3D%22text%2Fbabel%22%20data-presets%3D%22react%22%3E%0A%20%20%20%20%20%20%2F**%20%40jsx%20tevreact.createElement%20*%2F%0A%20%20%20%20%20%20%2F**%20In%20the%20comment%20above%20we%20are%20telling%20babel%20which%20function%20it%20should%0A%20%20%20%20%20%20use%20the%20default%20is%20React.createElement%20and%20we%20want%20to%20use%0A%20%20%20%20%20%20our%20own%20createElement%20function*%2F%0A%20%20%20%20%20%20class%20App%20extends%20tevreact.Component%20%7B%0A%20%20%20%20%20%20%20%20constructor(props)%20%7B%0A%20%20%20%20%20%20%20%20%20%20super(props)%3B%0A%20%20%20%20%20%20%20%20%20%20this.state%20%3D%20%7B%20movieName%3A%20%22John%20Wick%22%20%7D%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20render()%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20%7B%20movieName%20%7D%20%3D%20this.state%3B%0A%20%20%20%20%20%20%20%20%20%20const%20%7B%20userName%20%7D%20%3D%20this.props%3B%0A%20%20%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cdiv%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ch1%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Hello%20%7BuserName%7D%2C%20Have%20you%20watched%20%7BmovieName%7D.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fh1%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20tevreact.render(%3CApp%20userName%3D%7B%22Tev%22%7D%20%2F%3E%2C%20document.getElementById(%22app%22))%3B%0A%20%20%20%20%3C%2Fscript%3E%0A%20%20%3C%2Fbody%3E%0A%3C%2Fhtml%3E" message="" highlight="" provider="manual"/]

A class component reading from props and state 🎉

If you got rid of all the comments, we have a pretty decent library size of fewer than 300 lines. Hit npm publish and you’ll have your package on the NPM registry.

Even though this works, this is how React worked prior to having Fiber. In part 2 of this tutorial, we will work on integrating the Fiber reconciler which is what React 16 > is using at the time of writing this post.

Related posts

The latest articles from Andela.

Visit our blog

Customer-obsessed? 4 Steps to improve your culture

If you get your team's culture right, you can create processes that will allow you to operationalize useful new technologies. Check out our 4 steps to transform your company culture.

How to Build a RAG-Powered LLM Chat App with ChromaDB and Python

Harness the power of retrieval augmented generation (RAG) and large language models (LLMs) to create a generative AI app. Andela community member Oladimeji Sowole explains how.

Navigating the future of work with generative AI and stellar UX design

In this Writer's Room blog, Carlos Tay discusses why ethical AI and user-centric design are essential in shaping a future where technology amplifies human potential.

We have a 96%+
talent match success rate.

The Andela Talent Operating Platform provides transparency to talent profiles and assessment before hiring. AI-driven algorithms match the right talent for the job.