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
Top comments (1)
Good work. I tried it too, but it didn't work.
codepen