DEV Community

Albert Alises
Albert Alises

Posted on

Preact + Typescript + Parcel + Redux Zero: Rebuilding the QMENTA Front-End focusing on Performance and Minimalism.

At QMENTA we 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.


This post was originally posted on Medium

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.

Register Page


Figure 1: Register Page for the new front-end

Project List View

Figure 2: Project List view for all our projects on the new front-end

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.

GitHub logo jorgebucaran / hyperapp

The tiny 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!

<script type="module">
  import { h, text, app } from "https://unpkg.com/hyperapp"
  const AddTodo = (state) => ({
    ...state,
    value: "",
    todos: 
Enter fullscreen mode Exit fullscreen mode

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.

Typescript and Preact logos


Figure 3: Preact and Typescript logos.


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.

Preact does all this while keeping the API leaner (React 16.2.0 + React DOM is 31.8Kb while Preact is only 4Kb, both gzipped), making it faster, more lightweight, and reducing a lot of the noise added by React. The principal differences with React can be seen here. Some of them are:

  • 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? 🌠

Performance Test


Figure 4: TodoMVC Performance comparison between different frameworks. Seen at: https://developit.github.io/preact-perf/

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!⚡️

GitHub logo preactjs / preact-compat

🙌 React compatibility layer for Preact.

preact-compat

NPM travis-ci CDNJS

🚨 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:

preact-compat-example


Why?

... or really, "why preact"?

React is a great library and a great concept, and has a large community of module authors creating high-quality components However, these components are tightly coupled to React through the use of generic package imports (example).

Preact is…

Analysis List View


Figure 5: Analysis List View

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.

export interface AnalysisInput {
  container_id: number,
  date: { $date: number },
  in_out: string,
  filters?: any[][],
  passed: boolean,
  ssid: string,
  subject_name: string
}

export interface AnalysisSettings {
  acpc_alignment: string,
  age_months: number,
  alignment_tool: string,
  input: AnalysisInput
}
Enter fullscreen mode Exit fullscreen mode

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.

import { h, Component } from "preact";

interface FormManagement<M> {
  form: M,
  message: string
}

export abstract class ComponentForm<props, model> extends Component<props, FormManagement<model>> {

  protected setFormModel (model: any) {
    this.setState({ form: new model })
  }

  protected handleFormChange(change) {
    this.setState(({ form, ...state }) => ({
      form: { ...form, ...change },
      ...state
    }));
  }

  abstract submitForm(...args): void;
}
Enter fullscreen mode Exit fullscreen mode

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";

export interface CommonSelectorAttrs {
  options: any[];
  class?: string;
  value?: any;
  printBy?: string;
  onChange?: (any) => void;
}

export interface CommonSelectorState {
  visible: boolean;
  innerValue: string;
}

export default class CommonSelector extends Component<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 {
    return this.getValue(opt) === this.state.innerValue;
  }

  checkMatch(): void {
    const match = this.props.options.find((opt) => this.getMatcher(opt));
    !!match ? this.selectOption(match) : this.setState(() => ({ innerValue: "" }));
  }

  explicitSelection(opt: any): void {
    const val = this.getValue(opt);
    this.setState(() => ({ innerValue: val }));
    this.closeTooltip();
  }

  setValue(){
    if(this.state.innerValue === 'undefined'){
      return "";
    }else{
      return this.state.innerValue;
    }
  }

  render() {
    return (
      <div>
        <div>
          <p>
            <input placeholder="Select one option" />
          </p>
        </div>
        <div>
          {this.props.options
            .map((opt) => (
              <div onClick={() => this.explicitSelection(opt)}>
                <span>{this.getValue(opt)}</span>
              </div>
          ))}
        </div>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo k1r0s / kaop-ts

Simple Yet Powerful Library of ES2016 Decorators with Strongly typed method Interceptors like BeforeMethod, AfterMethod, OnException, etc

kaop

semantic-release Greenkeeper badge Image travis version Coverage Status dependencies dev-dependencies downloads Known Vulnerabilities

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.

Demo

https://jsbin.com/bogecojuvi/edit?js,console

Get started

npm install kaop-ts --save
Enter fullscreen mode Exit fullscreen mode

Use a join point to plug it to any method/class:

import { afterMethod } from 'kaop-ts'
const double = afterMethod(meta => meta.result *= 2)
class DummyExample {
  @double
  static calculateSomething (num, num2) {
    return num * num2
  }
}
DummyExample.calculateSomething(3, 3
Enter fullscreen mode Exit fullscreen mode
- Connecting with and Managing API calls

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.

const http = options => meta => {
  if(!options.method) options.method = "get";
  const [ params, ...aditional ] = meta.args;
  options[options.method === 'get' ? 'params' : 'data'] = params;
  (request as any)({ ...options })
  .then(res => {
    const data = res.data;
    meta.args = [params, ...aditional, null, res.data];
    meta.commit();
  })
  .catch(err => {
    meta.args = [params, ...aditional, err, null];
    meta.commit();
  })
};

const checkError = meta => {
  const [err, result] = meta.args.slice(-2);
  checkInvalidCredentials(err);
  if(err) return;
  if(typeof result !== "object") {
    meta.args.splice(-2);
    meta.args = [ ...meta.args, new Error("Wrong response format! (not a json obj)"), null ];
  }
}

const stringifyParams = meta => {
  const [ params, ...args ] = meta.args;
  meta.args = [ stringify(params), ...args ];
}

export const platformRequest = (url, method = "get", noop = _ => _) => beforeMethod(
  method === "get" ? noop : stringifyParams,
  http({ method, url }),
  checkError)

/* USAGE EXAMPLE */
interface ChangePasswordRequest {
  old_password: string,
  new_password: string,
  new_password_confirm: string
}

@platformRequest("/change_password", "post")
  RecoverPassword(params : ChangePasswordRequest, err?, result?) {
    if(err) {
      throw err;
      //Cannot change the password
    } else {
      //Password changed succesfully!
    }
 }
Enter fullscreen mode Exit fullscreen mode

The decorator uses axios under the hood to perform the request 🕵.

An implementation of it (as a minimal library) wrapping the Fetch API can be seen on the http-fetch-decorator repo:

GitHub logo aalises / http-fetch-decorator

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:

@Fetch(Url: String, initObj?: Object)
static wrappedFunction(params: Object, result? : Response, err? : any){
}
Enter fullscreen mode Exit fullscreen mode

How to use it

import Fetch from "http-fetch-decorator"
class AnyES6Class {
  @Fetch("apiexample/getsomething", {method: 'GET',mode:'cors'})
  static parseResponse({id: '1'},result,err) {
    if(err) throw err
    //Result contains the Response object from fetch, then you can parse
Enter fullscreen mode Exit fullscreen mode

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.

Subject Detail Viewing


Figure 6: Specific Subject files viewing with the edit file metadata modal.


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.

GitHub logo redux-zero / redux-zero

A lightweight state container based on Redux

redux zero logo

A lightweight state container based on Redux

Read the intro blog post


codacy build npm downloads license dependencies

Table of Contents

Installation

To install the stable version:

npm i redux-zero

This assumes that you’re using npm with a module bundler like webpack

How

ES2015+:

import createStore from "redux-zero";
import { Provider, connect } from "redux-zero/react";
Enter fullscreen mode Exit fullscreen mode

TypeScript:

import * as createStore from "redux-zero";
import { Provider, connect } from "redux-zero/react";
Enter fullscreen mode Exit fullscreen mode

CommonJS:

const createStore = require("redux-zero");
const { Provider, connect } = require("redux-zero/react");
Enter fullscreen mode Exit fullscreen mode

UMD:

<!-- the store -->
<script src="https://unpkg.com/redux-zero/dist/redux-zero.min.js"></script>
<!-- for react -->
<script src="https://unpkg.com/redux-zero/react/index.min.js"></script>
<!-- for preact -->
<script src="https://unpkg.com/redux-zero/preact/index.min.js"></script>
<!-- for vue -->
<script
Enter fullscreen mode Exit fullscreen mode

Brain Fibers Visualizer


Figure 7: Brain Tractography Fiber visualization on the front-end using WebGL

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.

import { h, Component } from "preact";
import { Connect } from "redux-zero/preact";

export default function(actions = {}): any {
  return Child => props => (
    <Connect mapToProps={state => ({ ...state })} actions={actions}>
      {mappedProps => <Child {...mappedProps} {...props} />}
    </Connect>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo k1r0s / preact-routlet

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 React
import { renderOnRoute, navigate, RouterOutlet, PathLookup, routePool, Link } from "preact-routlet/react"
// if you're using Preact
import { renderOnRoute, navigate, RouterOutlet, PathLookup, routePool, Link } from "preact-routlet/preact"
Enter fullscreen mode Exit fullscreen mode

Either from "preact-routlet/preact" or from "preact-routlet/react"

Place your RouterOutlet element somewhere in your JSX:

<div>
  <RouterOutlet /&gt
</div>
Enter fullscreen mode Exit fullscreen mode

Plug the renderOnRoute decorator on some component

@renderOnRoute("/login")
export default class Login extends Component<any, any> {
Enter fullscreen mode Exit fullscreen mode

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";
import connectStore from "../../../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 })
export default class ForgotPassword extends Component<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 message
      navigate("/login"); //Navigate back to the login using the router
    }
  }

   render() {
     return (
        <div>
          Just an example component with some decorators...
        </div>
   }
}

Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo parcel-bundler / parcel

📦🚀 Blazing fast, zero configuration web application bundler

Parcel

Backers on Open Collective Sponsors on Open Collective Build Status npm package npm package Twitter Follow

Features

  • 🚀 Blazing fast bundle times - multicore compilation, and a filesystem cache for fast rebuilds even after a restart.
  • 📦 Out of the box support for JS, CSS, HTML, file assets, and more - no plugins to install.
  • 🐠 Automatically transforms modules using Babel, PostCSS, and PostHTML when needed - even node_modules.
  • ✂️ Zero configuration code splitting using dynamic import() statements.
  • 🔥 Built in support for hot module replacement
  • 🚨 Friendly error logging experience - syntax highlighted code frames help pinpoint the problem.

Below is the design document that was created before work on the implementation of Parcel 2 started and some sections are outdated. The actual (somewhat complete) documentation for Parcel 2 is available here: https://v2.parceljs.org/.


Getting Started

Before we get started, you'll need to install Node and Yarn (or npm) and create a package.json for your project if you haven't already.

yarn init
Enter fullscreen mode Exit fullscreen mode

Then…

Parcel Performance


Figure 8: Screenshot of the build of the whole application. Parcel’s magic💥

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";
import Main from "./components/main"

declare const module: any

const mountNode = document.getElementById('root');

render(<Main />, mountNode, mountNode.lastChild as Element);

// Hot Module Replacement
if (module.hot) {
    module.hot.accept();
}

Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo puppeteer / puppeteer

Headless Chrome Node.js API

Puppeteer

Build status npm puppeteer package

API | FAQ | Contributing | Troubleshooting

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

What can I do?

Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started:

  • Generate screenshots and PDFs of pages.
  • Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)).
  • Automate form submission, UI testing, keyboard input, etc.
  • Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features.
  • Capture a timeline trace of your site to help diagnose performance issues.
  • Test Chrome Extensions.

Give it a spin: https://try-puppeteer.appspot.com/

Getting Started

Installation

To use Puppeteer in your…

Just an example of testing the pagination component using Jest.

import { h, render } from 'preact';
import Pagination, {generateMidPageTabValues} from '../../src/components/pagination-component';

const spy = jest.fn()

describe("<Pagination /> e2e tests", () => {

  let scratch = null;
  let component = null;

  beforeEach(done => {
    scratch = document.createElement('div');
    render(<Pagination initialPage={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([]);
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

Jenkins Pipeline


Figure 9: Jenkins pipeline for deploying the front-end

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.

This way, we avoid the whole application to suddenly crash and burn or turn into one of those amazing 90’s websites.


Summary

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. 👋

Special kudos 🙌 to

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.🤘🏼

GitHub logo aalises / preact-typescript-parcel-starter

Starter with Preact - Typescript - Parcel Bundler

Preact + Typescript + Parcel Starter

Zero configuration bundling and lightweight state management and view => Preact + Typescript + Parcel Bundler + Redux Zero + Jest + Pupetteer

This starter project contains many features and technologies to start the development ASAP.

Features:

  • TypeScript: Strong typing and the future of JavaScript
  • Preact: dom manipulation + components
  • Redux-Zero: state management
  • Preact-Bind-Group: Group Form fields onChange Events to a single Callback, making forms easier to manage
  • Parcel: 0 configuration bundler
  • Jest: Delightful javascript testing
  • Puppeteer: Headless chrome node API scrapper
  • Lynt: 0 configuration linting for TS + React

run development server npm run dev

build the project (on /dist) npm run build

run tests npm run test

run linting npm run lint

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…

GitHub logo k1r0s / preact-poi-starter

Preact Example App powered by Poi and BabelJS

Preact Poi starter

So whats the deal here?

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.

{
  "sourceMaps": true,
  "presets": [
    ["es2015", { "loose":true }],
    "stage-0"
  ],
  "plugins": [
    ["transform-decorators-legacy"],
    ["transform-react-jsx", { "pragma": "h" }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

For this example I added transform-decorators-legacy as well.

run development server

npm run dev aka $ poi [root file]

run production build

npm run build aka $ poi build [root file]




GitHub logo k1r0s / bleeding-preact-starter

TypeScript · Preact · Redux-zero => frontend starter with bleeding features. Check it out!

Bleeding Frontend Starter

Preact + TypeScript + Redux-Zero + Moar..

This starter project contains many features to speedup development and have fun which is pretty handy today.

Features:

common folder contains Connect decorator for redux-zero (which is not yet included on the lib)

Powered by Webpack

Heading production env -> Polyfills, Shims & Extensions

I recommend to install core-js and import all its contents on your main file import 'core-js'




Oldest comments (4)

Collapse
 
insuusvenerati profile image
Sean

This is awesome work! Why has no one commented on this yet! O.o I'm firing up a workspace to play with this right now :)

Collapse
 
aalises profile image
Albert Alises

Thank you very much Sean!! Tried to gave an overview of pretty much everything (except styling 💅). The boilerplate used for the project is exactly this one:

k1r0s / bleeding-preact-starter

TypeScript · Preact · Redux-zero => frontend starter with bleeding features. Check it out!

Bleeding Frontend Starter

Preact + TypeScript + Redux-Zero + Moar..

This starter project contains many features to speedup development and have fun which is pretty handy today.

Features:

common folder contains Connect decorator for redux-zero (which is not yet included on the lib)

Powered by Webpack

Heading production env -> Polyfills, Shims & Extensions

I recommend to install core-js and import all its contents on your main file import 'core-js'


but switching Webpack for Parcel.

If you have any suggestion or idea on how to extend this stack or improve, or any question, let me know! 🤘

Collapse
 
adrienpoly profile image
Adrien Poly

thanks for this great article. It is very detailed. I discovered Redux-zero will give it a try soon with preact.
Thumbs up!!!

Collapse
 
pierre profile image
Pierre-Henry Soria ✨

Great article Albert! Really useful 🙂