DEV Community

Kamesh Sethupathi
Kamesh Sethupathi

Posted on • Edited on

KReact: Building your own React & Virtual DOM

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

[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 or state gets modified, the changes should be reflected to the UI automatically. Lets call this re-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>;
Enter fullscreen mode Exit fullscreen mode
  • 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!');
Enter fullscreen mode Exit fullscreen mode

Components

Component Fn

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
Enter fullscreen mode Exit fullscreen mode

Patching Logic

patch(dom, vnode, parent):
     IF dom AND vnode DIFFED:
         RENDER vnode --> DOMNode
         REPLACE dom WITH DOMNode
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

Reach me

Top comments (1)

Collapse
 
temaovchinnikov profile image
TemaOvchinnikov

Good work. I tried it too, but it didn't work.
codepen