Introduction To Functional Front-Ends With Inferno

A. Sharif
JavaScript Inside
Published in
6 min readDec 11, 2016

--

This write up is intended as an introduction as well as reflection on the current state of functional front-ends. In part one we will cover some fundamental basics, while in part two our focus will mainly be on handling side-effects, routing and more advanced topics.

Introduction

Sometimes the virtual DOM is simply seen as an efficient and performant way to update the UI. What is more important than performance is the fact that we can ensure that our view functions are pure. By decoupling the concrete rendering, we can be assured that our view is a function of the actual state.

The Virtual DOM concept has been often emphasized for its speedy performance. But there is also an equally important — if not more important — property. The concept makes it possible to represent the UI as a function of its state.

Yassine Elouafi, React-less Virtual DOM with Snabbdom

With the rise of React and concepts like Flux or libraries like Redux, functional front-ends have become common ground.

We will get a clear idea on why embracing functions and composition to build large applications makes sense and also see what we can achieve by emphasizing these concepts by the end of this series.

All the examples will be built with Inferno, a very efficient React-Like library, but you can also use React, Preact or any other standalone virtual-Dom library. I have chosen Inferno for it’s capability to provide lifecycle hooks on stateless functions. Inferno is optimized for speed, but the aforementioned capability will ensure not having to make use of class and this any where inside our application.

To follow along, install create-inferno-app.

npm install create-inferno-app

After installing create-inferno-app you can create a new project.

create-inferno-app functional-frontend
cd functional-frontend
npm start

That’s it. We have a basic Inferno setup running. We will start off, by building a couple of basic examples.

Basics

The first example is inspired by Yassine Elouafi’s example of a dynamic view from his React-less Virtual DOM with Snabbdom article.

We want to display the time. To begin with, let’s import Inferno and Ramda. Inferno offers a render function via the core module, which simply expects a component and a node, similar to React. First thing we will do is flip the arguments so we can compose the render function. We’ll use Ramda’s curry function to achieve this. We can pass in the node as the first argument and get a function in return, that expects a component.

import Inferno from 'inferno'
import
R from 'ramda'
const
renderer =
R.curry((node, component) => Inferno.render(component, node))
const render = renderer(document.getElementById('root'))

Now that we have the basic setup out of the way, let’s focus on writing our main function, that takes care of updating our components with the current state. Time is a view function that expects the current date as input and returns an element based on that input.

const Time = (currentDate) => (
<div>Current date {`${currentDate}`}</div>
)

Now all that is left to do, is to define is our main function, which is a composition of render and Time and we update main every 1000ms with the current date.

const main = R.compose(render, Time)
setInterval(() => main(new Date()), 1000)

You can see the full example here.

Before we start thinking about how this approach will scale, let’s see examples where user input will trigger a re-rendering.

const getTargetValue = R.path(['target', 'value'])
const View = (name) => (
<div>
<input
placeholder='Type your name'
onInput=
{R.compose(main, getTargetValue)}
/>
<div>Hi, {name}</div>
</div>
)
const main = R.compose(render, View)
main('')

Again, Our main is a composition of render and View, where main is being initialized with an empty string. The rest should be clear. Every time the input is updated via user interaction, we call main with the new value. The example composes main and getTargetValue, which could also be written as onInput={e => main(e.target.value)}. Find the complete example here.

To round up the basics, let’s also see how the classic counter example can be implemented with this approach.

const Counter = (count) => (
<div>
<button onClick={() => main(R.inc(count))}>+</button>
{count}
<button onClick={() => main(R.dec(count))}>-</button>
</div>
)
const main = R.compose(render, Counter)
main(0)

Full example code here.

While this example works, we can see a couple of limitations to this approach. We’re calling main inside our onClick function as well as updating our count directly. Let’s see how we can structure our application more efficiently.

Elm Inspired Architecture

From here on out, we’ll focus on an Elm inspired architecture and will also use some ideas from elmar.js. elmar.js is (currently) not a library per se but a collection of examples and helper functions written by Stefan Oestreicher for creating Elm architecture inspired applications in JavaScript.

Let’s get back to our counter example. We will define an init function, which should be run when initially calling our Counter.

const init = count => count

Next let’s define possible actions, which should be nothing new if you’ve been using Redux.

const action = {
Inc: count => count + 1,
Dec: count => count - 1,
}

After defining our increment and decrement actions, we finally create an update function that should be called when an action has been dispatched. The dispatch function is passed to the view and applied with the proper action when an onClick event has taken place f.e. Finally we export our init, update and view functions.

const update = (action, model) => action(model)
const view = (dispatch, count) => (
<div>
<button onClick={dispatch(action.Inc)}>+</button>
{count}
<button onClick={dispatch(action.Dec)}>-</button>
</div>
)
export default {init, update, view}

So now that we have written our Counter, how do we update the Counter and where does dispatch come from? Let’s rewrite our main function.

const startApp = (state, view, update) => {
const dispatch = R.curry((state, action) =>
() => main(update(action, state), view))
const main = (state, view) => render(view(dispatch(state), state))
main(state, view)
}

By defining startApp we decouple any concrete component from our main function. We just pass in an initial state, an update and a view function. Taking a closer look at our dispatch function, it becomes obvious that we’re seeing how all 0f this is actually a cycle, with the dispatched action being asynchronous. So as soon as an action is dispatched we call the main function with the newly created state (by applying the update function with the action and the current state) and the view. main itself calls view with a dispatch function and the current state.

All we need to do now is call startApp with the counter view and update function as well as the calculated initial state.

startApp(Counter.init(0), Counter.view, Counter.update)

Find the full working example here.

Before we finish this introduction, let’s see how we can create a counter pair with this approach.

// CounterPair.js
//...
import Counter from './Counter'
const init = (first, second) => ({
first: Counter.init(first),
second: Counter.init(second),
})

Our init function now expects two values and assigns the initial values by calling the counter init function.

const action = {
Reset: model => init(0, 0),
First: (action) =>
model =>
({...model,
...{first: Counter.update(action, model.first)}}),
Second: (action) =>
model =>
({...model,
...{second: Counter.update(action, model.second)}}),
}
const update = (action, model) => action(model)

Our action looks quite differently as opposed to the Counter actions. First of all, we added a Reset action, which simply resets both counters to zero again. Additionally we also defined a First and Second action, which take care of receiving any action from within counter and updating the state depending from where the action has been dispatched. This might seem confusing at first glance, but will become very clear, the further we start to dive into the topic.

const forward = (dispatch, action) => R.compose(dispatch, action)

What is forward? forward is a helper function to delegate action calls down to child views. What we’re actually doing is defining which action will be called when clicking inside of one our counter views. Counter will dispatch an action without having to know which action is actually called in CounterPair.

const view = (dispatch, model) => (
<div>
{Counter.view(forward(dispatch, action.First), model.first)}
{Counter.view(forward(dispatch, action.Second), model.second)}
<button onClick={dispatch(action.Reset)}>Reset</button>
</div>
)
export default {init, update, view}

Finally let’s call startApp with CounterPair.

startApp(CounterPair.init(0, 10), 
CounterPair.view,
CounterPair.update)

Find the full working example here.

In part two we will focus on side-effects handling, routing and other advanced topics. I would recommend to try out all the examples and also try to implement a todo-list or a counter-list based on the described architecture.

If you have any feedback please leave a comment here or on Twitter.

Links

Inferno

create-inferno-app

elmar.js

Elm Architecture

Elm Architecture for React

React-less Virtual DOM with Snabbdom

--

--

A. Sharif
JavaScript Inside

Focusing on quality. Software Development. Product Management.