How To Create Your Own React UI Framework in 5 mins
Some time ago we thought about why so many UI frameworks for the web? We have been in IT for a long time and we do not remember that UI libraries on other platforms were born and died at the same speed as on WEB.
Libraries for desktop OS, such as MFC, Qt, WPF, etc. – were monsters that developed over the years and did not have a large number of alternatives. The
- The web is not all that – frameworks are coming out almost every week, leaders are changing – why is this happening?
We think the main reason is that the complexity of writing UI libraries has dramatically decreased. Yes, in order to write a library that many will use – it still requires considerable time and expertise, but it takes quite a bit of time to write a prototype – which, being wrapped in a handy API, will be ready for use. If you are wondering how this can be done – read on.
Why should you read this article?
At one time, at SmartSpate was a series of articles – write X for 30 lines of code on js.
We thought – can we write our own React in a 30-line of code? Yes, for 30 lines we did not succeed, but the final result is quite commensurate with this figure.
- In general, the purpose of the article is purely educational. It can help a little deeper understand the principle of the UI framework based on a virtual home. In this article, we want to show how quite simply to make another UI Framework based on a virtual home.
In the beginning, we want to say what we understand by the UI framework – because many people have different opinions on this matter. For example, some people think that Angular and Ember are UI framework a React – this is just a library that allows you to make it easier to work with the view part of the application
Define the UI framework so it is a library that helps to create/ update/delete pages or individual elements in this sense a fairly wide selection of the wrapper over the DOM API can be a UI interface, the question is only in the abstraction (API) options that this library provides for manipulation with DOM and in the effectiveness of these manipulations
In the proposed formulation – React is quite the structure of the user interface.
- Well, let’s see how to write your own React. It is known that React uses the concept of a virtual DOM. In a simplified form, it consists in that the nodes of the real DOM are built in strict accordance with the nodes of the pre-built virtual DOM tree.
- Direct manipulation with the real DOM is not welcome, if you need to make changes to the real DOM, the change is made to the virtual DOM, then the new version of the virtual DOM is compared with the old one, changes are collected that need to be applied to the real DOM and they are applied thus minimizing the interaction with real DOM – which makes application work more optimal.
Because the virtual DOM tree is a regular java-script object – it’s quite easy for them to manipulate – to change/compare its nodes, by the word easily, we understand that the virtual assembly code is fairly simple and can be partially generated by the preprocessor from the declarative level of the higher JSX level.
Let’s start with JSX
This is an example of JSX code
const Component = () => ( <div className="main"> <input /> <button onClick={() => console.log('yo')}> Submit </button> </div> ) export default Component
We need to make sure that when we call the Component function, we create such a virtual DOM
const vdom = { type: 'div', props: { className: 'main' }, children: [ { type: 'input' }, { type: 'button', props: { onClick: () => console.log('yo') }, children: ['Submit'] } ] }
Of course, we will not write this conversion manually, we will use this plugin, the plugin is obsolete, but it is simple enough to help us understand how everything works. It uses jsx-transform, which converts JSX like this:
jsx.fromString('<h1>Hello World</h1>', { factory: 'h' }); // => 'h("h1", null, ["Hello World"])'
So, all we need is to implement the constructor of vdom nodes h – a function that will recursively create nodes of a virtual DOM in the case of a reactant this is handled by the function React.createElement. Below is a primitive implementation of such a function
export function h(type, props, ...stack) { const children = (stack || []).reduce(addChild, []) props = props || {} return typeof type === "string" ? { type, props, children } : type(props, children) } function addChild(acc, node) { if (Array.isArray(node)) { acc = node.reduce(addChild, acc) } else if (null == node || true === node || false === node) { } else { acc.push(typeof node === "number" ? node + "" : node) } return acc }
Of course, the recursion here slightly complicates the code, but we hope it is understandable, now with the help of this function we can compile it.
'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']}
and so for nodes of any nesting
Ok, now our Component function returns the vdom node.
Now there will be a difficult part, we need to write a patch function that takes the entry of the root DOM application, the old vdom, the new vdom – and updates the real DOM nodes according to the new vdom.
Perhaps you can write this code easier, but it worked out so we took as a basis the code from the package picodom
export function patch(parent, oldNode, newNode) { return patchElement(parent, parent.children[0], oldNode, newNode) } function patchElement(parent, element, oldNode, node, isSVG, nextSibling) { if (oldNode == null) { element = parent.insertBefore(createElement(node, isSVG), element) } else if (node.type != oldNode.type) { const oldElement = element element = parent.insertBefore(createElement(node, isSVG), oldElement) removeElement(parent, oldElement, oldNode) } else { updateElement(element, oldNode.props, node.props) isSVG = isSVG || node.type === "svg" let childNodes = [] ; (element.childNodes || []).forEach(element => childNodes.push(element)) let oldNodeIdex = 0 if (node.children && node.children.length > 0) { for (var i = 0; i < node.children.length; i++) { if (oldNode.children && oldNodeIdex <= oldNode.children.length && (node.children[i].type && node.children[i].type === oldNode.children[oldNodeIdex].type || (!node.children[i].type && node.children[i] === oldNode.children[oldNodeIdex])) ) { patchElement(element, childNodes[oldNodeIdex], oldNode.children[oldNodeIdex], node.children[i], isSVG) oldNodeIdex++ } else { let newChild = element.insertBefore( createElement(node.children[i], isSVG), childNodes[oldNodeIdex] ) patchElement(element, newChild, {}, node.children[i], isSVG) } } } for (var i = oldNodeIdex; i < childNodes.length; i++) { removeElement(element, childNodes[i], oldNode.children ? oldNode.children[i] || {} : {}) } } return element }
This naive implementation, it is terribly not optimal, does not take into account the identifiers of the elements (key, id) – to correctly update the necessary elements in the lists, but in primitive cases, it works norms.
Implementing the functions createElement updateElement removeElement I’m not here to bring it, it’s interesting, you can see the source code here.
There is a unique nuance – when the value properties for input elements are updated, the comparison should not be done with the old vnode but with the value attribute in the real house – this will prevent the active element from updating this property (since it has already been updated there) and will prevent problems with the cursor and excretion.
Well, that’s all, now we just need to collect these pieces together and write the UI Framework
- We will fit into 5 lines.
- As in React to assemble the application, we need 3 parameters
export function app (selector, view, initProps) {
selector – the root selector dom in which the application will be mounted (by default ‘body’)
the view is a function that constructs the root vnode
initProps – initial properties of the application - Take the root element in the DOM
const rootElement = document.querySelector (selector || ‘body’) - Now, collect vdom with initial properties
let node = view (initProps) - Mount the resulting vdom in the DOM as an old vdom, take null
patch (rootElement, null, node) - Return the function to update the application with new properties
return props => patch (rootElement, node, (node = view (props)))
The framework is ready!
‘Hello world’ on this Framework will look like this:
import { h, app } from "../src/index" function view(state) { return ( <div> <h2>{`Hello ${state}`}</h2> <input value={state} oninput={e => render(e.target.value)} /> </div> ) } const render = app('body', view, 'world')
This library, as well as React, supports component composition, addition, deletion of components at runtime so that it can be considered a full-fledged UI Framework. A slightly more complex example of use can be seen here ToDo example.
Of course, there are many things in this library: life cycle events (although they are not difficult to fasten, we also manage the creation/ updating/deletion of nodes), a separate update of child nodes by the type this.setState (for this you need to save links to DOM elements for each node vdom – this will slightly complicate the logic), the patchElement code is terribly suboptimal, it will not work well on a large number of elements, it does not track elements with an identifier, etc.
In any case, the library was developed for educational purposes – do not use it in production.
For this article, we were inspired by the excellent Hyperapp library, part of the code is taken from there.
Successful coding!