Functional stateful components in hyperapp

Introduction

Hyperapp is a tiny JavaScript library to create views. It stands out with it's small size (1kB gzipped) and its simplicity. Its functional approach doesn't allow to use objects/classes as components. Here I show you how to create components with a local state. This can come in handy for example when one wants to create a form and only persist the state when submitting it.

Note, that I assume you already have a basic knowledge of hyperapp and JavaScript since I don't explain in detail how to use hyperapp.

Closures to the rescue

As you may know, JavaScript allows the usage of closures which means inner functions have access to outer variables.

function outer () {
    const outerValue = 1;

    return function inner () {
        console.log(outerValue);
    };
}

Components in hyperapp

A hyperapp component is a function which returns a virtual node. This looks something like this:

import { h } from 'hyperapp';

export function MyComponent (props, children) {
    const { value, onChange } = props;
    
    return h('div', {}, [
        h('p', { value }),
        h('button', { 
            onclick () {
                onChange(value + 1);
            }
        }, 'increase the value'),
    ]);
}

One would use this component like this:

import { h } from 'hyperapp';

export function App (props, children) {
    const { value, changeValue } = props;
    
    return h('div', {}, [
        h('MyComponent', {
            value,
            onChange: changeValue
        })
    ]);
}

Creating the stateful hyperapp component

Actually if you want to have state in hyperapp you would have a unique state object which gets bundled with your app and you pass down the props to your components.

To create a stateful component we now can make use of closures. This means we surround our component function with another function which holds our local state.

import { h } from 'hyperapp';
import { wiredActions } from './app';

export function myComponentFactory () {
    let value = 3;

    function onChange (newValue) {
        value = newValue;
    }

    return function MyComponent () {
        return h('div', {}, [
            h('p', {}, value),
            h('button', {
                onclick () {
                    onChange(value + 1);
                }
            }, 'increase the value'),
        ]);
    }
}

Note, that we now export a factory function, which we use like this to create our actual component:

import { h } from 'hyperapp';
import { myComponentFactory } from './MyComponent.js';

const MyComponent = myComponentFactory();

export function App (props, children) {
    const { value, changeValue } = props;
    
    return h('div', {}, [
        h(MyComponent, {
            value,
            onChange: changeValue
        })
    ]);
}

The drawback of this is that if we want multiple instances of that component, we have to call the factory again for every instance. Otherwise all instances of that component share the same state. This makes them inappropriate for using them in a loop.

Enable rerendering

When you try that out you may notice that even when our onChange function is called, hyperapp won't rerender our component with that updated value. This is because we avoid the hyperapp cycle this way. Hyperapp only triggers rerendering when the state has changed.

We can force the rerendering by creating a dummy action which we call with our state update. The actions are wired to the app when we initialize it.

import { app, h  } from 'hyperapp';
import { myComponentFactory } from './MyComponent.js';

const state = {};
const actions = {
    rerender () {
        return function () {
            // create a new object to trigger rerendering
            return {};
        };
    }
};

const MyComponent = myComponentFactory();

function App () {
    return h('div', {}, [
        h(MyComponent)
    ]);
}

// we export the wired actions so we can use it all over our app
export const wiredActions = app(state, actions, App, document.body);

Now we have an action available which can be used to force hyperapp to rerender our component.

Our updated Component now looks like this:

import { h } from 'hyperapp';
import { wiredActions } from './app';

export function myComponentFactory () {
    let value = 3;

    function onChange (newValue) {
        value = newValue;
        
        // this triggers the rerendering
        wiredActions.rerender();
    }

    return function MyComponent () {
        return h('div', {}, [
            h('p', {}, value),
            h('button', {
                onclick () {
                    onChange(value + 1);
                }
            }, 'increase the value'),
        ]);
    }
}

Et voilĂ , we have our functional stateful component. If you want to persist the value to the actual application state, you can pass a persistance function to the props of the component.

Please keep in mind that you should only use this when you really need it. The philosophy of a unique state is very handy since it makes it easier to track changes. But sometimes you only need a local state for your component and then this solution may be your friend.

Thanks for reading and happy coding!