KReact: Building your own React & Virtual DOM
Kamesh Sethupathi
Posted on October 24, 2021
In this blog we will build a library similar to react and understand how react works internally. This blog is written in a way that even if you don't know react or want to learn the internal working of react, you would definitely find it useful.
Link to the GitHub Repository: https://github.com/git-kamesh/kReact
Reach me
- 💬 Ask me about Frontend and Backend Technologies
- 📫 How to reach me: Twitter @kamesh_koops
[REQ] Requirements & Use-cases : What we want to build?
- [COMPONENT] Able to split complicated UI's into smaller reuseable components (Button, DropDown, ImageSlider, etc ).
-
[JSX] Able to write HTML template in JavaScript itself. Lets call this
JSX
. - [LOGIC] JSX should allows to embed JavaScript expressions and logically controlled .
-
[PROPS] Able to pass data/information to component from outside, lets call this
props
. -
[STATE] Component can have its own data/information without passing to it from outside, lets call this
state
. -
[RE-RENDER] Whenever
props
orstate
gets modified, the changes should be reflected to the UI automatically. Lets call thisre-render
. -
[PREVENT] Should be explicitly able to prevent
re-render
whenever required. -
[LIFE-CYCLE] Know components life-cycle events like,
- [1] before component mounted into DOM (birth of component)
- [2] after component mounted into DOM
- [3] component's props gets changed
- [4] before component is updated
- [5] after component is updated
- [6] before component is unmounted from the DOM (death of component)
- [DOM-OPERATION] Should handle DOM operations itself.
-
[DIFF] On re-render should find difference between the old DOM and new Virtual DOM, and only update that part to DOM. Lets call this
patch
.
Lets understand
- Before jumping, we have to understand few things/concepts like React, DOM, Virtual DOM, JSX, Components, State, JSX Transpiler, Reconsiliation.
- Don't know? don't worry we will see it here itself.
React
- React is a component based UI library.
- Component can be anything like Button, DropDown, ImageSlider, PasswordInput, etc.
- Here, components are building blocks of UI and will respond to data change.
- Components allows reusability thus providing development speed, consistancy, seperation of concern, easy to maintain and unit testable.
- Also alows both,
- Building an entire application (or)
- Part of an application/Feature.
DOM
- DOM stands for Document Object Model
- Its an object representation of the parsed HTML document
- We could update the dom through DOM APIs (e.g: )
- When an DOM object is updated, browser run two expensive operations
- Reflow - Calculates dimension and position of every element and its children.
- Repaint - Determines visual changes (like color, opacity, visibility) and applies them.
Virtual DOM
- Virtual DOM is nothing but an lightweight in-memory javascript object representation of the actual DOM.
- It basically mimics as an actual DOM.
JSX
const element = <h1 className="clrR">Hello, world!</h1>;
- The above tag syntax is neither a string nor HTML. It is JSX.
- JSX stands for JavaScript XML. It is used to define our virtual DOM.
- Just like HTML used for buildinng actual DOM, JSX is used for building virtual DOM.
- JSX in most simple word is how React allows us to write HTML in JavaScript.
- JSX is a syntax extension for JavaScript and it is not valid JavaScript, web browsers cant read it directly.
- So, if JavaScript files contains JSX, that that file will have to be transpiled. That means that before the file gets to the web browser, a JSX compiler will translate any JSX into regular JavaScript.
- After compilation, JSX expressions become regular JavaScript function calls and evaluate to JavaScript objects.
- The above JSX example will be compiled smiliar to below.
React.createElement('h1', { className: 'clrR'}, 'Hello, world!');
Components
Reconsiliation
- Whenever component's state or props gets updated, the component gets re-rendered and builds a new virtual DOM.
- Then react runs the diffing algorithm to calculate what changes should be applied to real DOM. This process is know as reconsiliation.
Terms to understand
- Rendering: Process of converting virtual dom into dom
- Mounting: Process of injecting rendered dom into target dom
- Patching: Process of comparing the virtual dom and actual dom, updating the nodes which are changed
Theories over lets play with code 😍
Rendering Logic
render(vnode, parent):
IF vnode IS class component:
CREATE NEW INSTANCE FOR component --> componentInstance
GENERATE component VNODE BY INVOKING componentInstance.render() --> VNode
RENDER VNODE BY PASSING VNODE INTO render FUNCTION --> DOMNode
ELSE IF vnode IS function component:
GENERATE VNODE BY EXECUTING vnode --> VNODE
RENDER VNODE BY PASSING VNODE INTO render FUNCTION --> DOMNode
ELSE IF vnode IS OBJECT:
CONVERT vnode OBJECT INTO DOMNode
RECURSIVELY APPLY render FUNCTION on vnode children
ASSIGN PROPS AS DOMNode attributes
MOUNT DOMNode ON parent
RETURN DOMNode
Patching Logic
patch(dom, vnode, parent):
IF dom AND vnode DIFFED:
RENDER vnode --> DOMNode
REPLACE dom WITH DOMNode
Full source code
Link to the GitHub Repository: https://github.com/git-kamesh/kReact
Follow me on twitter: @kamesh_koops
export class Component {
constructor( props = {}) {
this.props = props;
this.state = null;
}
setState(nextState) {
const isCompat = isObject(this.state) && isObject(nextState);
const commitState = ()=> this.state = isCompat? Object.assign({}, this.state, nextState) : nextState;
const prevState = isObject(this.state)? Object.assign({}, this.state) : this.state;
if( runHook(this, 'shouldComponentUpdate') && this.base ) {
runHook(this, 'componentWillUpdate', this.props, nextState);
commitState();
patch(this.base, this.render());
runHook(this, 'componentDidUpdate', this.props, prevState);
} else commitState();
}
static render(vnode, parent) {
if( isClassComponent(vnode) ) {
let instance = new vnode.type( combineChildrenWithProps( vnode ) );
runHook(instance, 'componentWillMount');
instance.base = render( instance.render(), parent);
instance.base.instance = instance;
runHook(instance, 'componentDidMount');
return instance.base;
} else return render( vnode.type(combineChildrenWithProps( vnode )), parent );
}
static patch(dom, vnode, parent=dom.parentNode) {
if (dom.instance && dom.instance.constructor == vnode.type) {
runHook(dom.instance, 'componentWillReceiveProps', combineChildrenWithProps( vnode ) );
dom.instance.props = combineChildrenWithProps( vnode );
return patch(dom, dom.instance.render(), parent);
} else if ( isClassComponent(vnode.type) ) {
const newdom = Component.render(vnode, parent);
return parent ? (replace(newdom, dom, parent) && newdom) : (newdom);
} else if ( !isClassComponent(vnode.type) ) return patch(dom, vnode.type( combineChildrenWithProps( vnode ) ), parent);
}
}
export const createElement = (type, props, ...children ) => ({ type, props: props || {}, children });
export function render(vnode, parent) {
if( isObject(vnode) ) {
let dom = isFunction(vnode.type) ? Component.render(vnode, parent) : document.createElement( vnode.type );
vnode.children.flat(1).map((child)=> render(child, dom));
!isFunction(vnode.type) && Object.keys(vnode.props).map((key)=> setAttribute(dom, key, vnode.props[key]));
return mount( dom, parent );
} else return mount( document.createTextNode(vnode || ''), parent );
}
function patch(dom, vnode, parent=dom.parentNode) {
if( isObject(vnode) ) {
if( isTextNode(dom) ) return replace( render(vnode, parent), dom, parent );
else if( isFunction(vnode.type) ) return Component.patch( dom, vnode, parent);
else {
let dom_map = Array.from(dom.childNodes) // Build a key value map to identify dom-node to its equivalent vnode
.reduce((prev, node, idx)=> ({...prev, [node._idx || `__${idx}`]: node}), {});
vnode.children.flat(1).map((child, idx)=> {
let key = (child.props && child.props.key) || `__${idx}`;
mount( dom_map[key]? patch(dom_map[key], child, dom) : render(child, dom) );
delete dom_map[key]; // marks dom-vnode pair available by removing from map
});
Object.values(dom_map).forEach(element => { // Unmount DOM nodes which are missing in the latest vnodes
runHook( element.instance, 'componentWillUnmount');
element.remove();
});
!isFunction(vnode.type) && Object.keys(vnode.props).map((key)=> setAttribute(dom, key, vnode.props[key]));
}
}
else if( isTextNode(dom) && dom.textContent != vnode ) return replace( render(vnode, parent), dom, parent );
}
function setAttribute(dom, key, value) {
if( key.startsWith('on') && isFunction(value) ) delegateEvent(dom, key, value);
else if( key == 'ref' && isFunction( value ) ) value( dom );
else if( ['checked', 'value', 'className', 'key'].includes(key) ) dom[key=='key'? '_idx' :key] = value;
else dom.setAttribute(key, value);
}
// Utils
const isFunction = ( node ) => typeof node == 'function';
const isObject = ( node ) => typeof node == 'object';
const isTextNode = ( node ) => node.nodeType == 3;
const replace = (el, dom, parent)=> (parent && parent.replaceChild(el, dom) && el);
const mount = (el, parent)=> parent? parent.appendChild( el ) : el;
const isClassComponent = ( node ) => Component.isPrototypeOf( node.type );
const runHook = (instance, hook, ...args) => isFunction(instance && instance[hook]) ? instance[hook]( ...args) : true;
const delegateEvent = (dom, event, handler)=> {
event = event.slice(2).toLowerCase();
dom._evnt = dom._evnt || {};
dom.removeEventListener(event, dom._evnt[ event ]);
dom.addEventListener(event, dom._evnt[ event ] = handler);
}
const combineChildrenWithProps = ({ props, children })=> Object.assign({}, props, { children });
Reach me
- 💬 Ask me about Frontend and Backend Technologies
- 📫 How to reach me: Twitter @kamesh_koops
💖 💪 🙅 🚩
Kamesh Sethupathi
Posted on October 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.