This is a special article which actually my story about my journey to choose the right UI framework/library for my new blog site, there are many web front-end UI frameworks and libraries available, in this journey I only use ReactJS, PreactJS, and HyperScript, this story is not giving a detail comparison between these frameworks/libraries because I have not use them to their fullest features, this story is about what I have tried and how I use each of them to make the UI for my blog site, the word 'I' is more frequently used than 'you' or 'we'.
To be honest this is a surprising journey for my self and I have thought a lot about how to write this surprising journey to just one article, where should I start? how to end it? should I write this story jumping back and forth (flashback) like recent Marvel movies? (^.^), at the end I concluded there is no perfect way but only perfect timing, so I just write this story from start to finish in a sequence of my real journey, I tried to make this article as short as I could while keeping the important detail and not skipping too much, also will share some of my thoughts along the way and why I did what I did.
If you are really interested in reading this rather long story then before you continue reading please get your favorite drink (a cup of hot coffee or hot-choco or fruit juice, cold spirit, etc.) and come back to enjoy this journey story.
A few months ago, I started to create new blog site quick.work, its main home page has a listing of articles, originally I use my server (written in PHP) to fully render the main page and other pages using traditional multi-pages application (MPA) web, the articles count is growing and even though the count is not a lot now but definitely will increase because I intend to write article regularly to share my experiences until unforeseen time.
The problem I was facing is sending all the article headline list fully rendered in the server (include header, footer and content which has many HTML tags) is causing my small PHP server a high load burden, opening the website is rather slow, so then I knew that the first thing I need to do is to convert the traditional MPA to SPA (Single Page Application) web, the conversion is simple and I have written other article about it (if you are interested then you could read my other SPA article), conversion from MPA to SPA means need to create some APIs and use them to get the JSON data then the JSON data need to be rendered to the screen in client side, in this article I will skip about how to make the API, (there are some tips and tricks about designing fast APIs which I will write for a future article), this article will just focus on the making of the UI (User Interface) for quick.work, below is the partial sample of JSON data which I will use throughout this article.
// sample JSON data var jsonData = { blogs: [ { "id": "10", "title": "Prime Number Obsession", "introduction": "Small prime numbers generator and factoring", "writeDT": "2019-03-19", "updateDT": "2019-06-12" }, { "id": "24", "title": "Can we implement faster sorting algorithm for JavaScript?", "introduction": "Faster JS Array.sort() alternative", "writeDT": "2019-05-15", "updateDT": "2019-06-11" }, { "id": "22", "title": "What is Single Page Application (SPA)?", "introduction": "It is easy to create SPA web with only vanilla JavaScript", "writeDT": "2019-05-08", "updateDT": "2019-06-10" } ] };
The first trial in this journey is to render the article-headline-list in the main home page, I use simple vanilla JavaScript below, just a note for the first-time reader to see my coding style please note that I purposely wrote long verbose comments and long function name to remind my self in the future and let readers easily understand.
// vanilla JavaScript function renderArticleHeadlineList(json, containerId) { var blogCount = json && json.blogs && Array.isArray(json.blogs) ? json.blogs.length : 0; if(1 > blogCount) { return; } var container = document.getElementById(containerId); if(!container) { return; } // articles container var html = '<div class="articles">'; json.blogs.forEach(blog => { // single article container html += '<div class="box">'; // article link html += '<a href="#view-blog?id=' + blog.id + '" title="' + blog.title + '">'; // image html += '<img src="/thumbnail/thumbnail_' + blog.id + '.jpg?update=' + (new Date(blog.updateDT).getTime()) + '" alt="' + blog.title + '"/>'; // title html += '<h4>' + blog.title + '</h4>'; // dates html += '<div class="article-dates">'; html += '<span>' + blog.writeDT + '</span>'; html += '<span>' + blog.updateDT + '</span>'; html += '</div>'; html += '</a>'; // close single article container html += '</div>'; }); html += '</div>'; // close articles container // render the list by appending at the last part of container. container.innerHTML += html; // for debugging in console return html; }
Above JavaScript is simple to understand, sort code and fast (no DOM element handling, only string operation), it works and has no problem, but other front-end developers may disagree and some may even scream 'OMG ..', it is rather lame to see that code in 2019 .. LOL, the code works but not friendly and not easy to upgrade for more additional features in the future such as handling onMouseOver, onClick or other additional attributes, the main problem of the code is using many hard-coded elements, very error-prone such as missing end-tag, spelling error, etc.
The Second trial in this journey is to find a more elegant way, introducing the rarely use HTML5 <template> tag, I created 2 templates, articlesContainerId is the container and articleHeadlineId is the content to be repeated for all article headlines.
<template id="articlesContainerId"> <div class="articles"> </div> </template> <template id="articleHeadlineId"> <div class="box"> <a> <img /> <h4></h4> <div class="article-dates"> <span></span> <span></span> </div> </a> </div> </template>
The JavaScript to render the same JSON data and using the template
function getVirtualDOMFromTemplate(templateId) { var template = document.querySelector(templateId.startsWith('#') ? templateId : '#' + templateId); return document.importNode(template.content, true); } function renderArticleHeadlineListUsingTemplate(json, containerId) { var blogCount = json && json.blogs && Array.isArray(json.blogs) ? json.blogs.length : 0; if(1 > blogCount) { return; } var container = document.getElementById(containerId); if(!container) { return; } var articlesContainerVirtualDOM = getVirtualDOMFromTemplate('articlesContainerId'); var anArticleVirtualDOM, html = ''; json.blogs.forEach(blog => { // clone template content anArticleVirtualDOM = getVirtualDOMFromTemplate('articleHeadlineId'); // article link var a = anArticleVirtualDOM.querySelector('a'); a.href = '#view-blog?id=' + blog.id; a.title = blog.title; // image var img = anArticleVirtualDOM.querySelector('img'); img.src = '/thumbnail/thumbnail_' + blog.id + '.jpg?update=' + (new Date(blog.updateDT).getTime()); img.alt = blog.title; // title var title = anArticleVirtualDOM.querySelector('h4'); title.innerHTML = blog.title; // dates var dates = anArticleVirtualDOM.querySelectorAll('span'); dates[0].innerHTML = blog.writeDT; dates[1].innerHTML = blog.updateDT; // append a single articles into articles container articlesContainerVirtualDOM.querySelector('div').appendChild(anArticleVirtualDOM); }); // append all articles into real DOM (screen will be updated) container.appendChild(articlesContainerVirtualDOM); }
Using JavaScript to get a template, clone the content to get virtual DOM, adjust and filled the virtual DOM elements then append it to the container dynamically, logic is simple and the is working well, it has less error-prone and less possible to make spelling mistake, but it is SLOWER compared to the first step (pure JavaScript to do string operation), DOM operations like importNode, querySelector, querySelectorAll, appendChild (render to actual screen) needs some overhead. If I have only a few lists then I can ignore this speed issue but I am sensitive to performance issue so I thought that I would revisit this and will change the logic when I found a better solution.
Both tried solutions are working as expected, a few days passed, then I thought that I want to follow the current 'trend' of web front-end UI development, so I read some topics about using 'Reusable Component' design pattern, the idea is similar to OOP (Object Oriented Programming) by creating objects (eg: data model, methods, forms, etc.) which can use with many functions, so 'Reusable Component' is to design/make a component which can be reusable in combination with other components or reusable with other functions (business logic).
Third trial in this journey is to use ReactJS, the facebook-back framework, somehow I still love to code in JavaScript without transpiler like Babel, it is much more convenient to make quick prototyping, which I often do in my articles, unfortunately without Babel to transpile the Component means that I can't use JSX (JavaScript XML), using JSX to create React Component (DOM elements) is easier and recommended, so I read some how-to articles including ReactJS without JSX, at the time of my trial to use ReactJS v16.8.6, I need to include React framework (ReactJS and ReactDOM libraries) as follow.
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
After React framework loaded properly then I created some components, when I started to use React, all my components are class-based (stateful component) to bind (enclose) some data with the component, but then I realized for my requirement there is no need to keep data inside the component to automatically refresh component (update UI) by calling this.setState(...) to change the data, this feature normally for MVVM or to make an interactive UI like forms or web games (more detail about ReactJS state), maybe I will need this feature in the future but definitely not now, so I changed all classes to functional (stateless) components.
var BLOG_LIST = props => { return e('div', {class: 'articles'}, // array props.blogs.map(blog => { return e(A_BOX, blog); }) ); } var A_BOX = props => { return e('div', {class: 'box'}, e(BLOG_LINK, props)); } var BLOG_LINK = props => { props.urlJpg = '/thumbnail/thumbnail_' + props.id + '.jpg?update=' + (new Date(props.updateDT).getTime()); return e('a', { href: '#view-blog?id=' + props.id, title: props.title} // multiple NON-NESTED children , e(IMG, { src: props.urlJpg, alt: props.title }) , e(BLOG_TITLE, { title: props.title}) , e(BLOG_DATES, { writeDT : props.writeDT, updateDT: props.updateDT }) ); } var IMG = props => { return e('img', { src: props.src, alt: props.alt}); } var BLOG_TITLE = props => { return e('h4', null, props.title); } var BLOG_DATES = props => { return e('div', { class: 'article-dates'} // multiple NON-NESTED children , e('span', null, props.writeDT) , e('span', null, props.updateDT) ); } const renderArticleHeadlineListUsingReact = (json, containerId) => { // create shortcuts for ReactJS and ReactDOM window.e = React.createElement; window.render = ReactDOM.render; //window.Component = React.Component; var rootElement = document.getElementById(containerId); render(e(BLOG_LIST, json), rootElement); }
Calling the method renderArticleheadlineListUsingReact(...) will automatically create the same article list as the previous first and second trials, please see that I have broken down the 'component' to single element whenever possible (ie: A_BOX, IMG and BLOG_TITLE), but it is not necessary, the reason I did that just want to show about the logic of 'reusable component', in my actual trials, I put the content of A_BOX, BLOG_LINK, IMG, BLOG_TITLE and BLOG_DATES directly into BLOG_LIST to make it smaller code, easier to understand and faster (not jumping around 'component'), in fact, if we don't care about 'reusability' then we can combine all those simple components as one big 'component'.
ReactJS has so many more features than what I use here, I only scratched the surface, in the beginning, I was satisfied with the result, seem simple and easy to get another person to join development too, days passed and then the guilty feeling started to rise, ReactJS is simply too much for my requirement, another more complex website such as Facebook may use ReactJS to its full potential (because ReactJS is born from Facebook), seeing that ReactJS features make me wonder what Facebook's web front-end developers would use if they are tasked to find a replacement for ReactJS? can they easily replace ReactJS with AngularJS or VueJS or other frameworks? it makes me think that Facebook's web front-end development and ReactJS are married for life.
Questioning what disadvantages of using ReactJS for my tiny blog site? I could only thing that I've wasted the framework because I don't use many of ReactJS features and also the framework download size issue (it is not a big problem but a waste nevertheless), slowly I continue to look for a more suitable front-end framework.
Fourth trial in this journey is the PreactJS, actually I did know PreactJS before I use ReactJS, at the time I was skeptical about the '3kb size' thing because what good thing can come in 3kb? to make a long story short, after ReactJS implementation is done and running properly, then the Preact's 'drop-in replacement' and 'closer to the metal' marketing start to kick-in my mind, so I dig deeper to understand the differences between PreactJS and ReactJS and see the Preact source code, after I reviewed the code then I regretted that I did not try it earlier, so I swap the code to verify whether the 'drop-in replacement' actually work ... and BAM !! Preact candy never taste so sweet and simple, the first thing I did was to replace the 'ReactJS framework' with 'Preact library' (it is only a single 3kb file and should not be called 'framework' .. LOL).
<script src="https://cdn.jsdelivr.net/npm/preact/dist/preact.min.js"></script>
The previous React 'component' code just need a little adjustment to work with Preact.
var BLOG_LIST = props => { return h('div', {class: 'articles'}, // array props.blogs.map(blog => { return h(A_BOX, blog); }) ); } var A_BOX = props => { return h('div', {class: 'box'}, h(BLOG_LINK, props)); } var BLOG_LINK = props => { props.urlJpg = '/thumbnail/thumbnail_' + props.id + '.jpg?update=' + (new Date(props.updateDT).getTime()); return h('a', { href: '#view-blog?id=' + props.id, title: props.title} // multiple NON-NESTED children , h(IMG, { src: props.urlJpg, alt: props.title }) , h(BLOG_TITLE, { title: props.title}) , h(BLOG_DATES, { writeDT : props.writeDT, updateDT: props.updateDT }) ); } var IMG = props => { return h('img', { src: props.src, alt: props.alt}); } var BLOG_TITLE = props => { return h('h4', null, props.title); } var BLOG_DATES = props => { return h('div', { class: 'article-dates'} // multiple NON-NESTED children , h('span', null, props.writeDT) , h('span', null, props.updateDT) ); } const renderArticleHeadlineListUsingPreact = (json, containerId) => { // create shortcuts for PreactJS //window.Component = window.preact.Component; var rootElement = document.getElementById(containerId); render(h(BLOG_LIST, json), rootElement); }
Can you spot the differences? the differences are mainly for the shortcuts, in React, we created 2 shortcuts e = React.createElement and render = ReactDOM.render, in Preact we also created 2 shortcuts h = window.preact.h and render = window.preact.render both React's render() and Preact's render() are using the same parameters, also React's e() and Preact's h() are using the same parameters, so I only replace every 'e(...)' with 'h(...)', all other parameters are the same, if in ReactJS we replace the shortcut from 'window.e = React.createElement' to 'window.h = React.createElement' then the component code will stay the same.
Eager to see a better result, I wasted no time to quickly replace React with Preact in my local server then uploaded to my remote hosting server to monitor server log output to see if there is any side-effect, there is no strange issue which is expected because no code changes related to the back-end, tried with different browsers, Chrome, Firefox, Safari in Mac and Android smartphone, everything seems stable and I was happy.
Days have passed then at one night when I was free, I remembered that my web does not need Virtual DOM nor stateful component, so in a way Preact also 'too big' for my web, there is some code in Preact to do a comparison (diff-ing) of data to update UI (data binding), then I was started to feel itchy and want to find a smaller library, I did not pay much attention to HyperScript (Hypertext + JavaScript) previously, so I start to read some articles about the relation between HyperScript and JSX, not much I can find but HyperScript is definitely old (checked Github, the project created on Aug 19th, 2012).
Fifth trial in this journey is to take HyperScript and customize it, HyperScript itself is very small, less than PreactJS, this time I want to really chop-off everything that I don't need, there is only 1 requirement in my head, the customized version must be able to use the same exact stateless functional component for React and Preact, so I can switch to React or Preact anytime in the future easily, so I only need the 'h()' and 'render()' below is the over-simplified (butchered) code.
var h = (nodeName, attributes, ...args) => { let children = null; if(args.length) { // ALWAYS put in array // so children could be 1 array with inner multiple arrays children = [].concat(...args.filter(v => v != null)); } return { nodeName, attributes, children }; }; // optional, only for Google Closure Compiler window['h'] = h; var render = (vnode, rootElement = undefined) => { let n; if (typeof vnode === 'string') { n = document.createTextNode(vnode); if(rootElement) { rootElement.appendChild(n); } return n; } if(typeof vnode.nodeName === 'function') { // execute this function, WARNING: no error check // if crashed then please make sure this function is a functional component! n = vnode.nodeName(vnode.attributes); // re-render inner children WITHOUT rootElement ! n = render(h(n.nodeName, n.attributes, n.children)); } else { // simplify everything, assume it is a string text // create element n = document.createElement(vnode.nodeName); // add attributes if exist if(vnode.attributes) { //Object.keys(vnode.attributes || {}).forEach( k => { Object.keys(vnode.attributes).forEach( k => { if('function' == typeof vnode.attributes[k]) { if(/^on\w+/.test(k)) { // it is an event function (ie: onclick, onmouseover, etc.) // remove prefix 'on' // use addEventListener (IE9+) to register it n.addEventListener(k.substring(2), vnode.attributes[k]); } else { // custom function n.addEventListener(k, vnode.attributes[k]); } } else { // treat everything as string // including: 'class', 'data-xxx', 'style', etc. // make everything as simple as can be // parsing object (maybe tree object) will increase code, slow and error-prone ! n.setAttribute(k, vnode.attributes[k]); } }); } } // if there is children then do recursive call to append to this element ! // WARNING: remove Array.isArray safety check, assume it is an array! if(vnode.children) { // remove null vnode.children.filter(c => c != null).forEach( c => n.appendChild(render(c)) ); } if(rootElement) { // turn Virtual-DOM to real DOM by append it to container rootElement.appendChild(n); } return n; }; // optional, only for Google Closure Compiler window['render'] = render;
The above code shine more light onto what is the h() and render() actually do, h() is to define the values for the element tag name, attributes and children, the render() is to create the DOM element using document.createElement(tagName), the tag name can be any valid HTML element tags or any custom element tag like 'this-is-my-custom-tag' to create <this-is-my-custom-tag></this-is-my-custom-tag>, in render() I append the created element(s) to rootElement just like PreactJS, it is different compare to React which will replace the rootElement full content (innerHTML), I think maybe because React has a need to understand (maintain) everything inside rootElement.
It is very tiny and can fulfill my web need at this moment, I am satisfied with it, in the future if I need more functions then I could either add to it or jump straight to PreactJS and if PreactJS not enough then maybe to ReactJS without much change, obviously I can not pull-request to HyperScript's Github, so for naming sake, I just call it HScript (an over-simplified version of HyperScript), using Google Closure Compiler with 'Advanced' optimization selected the output code is 1,010 bytes (1 KB), 566 bytes gzipped.
function f(a){var d=0;return function(){return d<a.length?{done:!1,value:a[d++]}:{done:!0}}}function g(a){if(!(a instanceof Array)){var d="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];a=d?d.call(a):{next:f(a)};for(var b=[];!(d=a.next()).done;)b.push(d.value);a=b}return a}function k(a,d,b){for(var c=[],e=2;e<arguments.length;++e)c[e-2]=arguments[e];e=null;c.length&&(e=[].concat.apply([],g(c.filter(function(m){return null!=m}))));return{nodeName:a,attributes:d,children:e}} window.h=k; function l(a,d){if("string"===typeof a){var b=document.createTextNode(a);d&&d.appendChild(b);return b}"function"===typeof a.nodeName?(b=a.nodeName(a.attributes),b=l(k(b.nodeName,b.attributes,b.children))):(b=document.createElement(a.nodeName),a.attributes&&Object.keys(a.attributes).forEach(function(c){"function"==typeof a.attributes[c]?(/^on\w+/.test(c)&&(c=c.substring(2)),b.addEventListener(c,a.attributes[c])):b.setAttribute(c,a.attributes[c])}));a.children&&a.children.filter(function(c){return null!=c}).forEach(function(c){return b.appendChild(l(c))}); d&&d.appendChild(b);return b} window.render=l;
During my journey with React, Preact, HyperScript, and HScript, I have a need to verify whether my 'components code' can work properly with all of them without any change, so I created wrapper functions to dynamically load the required framework/library, this makes my testing and switching between framework/library much easier.
var cl = console.log; const loadReactJS = () => { loadExternalScript('https://unpkg.com/react@16/umd/react.production.min.js', (scriptUrl, isSucceed) => { if(!isSucceed) { return; } cl('ReactJS is loaded, next load the ReactDOM'); loadExternalScript('https://unpkg.com/react-dom@16/umd/react-dom.production.min.js', (scriptUrl, isSucceed) => { if(!isSucceed) { return; } cl('ReactDOM is loaded, ready for action'); [h, Component] = React ? [React.createElement, React.Component] : [h, Component]; render = ReactDOM ? ReactDOM.render : render; }); }); } const loadPreactJS = () => { loadExternalScript('https://cdn.jsdelivr.net/npm/preact/dist/preact.min.js', (scriptUrl, isSucceed) => { if(!isSucceed) { return; } if(window.preact) { cl('PreactJS loaded, ready for action'); // Preact is loaded, so use it [ h, render, Component ] = [ window.preact.h, window.preact.render, window.preact.Component ]; } }); } var loadExternalScript = (scriptUrl, callback) => { var script = document.createElement('script'); script.src = scriptUrl; script.onload = () => { cl(scriptUrl + ' loaded, ready for action'); if(callback) { callback(scriptUrl, true); // succeed } } script.onerror = e => { cl(scriptUrl + ' failed to load !'); if(callback) { callback(scriptUrl, false); // failed } }; document.head.appendChild(script); };
Summary
For me personally this is quite a journey, a surprising one, started with using pure vanilla JavaScript then use the most features React framework, then to Preact library and to HyperScript library and circle back to vanilla JavaScript.
Throughout this journey I have learned some new useful things about React framework and have gained more experience using it, I also learned another important lesson, don't look down on small things and ignore them but try to be more open-minded and invest some time to try them all, even though my chosen method is actually very simple and I feel have wasted too much time for these long trials and tiresome journey but in the end it is all worth it, as in life, we always learn new things whether we like it or not.
Hopefully this story can help someone to give the inspiration to try these frameworks/libraries, your journey may well be so different than mine, enjoy your own journey.