What I cannot create, I do not understand - Richard Feynman
When I started learning React I felt whatever it did was pure magic, then I started to wonder what were the actual ingredients of this magic. I started to freak out when I realized whatever React does is very simple and we can build it with few lines of JavaScript if we are not betting on it for our next big startup. This is what has motivated me to write this article and hopefully after reading this, you will also feel the same way.
What features we will build?
JSX
This is most obvious as we are building a React clone. We will also add event binding.
Functional components
We will also support functional components with props.
Class components with state
We will support the Class component with props and state to update our component.
Lifecycle Hooks
For the sake of simplicity, we will implement only the componentDidMount() lifecycle hook.
What we won't be building?
Virtual DOM
Yes again for the sake of simplicity we will not implement our own virtual DOM at least in this article and we will use an off the shelf virtual DOM called snabbdom and the fun fact is that it is the virtual DOM used by Vue.js. You can read more about it here.
A virtual DOM library with focus on simplicity, modularity, powerful features and performance.
React Hooks
Some might get disappointed upon reading this but hey we don't want to chew more than we can so let us build the basic things first and we can always add on top of it. I also plan to write separate articles on implementing our own React hooks and virtual DOM on top of whatever we build here.
Debuggability
This is one of the key parts which adds a level of complexity to any library or framework and since we are just doing this for fun we can safely ignore the debuggability features that React provides like the dev tools and profiler.
Performance and portability
We won't be very much concerned about how efficient or how blazing fast our library is, we just want to build something that works. Let us also not put us through a hard time of making sure that it works on all the browsers in the market, it is fine if we can make it work at least on a few of the modern browsers.
Let us get our hand dirty
Before we get started we need a scaffold with support for ES6, auto-reloading but no worries I have already set up a very basic Webpack scaffold with just that, you can clone and set it up from the link below.
This is a very basic webpack setup with just ES6 support and everything else is left upto your creativity
JSX
JSX is an open standard and it is not restricted to React in any way so we can use it without React and it is pretty easier than you might think. To understand how we can exploit JSX for our library let us see what happens behind the curtains when we use JSX.
constApp=(<div><h1className="primary">QndReact is Quick and dirty react</h1><p>It is about building your own React in 90 lines of JavsScript</p></div>);// The above jsx gets converted into/**
* React.createElement(type, attributes, children)
* props: it is the type of the element ie. h1 for <h1></h1>
* attributes: it is an object containing key value pair of props passed to the element
* children: it the array of child elements inside it
*/varApp=React.createElement("div",null,React.createElement("h1",{className:"primary"},"QndReact is Quick and dirty react"),React.createElement("p",null,"It is about building your own React in 90 lines of JavsScript"));
As you can see every JSX element gets transformed into React.createElement(โฆ) function call by the @babel/plugin-transform-react-jsx plugin, you can play more with JSX to JavaScript transformation here
For the above transformation to happen React needs to be in your scope while writing JSX, this is the reason why you get weird errors when you try to write JSX without React in your scope.
Let us first install the @babel/plugin-transform-react-jsx plugin
npm install @babel/plugin-transform-react-jsx
Add the below config to the .babelrc file
{"plugins":[["@babel/plugin-transform-react-jsx",{"pragma":"QndReact.createElement",// default pragma is React.createElement"throwIfNamespace":false// defaults to true}]]}
After this whenever Babel sees JSX it will call QndReact.createElement(โฆ) but we have not yet defined that function so let us add it in src/qnd-react.js
// file: src/qnd-react.jsconstcreateElement=(type,props={},...children)=>{console.log(type,props,children);};// to be exported like React.createElementconstQndReact={createElement};exportdefaultQndReact;
We have console logged type, props, children to understand what are being passed to us. To test whether our transformation of JSX is working let us write some JSX in src/index.js
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom"./qnd-react";constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p></div>);
Now you should see something like this in your console.
From the above information, we can create our own internal virtual DOM node using snabbdom which we can then use for our reconciliation process. Let us first install snabbdom using command below.
npm install snabbdom
Let us now create and return our virtual DOM node whenever QndReact.createElement(...) is called
// file: src/qnd-react.jsimport{h}from'snabbdom';constcreateElement=(type,props={},...children)=>{returnh(type,{props},children);};// to be exported like React.createElementconstQndReact={createElement};exportdefaultQndReact;
Great now we can parse JSX and create our own virtual DOM nodes but still, we are not able to render it to the browser. To do so let us add a render function in src/qnd-react-dom.js
// file: src/qnd-react-dom.js// React.render(<App />, document.getElementById('root'));// el -> <App />// rootDomElement -> document.getElementById('root')constrender=(el,rootDomElement)=>{// logic to put el into the rootDomElement}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
Rather than us handling the heavy lifting of putting the elements on to the DOM let us make snabbdom do it, for that we need to first initialize snabbdom with required modules. Modules in snabbdom are kind of plugins that allow snabbdom to do more only if it is required.
// file: src/qnd-react-dom.jsimport*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';// propsModule -> this helps in patching text attributesconstreconcile=snabbdom.init([propsModule]);// React.render(<App />, document.getElementById('root'));// el -> <App />// rootDomElement -> document.getElementById('root')constrender=(el,rootDomElement)=>{// logic to put el into the rootDomElementreconcile(rootDomElement,el);}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
Let us use our brand new render function to do some magic in src/index.js
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom'./qnd-react';importQndReactDomfrom'./qnd-react-dom';constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p></div>);QndReactDom.render(App,document.getElementById('root'));
Voila! we should see our JSX being rendered to the screen.
Wait we have one little problem when we call render function twice we will get some weird error in the console, the reason behind that is only the first time we can call the reconcile method on a real DOM node followed by that we should call it with the virtual DOM node it returns when called for the first time.
// file: src/qnd-react-dom.jsimport*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';// propsModule -> this helps in patching text attributesconstreconcile=snabbdom.init([propsModule]);// we need to maintain the latest rootVNode returned by renderletrootVNode;// React.render(<App />, document.getElementById('root'));// el -> <App />// rootDomElement -> document.getElementById('root')constrender=(el,rootDomElement)=>{// logic to put el into the rootDomElement// ie. QndReactDom.render(<App />, document.getElementById('root'));// happens when we call render for the first timeif(rootVNode==null){rootVNode=rootDomElement;}// remember the VNode that reconcile returnsrootVNode=reconcile(rootVNode,el);}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
Sweet we have a working JSX rendering in our app, let us now move to render a functional component rather than some plain HTML.
Let us add a functional component called Greeting to src/index.js as shown below.
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom"./qnd-react";importQndReactDomfrom"./qnd-react-dom";// functional component to welcome someoneconstGreeting=({name})=><p>Welcome {name}!</p>;constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p><Greetingname={"Ameer Jhan"}/></div>);QndReactDom.render(App,document.getElementById("root"));
Ah oh! we get some error in the console as shown below.
Let us see what is going on by placing a console.log in the QndReact.createElement(...) method
We can see that the type that is being passed is a JavaScript function whenever there is a functional component. If we call that function we will get the HTML result that the component wishes to render.
Now we need to check whether that type of the type argument is function if so we call that function as type(props) if not we handle it as normal HTML elements.
// file: src/qnd-react.jsimport{h}from'snabbdom';constcreateElement=(type,props={},...children)=>{// if type is a function then call it and return it's valueif (typeof (type)=='function'){returntype(props);}returnh(type,{props},children);};// to be exported like React.createElementconstQndReact={createElement};exportdefaultQndReact;
Hurray! we have our functional component working now.
Great we have done a lot, let us take a deep breath and a cup of coffee with a pat on our back as we are almost done implementing React, we have one more piece to complete the puzzle Class components.
We will create our Component base class in src/qnd-react.js as shown below.
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
Cool let us write our first Counter class component in src/counter.js
Yes I know we have not yet implemented any logic for our counter but don't worry we will add those moving parts once we get our state management system up and running. Let us now try to render it in our src/index.js
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom"./qnd-react";importQndReactDomfrom"./qnd-react-dom";importCounterfrom"./counter";// functional component to welcome someoneconstGreeting=({name})=><p>Welcome {name}!</p>;constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p><Greetingname={"Ameer Jhan"}/><Counter/></div>);QndReactDom.render(App,document.getElementById("root"));
As expected we have an error in the console ๐ as shown below.
Does the above error looks familiar, you might get the above error in React when you try to use a class component without inheriting from React.Component class. To know why this is happening let us add a console.log in React.createElement(...) as shown below.
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{console.log(typeof (type),type);// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};...
Now peep into the console to see what is being logged.
You can see that the type of Counter is also a function, this is because at the end of the day Babel will be converting ES6 class into plain JavaScript function, then how are we going to handle the Class component case. Well, we can add a static property to our Component base class which we can then use to check whether type argument being passed is a Class. This is the same way React handles it, you can read Dan's blog on it here
// file: src/qnd-react.jsimport{h}from"snabbdom";...// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
Let us now add some code to handle Class component in our QndReact.createElement(...)
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a Class then// 1. create a instance of the Class// 2. call the render method on the Class instanceif (type.prototype&&type.prototype.isQndReactClassComponent){constcomponentInstance=newtype(props);returncomponentInstance.render();}// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
Hurray! we have Class component rendering something to the browser
Phew! let us move on to adding state to our Class component, before that it is important to understand that the responsibility of how to update the DOM whenever you call this.setState({...}) lies with react-dom package rather than React. This is to keep React's core parts like the Component class decoupled from the platform which in turn promotes high code reusability i.e in React native also you can use the same Component class while react-native package takes care of how to update the mobile UI. You might be now asking yourself how would React know what to do when this.setState({...}) is called, the answer is react-dom communicates it with React by setting an __updater property on React. Dan has an excellent article about this too which you can read over here. Let us now make QndReactDom to add a __updater property to QndReact
// file: src/qnd-react-dom.jsimportQndReactfrom'./qnd-react';import*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';...// QndReactDom telling React how to update DOMQndReact.__updater=()=>{// logic on how to update the DOM when you call this.setState}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
Whenever we call this.setState({...}) we need to compare the oldVNode of the component and the newVNode of the component generated by calling render function on the component, for this purpose of comparison let us add a __vNode property on the Class component to maintain the current VNode instance of the component.
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a Class then// 1. create a instance of the Class// 2. call the render method on the Class instanceif (type.prototype&&type.prototype.isQndReactClassComponent){constcomponentInstance=newtype(props);// remember the current vNode instancecomponentInstance.__vNode=componentInstance.render();returncomponentInstance.__vNode;}// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
Let us now implement our setState function on our Component base class
// file: src/qnd-react.jsimport{h}from"snabbdom";...// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){// update the state by adding the partial statethis.state={...this.state,...partialState}// call the __updater function that QndReactDom gaveQndReact.__updater(this);}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
Cool let us now handle the __updater function in QndReactDom
// file: src/qnd-react-dom.jsimportQndReactfrom'./qnd-react';import*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';...// QndReactDom telling React how to update DOMQndReact.__updater=(componentInstance)=>{// logic on how to update the DOM when you call this.setState// get the oldVNode stored in __vNodeconstoldVNode=componentInstance.__vNode;// find the updated DOM node by calling the render methodconstnewVNode=componentInstance.render();// update the __vNode property with updated __vNodecomponentInstance.__vNode=reconcile(oldVNode,newVNode);}...exportdefaultQndReactDom;
Awesome let us now check whether our setState implementation is working by adding state to our Counter Component
importQndReactfrom'./qnd-react';exportdefaultclassCounterextendsQndReact.Component{constructor(props){super(props);this.state={count:0}// update the count every secondsetInterval(()=>{this.setState({count:this.state.count+1})},1000);}componentDidMount(){console.log('Component mounted');}render(){return<p>Count: {this.state.count}</p>}}
Great, we have our Counter component working as expected.
Let us add the ComponentDidMount life cycle hook. Snabbdom provides hooks by which we can find whether a virtual DOM node was added, destroyed or updated on the actual DOM, you can read more about it here
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a Class then// 1. create a instance of the Class// 2. call the render method on the Class instanceif (type.prototype&&type.prototype.isQndReactClassComponent){constcomponentInstance=newtype(props);// remember the current vNode instancecomponentInstance.__vNode=componentInstance.render();// add hook to snabbdom virtual node to know whether it was added to the actual DOMcomponentInstance.__vNode.data.hook={create:()=>{componentInstance.componentDidMount()}}returncomponentInstance.__vNode;}// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};...exportdefaultQndReact;
Wonderful we have completed the implementation of Class component with componentDidMount life cycle hook support.
Let us finish things off by adding event binding support, to do that let us update our Counter component by adding a button called increment and incrementing the counter only when the button is clicked. Please beware that we are following the usual JavaScript based event naming convention rather than React based naming convention i.e for double click event use onDblClick and not onDoubleClick.
The above component is not going to work as we have not told our VDom how to handle it. First, let us add the event listener module to Snabdom
// file: src/qnd-react-dom.jsimport*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';importeventlistenersModulefrom'snabbdom/modules/eventlisteners';importQndReactfrom'./qnd-react';// propsModule -> this helps in patching text attributes// eventlistenersModule -> this helps in patching event attributesconstreconcile=snabbdom.init([propsModule,eventlistenersModule]);...
Snabdom wants the text attributes and event attributes as two seperate objects so let us do that
// file: src/qnd-react.jsimport{h}from'snabbdom';constcreateElement=(type,props={},...children)=>{...props=props||{};letdataProps={};leteventProps={};// This is to seperate out the text attributes and event listener attributesfor(letpropKeyinprops){// event props always startwith on eg. onClick, onDblClick etc.if (propKey.startsWith('on')){// onClick -> clickconstevent=propKey.substring(2).toLowerCase();eventProps[event]=props[propKey];}else{dataProps[propKey]=props[propKey];}}// props -> snabbdom's internal text attributes// on -> snabbdom's internal event listeners attributesreturnh(type,{props:dataProps,on:eventProps},children);};...// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
The counter component will now increment whenever the button is clicked.
Awesome we have finally reached the end of our quick and dirty implementation of React yet we still can't render lists and I want to give it to you as a fun little task. I would suggest you to try rendering a list in src/index.js and then debug QndReact.createElement(...) method to find out what is going wrong.
Thanks for sticking around with me and hopefully you enjoyed building your own React and also learned how React works while doing so. If you are stuck at any place feel free to refer the code in the repo that I have shared below.