Preact + Typescript + Parcel + Redux Zero: Rebuilding the QMENTA Front-End focusing on Performance and Minimalism.
Albert Alises
Posted on September 17, 2018
AtQMENTAwe have been rebuilding the front-end from scratch, seeking simplicity and performance not only in design, but also in the technology stack used for it . This article provides a comprehensive overview of the different parts that compound the new platform, such as how decorators are extensively used or the choice of technology stack.
The QMENTA platform has been going on for quite some time now. The old platform front-end was built using well established technologies like JQuery, Dojo or D3. While being feature rich, one of the problems was the scalability and maintainability of such a big codebase and it being quite complex in terms of UX/UI. In medical imaging storing platforms, data and operations are complex enough for the user to manage, so making it more difficult in terms of user experience or visual design is a no-go zone. 🙅🏻
One of the main challenges coming into this year was to rebuild the front-end from scratch to accommodate the new necessities of a growing neuroimaging processing hub, to make it clean and in a way that can be easily maintainable, scalable and up to date with the latest technologies on front-end development.
Having speed, performance and minimalism as a flagship, the aim was to build a single page web application using the FrameworkOfChoice, which can be VueJS, AngularJS, React, or other frameworks such as Hyperapp.
1kB-ish JavaScript framework for building hypertext applications
Hyperapp
The tiny framework for building hypertext applications.
Do more with less—We have minimized the concepts you need to learn to get stuff done. Views, actions, effects, and subscriptions are all pretty easy to get to grips with and work together seamlessly.
Write what, not how—With a declarative API that's easy to read and fun to write, Hyperapp is the best way to build purely functional, feature-rich, browser-based apps using idiomatic JavaScript.
Smaller than a favicon—1 kB, give or take. Hyperapp is an ultra-lightweight Virtual DOM, highly-optimized diff algorithm, and state management library obsessed with minimalism.
Here's the first example to get you started. Try it here—no build step required!
Up until there, pretty standard and mainstream definition for a web application project. After pondering the different frameworks and technologies, we chose Preact coupled with Typescript as the core libraries of choice for the project.
But… why Preact + Typescript? 🤔
Preact, according to its official documentation, is a “fast 3kB alternative to React with the same modern API”. It offers everything we like and love about React such as the Virtual DOM, Component-Based development, lifecycle hooks and jsx syntax for generating dynamically, data-driven interfaces.
this.props and this.state are passed to render() for you.
You can just use class for CSS classes.
Getting rid of a lot of React / React DOM specific functions like React.CreateElement or ReactDOM.render , being separate exports thus avoiding the aliasing, making your code cleaner and more readable.
Regarding performance, testing it on a simple TODO application, the results are astounding, with Preact being among the fastest of them approached by Vue.js and circa 170ms faster than React. Amazing, isn’t it? 🌠
You can perform the performance test on your device by going here.
Not everything is great of course, one can find that the community is not as big or responsive as the React one (it is not that widely used after all). Some sweet functionalities are still not there, such as support for fragments (So you still have to create some good old div wrappers). Furthermore, some libraries are still not supported, but worry not, preact-compat creates a layer on top of Preact to make it fully compatible with React. Magic!⚡️
ATTENTION: The React compatibility layer for Preact has moved to the main preact repo.
ATTENTION: preact-compat has moved to the main repo.
The code here is only meant for the older Preact 8.x release line. If you're still on Preact 8.x we highly recommend upgrading to the 10.x release line as it includes significant improvements and a much more stable React compatibility layer.
🚨 Note: This module is for Preact 8.x and prior - Preact X includes compat by default
For Preact X, please uninstall preact-compat and replace your aliases with preact/compat.
This module is a compatibility layer that makes React-based modules work with Preact, without any code changes.
It provides the same exports as react and react-dom, meaning you can use your build tool of choice to drop it in where React is being depended on.
Interested? Here's an example project that uses preact-compat to work with an existing React library unmodified
achieving more than 95% reduction in size:
We ❤️ Typescript. A lot. For an application that is rich in data and manages a lot of different metadata and results from APIs, Typescript offers us static type checking that comes very handy when defining the different data structures that our application handles, as well as the structure of the props and state of the different components, so we know what to expect, have it documented and have all the data to be consistent at all different stages.
You can create interfaces with Typescript that characterize the input/output data your application manages. In our case, it helps modeling the data necessary to characterize a subject, or an analysis so everything is consistant and we know what to expect when dealing with those structures.
With Typescript, you can also create Custom Components like the one below. This enforces the classes extending ComponentForm to implement methods for handling the form change and also setting the form model (i.e the object with all the fields required for the form such as the user, password, etc…), this model is then required as the state of the component, also a method submitForm() has to be implemented. With that, we have a skeleton or structure that all forms follow with a generic form component that can have any given number of fields.
An example of a simplified generic Preact Component our application uses that enforce Typescript. We can see the different lifecycle hooks and how the props and state of the component are set as Typescript interfaces.
mport{h,Component}from"preact";exportinterfaceCommonSelectorAttrs{options:any[];class?:string;value?:any;printBy?:string;onChange?:(any)=>void;}exportinterfaceCommonSelectorState{visible:boolean;innerValue:string;}exportdefaultclassCommonSelectorextendsComponent<CommonSelectorAttrs,CommonSelectorState>{state={visible:false,innerValue:""};getValue=(entry)=>String(this.props.printBy?entry[this.props.printBy]:entry);componentDidMount():void{this.props.value&&this.setState(()=>({innerValue:this.getValue(this.props.value)}));}selectOption(opt:any):void{this.props.onChange.call(null,{target:{value:opt}});}closeTooltip():void{this.setState(()=>({visible:false}));this.checkMatch();}getMatcher(opt:any):boolean{returnthis.getValue(opt)===this.state.innerValue;}checkMatch():void{constmatch=this.props.options.find((opt)=>this.getMatcher(opt));!!match?this.selectOption(match):this.setState(()=>({innerValue:""}));}explicitSelection(opt:any):void{constval=this.getValue(opt);this.setState(()=>({innerValue:val}));this.closeTooltip();}setValue(){if(this.state.innerValue==='undefined'){return"";}else{returnthis.state.innerValue;}}render(){return (<div><div><p><inputplaceholder="Select one option"/></p></div><div>{this.props.options.map((opt)=>(<divonClick={()=>this.explicitSelection(opt)}><span>{this.getValue(opt)}</span></div>))}</div></div>)}}
Decorators, Decorators… 💎
Ah, Javascript decorators. As a concept, they simply act as wrappers to another function, extending its functionality, like higher-order functions. We extensively use decorators to keep our code cleaner and separate the back-end, state or application management concerns and provide a more structured way to define common functions across components like connecting to the Redux store, connecting to the API or defining some asynchronous behavior to happen before or after the responses are sent (sort of like method proxies), so we do not have to repeat code and provide an elegant, minimal way to define these behaviors. We use them for:
- Asynchronous Method Interceptors
For managing asynchronous behavior we use kaop-TS , which is a decorator library that provides some method interceptors written in Typescript. With them, we can plug in behavior at a join point on the asynchronous method, like perform some operations before the method starts, or after the method has finished, or plugging in a method that intercepts an error and performs some logic. We can see the beforeMethod decorator being used in the snippet below where we define the http decorator.
Simple Yet Powerful Library of ES2016 Decorators with Strongly typed method Interceptors like BeforeMethod, AfterMethod, OnException, etc
Lightweight, solid, framework agnostic and easy to use library written in TypeScript to deal with Cross Cutting Concerns and improve modularity in your code.
Short Description (or what is an Advice)
This library provides a straightforward manner to implement Advices in your app. Advices are pieces of code that can be plugged in several places within OOP paradigm like 'beforeMethod', 'afterInstance'.. etc. Advices are used to change, extend, modify the behavior of methods and constructors non-invasively.
For in deep information about this technique check the resources.
For managing the calls to the QMENTA API, we implemented a @platformRequest(url,method) decorator that you can wrap in any function of with signature function(params,error?,result?) and convert it into an asyncronous call to the API with certain parameters to send in the request and receive the result JSON object from the call or the error thrown by the request. The implementation of the decorator can be seen below as well as an example method calling it.
consthttp=options=>meta=>{if(!options.method)options.method="get";const[params,...aditional]=meta.args;options[options.method==='get'?'params':'data']=params;(requestasany)({...options}).then(res=>{constdata=res.data;meta.args=[params,...aditional,null,res.data];meta.commit();}).catch(err=>{meta.args=[params,...aditional,err,null];meta.commit();})};constcheckError=meta=>{const[err,result]=meta.args.slice(-2);checkInvalidCredentials(err);if(err)return;if(typeofresult!=="object"){meta.args.splice(-2);meta.args=[...meta.args,newError("Wrong response format! (not a json obj)"),null];}}conststringifyParams=meta=>{const[params,...args]=meta.args;meta.args=[stringify(params),...args];}exportconstplatformRequest=(url,method="get",noop=_=>_)=>beforeMethod(method==="get"?noop:stringifyParams,http({method,url}),checkError)/* USAGE EXAMPLE */interfaceChangePasswordRequest{old_password:string,new_password:string,new_password_confirm:string}@platformRequest("/change_password","post")RecoverPassword(params:ChangePasswordRequest,err?,result?){if(err){throwerr;//Cannot change the password}else{//Password changed succesfully!}}
The decorator uses axios under the hood to perform the request 🕵.
Simple Fetch interface http decorator wrapper for your functions.
HTTP Fetch Decorator
Simple Fetch interface decorator wrapper for your functions.
$ npm install http-fetch-decorator
Signature
Then you can place the decorator which has the same input structure as the fetch function. The parameters of the request or the data to send as a body are passed via arguments to the wrapped function, and the result and error objects are available as parameters of the function as well:
importFetchfrom"http-fetch-decorator"classAnyES6Class{
@Fetch("apiexample/getsomething",{method: 'GET',mode:'cors'})staticparseResponse({id: '1'},result,err) {
if(err) throw err;
//Result contains the Response object from fetch, then you can parse
Also, the state management and routing of our application use decorators to extend functionality to the components to, for example, connect it to the store or listen to be rendered at a specific route. On the next section we will talk a little bit more about it.
State Management and Routing 🚀
State management is one of the main concerns of any growing React application. When the number of entities and components keep growing, the need for global state management arises. Redux is one of the mainstream solutions that aims to solve that. This post assumes some previous knowledge of how Redux operates; if not, here you can find a guide on how it works .
In our application, we did not want to have a big store and we tried to keep it as small as possible, reducing the use of the shared state, enforcing encapsulation and separation of concerns. Given so, we decided to use a lightweight (less than 1Kb) version called Redux-Zero. While having some differences, it also reduces a lot of the unnecessary overhead (for the purpose of our application) that Redux has. It still has a store (albeit just one), which provides all the global state that we want to manage; in our case, session and project information, current pages, notifications of the current project id, among others. It also has no reducers, which ironically does reduce the complexity of the library quite a lot.
<!-- the store --><scriptsrc="https://unpkg.com/redux-zero/dist/redux-zero.min.js"></script><!-- for react --><scriptsrc="https://unpkg.com/redux-zero/react/index.min.js"></script><!-- for preact --><scriptsrc="https://unpkg.com/redux-zero/preact/index.min.js"></script><!-- for vue --><script
To get it up and working, just wrap your root component with the <Provider store={store}/> component and set the store on it, an object created by the createStore(state) method, where the state is the object that contains our shared state. To change that state, you create actions which are just pure functions that update this global state , e.g setSession = (state,val) => ({sessions: val});
To call those actions from a component, we have to connect that component to the store. Connecting a given component to the store allows the component to gain access to some actions and the global state via props. We created a decorator to simplify the process of connecting a component to the store.
With this decorator, we can plug it in on top of any component specifying the actions the component will get as props. e.g by putting @connectStore({removeSession, setCurrentPages}); right on top of the declaration of the component, you have access to these actions inside the component which update the global state on the store, while also having access to the global state itself via props ( this.props.removeSession(); ). With this method, we provide a cleaner, more elegant and compact way to connect a component to the store.
Another integral part of any modern application is the option to route between the different views of the project depending on the URL or other parameters, being able to pass parameters to the routes, etc. A common solution is to use the amazing router that comes with React . As much as we like it, it comes with a lot of functionality that we would not really be using, so we made our own preact-routlet , a simple router for Preact/React based in decorators.
Simple `Component Driven` Routing for Preact/React using ES7 Decorators
Preact Routlet
This package contains routing functionalities for Preact and, now from 1.0.0 React applications as well. Instead of using HTML5 history API it uses the oldie /#/what-ever hash routing (this will change in the future).
This project was created by exploring contextual ways to define routes rather than placing all the routes in a single file.
Usage:
Available imports:
// if you're using Reactimport{renderOnRoute,navigate,RouterOutlet,PathLookup,routePool,Link}from"preact-routlet/react"// if you're using Preactimport{renderOnRoute,navigate,RouterOutlet,PathLookup,routePool,Link}from"preact-routlet/preact"
Either from "preact-routlet/preact" or from "preact-routlet/react"
Place your RouterOutlet element somewhere in your JSX:
<div><RouterOutlet />
</div>
Plug the renderOnRoute decorator on some component
The intricacies of it are simple, just wrap your application with the router component <RouterOutlet /> and you are ready to go. You can also wrap some components with <PathLookup shouldRender={condition}/> specifying a condition to render some path, or use the <RouterOutlet redirect="/route" shouldRedirect={condition} /> to specify a condition which, in case it is met, the router automatically redirects to the specified route (for example, we use it to redirect to the login if the session is invalid or has expired).
To navigate to a route you just have to call navigate("/route") and to specify a Component to be rendered at a specific route, you just have to plug the decorator on top of the component e.g @renderOnRoute("forgot-password") making it clear and visual in which route the component is rendered.
With that, we have a compact way of representing routing and state management with the signatures on top of the component that makes it very readable. A dummy component that connects to the store and is rendered on a given route can be seen below.
import{h,Component}from"preact";import{renderOnRoute,navigate}from"preact-routlet";import{setLoginMessage}from"../actions";importconnectStorefrom"../../../connect";import{platformRequest}from"../../../services/api-decorators";//With these decorators, we know the route this component is rendered at and which actions does it have access to, in a compact way@renderOnRoute("/forgot-password")@connectStore({setLoginMessage})exportdefaultclassForgotPasswordextendsComponent<any,any>{//Call the forgot_password API with a POST@platformRequest("/forgot_password","post")performLogin(params,err?,result?){//Params are the parameters of the request e.g {email: "test@gmail.com , user_id: 1234}if(err||result.error){this.setState({message:"There was an error with the request"})}else{this.props.setLoginMessage(result.message);//Calling the store to set a login warning messagenavigate("/login");//Navigate back to the login using the router}}render(){return(<div>
Just an example component with some decorators...
</div>}}
Bundling with Parcel 📦
Parcel, not to be confused with the australian band Parcels, defines itself as a “blazing fast, zero configuration web application bundler”. At the start of the project, we used the fairly standard Webpack for bundling our application. It required quite a lot of plugins and configuration. (webpack.config.ts). Switching to Parcel, we don’t need configuration for typescript or html files anymore, just adding npm install --save-dev parcel-bundler parcel-plugin-typescript does the trick.
The only remaining thing is to specify an entry point and output directory and voilà, it just works. The difference in speed and performance compared to webpack is not very acute in our case (it’s essentially the same in terms of speed), but it’s the zero configuration and minimality of Parcel what makes it our bundler of choice for the project.
Parcel is a zero configuration build tool for the web. It combines a great out-of-the-box development experience with a scalable architecture that can take your project from just getting started to massive production application.
Features
😍 Zero config – Parcel supports many languages and file types out of the box, from web technologies like HTML, CSS, and JavaScript, to assets like images, fonts, videos, and more. It has a built-in dev server with hot reloading, beautiful error diagnostics, and much more. No configuration needed!
⚡️ Lightning fast – Parcel's JavaScript compiler is written in Rust for native performance. Your code is built in parallel using worker threads, utilizing all of the cores on your machine. Everything is cached, so you never build the same code twice. It's like using watch mode, but even when you restart Parcel!
🚀 Automatic production optimization – Parcel optimizes your whole app for production automatically…
The only downside is that, in order to get the hot reloading working for the dev server in Preact + Typescript, you have to add the module.hot.accept() flag to your root component and specify some types in the render function (the third argument as foo.lastChild as Element for Typescript not to complain. The fix can be seen on the following snippet.
import{h,render}from"preact";importMainfrom"./components/main"declareconstmodule:anyconstmountNode=document.getElementById('root');render(<Main/>,mountNode,mountNode.lastChildasElement);// Hot Module Replacementif (module.hot){module.hot.accept();}
Unit and e2e acceptance testing with Jest + Puppeteer
Testing is an integral part of any project. In our project, we use the fairly standard Jest for testing our components coupled with Puppeteer , wich is a web scraper, or having your own highly trained monkey perform the operations you tell him/her on the browser, like clicking a certain button or dragging the mouse over an element. Using those, we can perform some operations on the front-end via a headless Chrome API and then check for expected results with Jest, like checking an element of confirmation appears or the warning message displayed is correct👌. If you want to learn more on how to use Jest + Puppeteer for testing in React, there is a nice article talking about it.
Puppeteer is a JavaScript library which provides a high-level API to control
Chrome or Firefox over the
DevTools Protocol or WebDriver BiDi
Puppeteer runs in the headless (no visible UI) by default
npm i puppeteer # Downloads compatible Chrome during installation.
npm i puppeteer-core # Alternatively, install as a library, without downloading Chrome.
Example
importpuppeteerfrom'puppeteer';// Or import puppeteer from 'puppeteer-core';// Launch the browser and open a new blank pageconstbrowser=awaitpuppeteer.launch();constpage=awaitbrowser.newPage();// Navigate the page to a URL.awaitpage.goto('https://developer.chrome.com/');// Set screen size.awaitpage.setViewport({width: 1080,height: 1024});// Type into search box.awaitpage
Just an example of testing the pagination component using Jest.
import{h,render}from'preact';importPagination,{generateMidPageTabValues}from'../../src/components/pagination-component';constspy=jest.fn()describe("<Pagination /> e2e tests",()=>{letscratch=null;letcomponent=null;beforeEach(done=>{scratch=document.createElement('div');render(<PaginationinitialPage={1}ref={ref=>component=ref}pageChange={(from,to)=>spy(from,to)}totalItems={45}/>, scratch);
done()});it("should display proper information about pages",()=>{expect(scratch.innerHTML).toContain("Showing items 1 to 15 of 45");});it("shouldn't allow to decrease current page if we're on the first page",()=>{expect(component.state.prevAllowed).toBe(false);});it("should properly increase current page",(done)=>{component.pageForward();setTimeout(_=>{expect(scratch.innerHTML).toContain("Showing items 16 to 30 of 45");expect(spy).toHaveBeenLastCalledWith(15,30);done();},1000);});it("Should correctly compute first page indices",()=>{expect(generateMidPageTabValues(1,5,10)).toEqual([2,3,4,5,6]);expect(generateMidPageTabValues(4,5,10)).toEqual([2,3,4,5,6]);});it("Should correctly compute last page indices",()=>{expect(generateMidPageTabValues(13,5,15)).toEqual([10,11,12,13,14]);expect(generateMidPageTabValues(11,5,15)).toEqual([10,11,12,13,14]);});it("Should correctly compute middle pages indices",()=>{expect(generateMidPageTabValues(10,5,15)).toEqual([8,9,10,11,12]);expect(generateMidPageTabValues(7,5,15)).toEqual([5,6,7,8,9]);});it("Should correctly compute indices when few pages",()=>{expect(generateMidPageTabValues(1,5,5)).toEqual([2,3,4]);expect(generateMidPageTabValues(2,5,3)).toEqual([2]);expect(generateMidPageTabValues(2,5,2)).toEqual([]);expect(generateMidPageTabValues(1,5,1)).toEqual([]);});});
The building, testing and deploying process is automated using Jenkins , with the tests running in a docker container in order to perform the tests in an environment with all the graphical libraries puppeteer requires. The fairly simple pipeline can be seen in Figure 9:
We build the binaries, perform the unit tests and then deploy to a testing dominion which varies based on the git branch we are on. Then we perform the e2e tests on that dominion.
The docker image used for the container in which Jenkins runs the pipeline is a custom version of the Puppeteer Docker Image , based on the ubuntu 16.04 image with node 8. The dockerfile can be seen below:
We add the “deployuser” to the container because Jenkins mounts its workspace directory (owned by deployuser) and it requires the user inside the container and outside to be the same in order to perform all the operations required for the deployment to be succesful.
In this article we have presented an overview of the technology stack we used for rebuilding the QMENTA front-end and how it focuses on being small, efficient and simple. We have also seen how we use decorators in our project and the different parts that compound it such as API calls, routing, state management etc.
By setting the foundations and patterns of the project, the next steps are continuously improving the new front-end in a scalable and maintainable way and adding meaningful features to match the functionality of the old platform, while keeping it minimal. You can see how it goes here. 👋
for setting the foundations of the project, carrying the heavyweight through most of it and teaching me the ways of the H̶a̶c̶k̶e̶r̶m̶a̶n̶ Javascript ninja. 🤖 🏋🏻
Want to experiment with these technologies in your next projects? Yay! Here you have some boilerplates we created in github to get you up and running as soon as possible.🤘🏼
The starter includes a basic sample application showcasing the different technologies using Bulma for styles. The example consists on a restaurant order making form with validation/model and some global state…
Poi justs works. Of course Poi will try to look up a .babelrc. Since Preact has a different jsx pragma you must have transform-react-jsx plugin with { pragma: "h" } opt at least to work with.