dennis sänger

personal blog

I'm a fullstack developer from germany, particularly interested in functional programming, clean code and all things javascript & software development in general.

Hyperapp + Parcel = 😍

March 25, 2018

In this post I'll have a first look at two new tools in the JS world: Hyperapp and Parcel. You can find the example app I build here on github.

Hyperapp

Hyperapp is a micro-frontend-framework that combines state management with a virtual dom. Its architecture reminds me of Elm, with its state, view and actions. The idea behind both is simple: An app has a single state object. Based on the state, every view (component) of the app is rendered. Every time the state changes, all affected views re-render. The state object itself is immutable and can only be changed via actions which return a new state (more on this pattern latter)

parceljs

Parcel is a new, fast, zero config web application bundler. I cannot say much about it, because it is so easy to use and just works out of the box. Everybody who setup webpack once will love parcel. Just install it .. and you are done - awesome 😎.

example app

Let's build a simple example app to see how these two work. Our app should fetch 10 random users from github and show their username and avatar. It should provide a button to load another 10 random user. The final app should look like this:

Example-App

Let's get started by creating a new npm package and install our deps:

mkdir random-github-user
cd random-github-user
npm init --y
npm install --save hyperapp ramda bulma
npm install --save-dev parcel-bundler babel-preset-env babel-preset-hyperapp

We could build the whole thing without ramda and bulma, but using a utility library and a css framework in your app is a common scenario, though I stick with them.

Next, I added the most basic HTML file I could think of:

<html>
<body>
  <script src="./index.js"></script>
</body>
</html>

Since I want to use JSX (it's not required with hyperapp, but I like it) there is one config we have to add after all, and thats babel. So, create the following .babelrc file:

{
  "presets": ["env", "hyperapp"]
}

Now let's create a minimal hyperapp with JSX; add this code to the index.js file:

import { h, app } from 'hyperapp';

const state = {};
const actions = {};

const view = (state, actions) => <h1>Hello Hyperapp</h1>;

app(state, actions, view, document.body);

Now, we can fire up parcel in dev mode

./node_modules/.bin/parcel index.html

And open the result in the browser at http://localhost:1234. Congratulations, that is your first hyperapp - already bundeld with parcel! 😸

Let's make it prettier by adding bulma. Since parcel can bundle css as well, it's as easy as adding the bulma css file as import to the index.js file:

import 'bulma/css/bulma.css';

And give the h1 a title class:

const view = (state, actions) => <h1 class="title">Hello Hyperapp</h1>;

Now it's time to add a action to fetch some github users. To do this, first some things about actions in hyperapp. An action should return a partial state object or null/undefined/a Promise. Moreover an action can return a function; if thats the case, hyperapp automatically calls that function and passing the current state and actions as parameters.

If the action returns a partial state, hyperapp automatically shallow merges this with the current state to create the new (next) state and triggers a re-render of the views.

If the actions returns null/undefined or a Promise, the state is not updated and nothing is re-rendered.

To handle async actions (aka. side effects) we can call actions from actions. So, to fetch something over http, we need two actions: One to trigger the http-request and one to handle the response (the used helper functions are defined below):

const actions = {
  fetchUsers: () => (state, actions) =>
    fetchRandomGithubUser()
      .then(invokeJson)
      .then(take10)
      .then(actions.fetchDone),
  fetchDone: users => ({ users }),
};

fetchUsers is an action without parameters and returns a function. This function is called by hyperapp automatically, passing the state and actions. The fetchUsers-action than triggers the http-request and processes the response in the promise-chain. Since fetchUsers returns a promise, the state is not immediately updated.

Only when actions.fetchDone is called in the promise chain, the state is updated by shallow merging the return value of actions.fetchDone with the current state.

This is the whole index.js including the used helper functions so far:

import { h, app } from 'hyperapp';
import { pipe, invoker, take } from 'ramda';

import 'bulma/css/bulma.css';

// returns a random number
const random = (max = 1000000) => Math.floor(Math.random() * Math.floor(max));

// uses the github api to fetch 30 users starting with the userid provided by the since parameter
const fetchGithubUserSince = since =>
  fetch(`https://api.github.com/users?since=${since}`);

// combine random and fetchGithubUserSince to fetch random users
const fetchRandomGithubUser = pipe(random, fetchGithubUserSince);

// this helper invokes the .json() method on the given object => invokeJson(foo) -> foo.json()
const invokeJson = invoker(0, 'json');

// take10 entries from an array
const take10 = take(10);

// get github url of a user
const githubUrl = username => `https://github.com/${username}`;

const state = {
  users: [],
};

const actions = {
  fetchUsers: () => (state, actions) =>
    fetchRandomGithubUser()
      .then(invokeJson)
      .then(take10)
      .then(actions.fetchDone),
  fetchDone: users => ({ users }),
};

const view = (state, actions) => <h1 class="title">Hello Hyperapp</h1>;

app(state, actions, view, document.body);

This was the hard part. Now we just need to add some components to show the result. We need a users component to render a single username + avatar:

const UserComponent = ({ user }) => (
  <div class="column is-one-fifth">
    <div class="box has-text-centered">
      <a href={githubUrl(user.login)} target="_blank">
        <p class="is-size-4">{user.login}</p>
        <img width="128" src={user.avatar_url} alt="" />
      </a>
    </div>
  </div>
);

We need a button to fetch more random users:

const FetchButton = ({ fetch }) => (
  <button class="button" onclick={fetch}>
    Fetch random users
  </button>
);

See how these are dump components? Everything they need is passed via parameters.

Now wire it together in the main view:

const view = (state, actions) => (
  <div oncreate={actions.fetchUsers}>
    <h1 class="title">hyperapp + parcel experiment</h1>
    <h2>
      <FetchButton fetch={actions.fetchUsers} />
    </h2>
    <div class="container" style={{ margin: '10px', padding: '10px' }}>
      <div class="columns is-multiline is-2 is-variable">
        {state.users.map(u => <UserComponent key={u.id} user={u} />)}
      </div>
    </div>
  </div>
);

I added the oncreate attribute to the outer div to load some users on the first pageload (to be honest idk if thats the correct way to handle this in hyperapp, but it feels correct).

I also added the key attribute to the UserComponent that hyperapp can identify each component in the loop easily.

Time to build the whole thing:

./node_modules/.bin/parcel build index.html

This took about ~15 seconds on my old macbook air.

✨  Built in 15.60s.

dist/helloworld-hyperapp.js      180.5 KB    1204.90s
dist/helloworld-hyperapp.css    160.68 KB        10ms
dist/index.html                     142 B       3.27s

Looks good, but why the heck is the helloworld-hyperapp.js about 180kb? Quite big for this tiny app and given the fact that hyperapp is really small. The reason is ramda. Sadly, parcel cannot treeshare the bundle yet, so the complete ramda library is imported. Good news is, treeshaking in parcel is one roadmap!

Until than, we can fix the bundle size by importing only the functions we need from their own files:

import pipe from 'ramda/src/pipe';
import invoker from 'ramda/src/invoker';
import take from 'ramda/src/take';

This improves the build time and the bundle size:

✨  Built in 8.67s.

dist/helloworld-hyperapp.css    160.68 KB     3.37s
dist/helloworld-hyperapp.js      23.85 KB    37.43s
dist/index.html                     142 B     1.86s

The same can be achieved by using babel-plugin-ramda and keep the imports as they were; the plugin than handles the transform.

The great Gatsby

March 17, 2018

This blog now runs with gatsby.js I mainly worked with angular 1 & 2 in the frontend over the last years, but I'm keen to use more react. I love the simplicity and freedom of react and if I could, I would totally rewrite our whole app@work with react .. since I can't do that at the moment without getting into trouble with my super, I revived this blog using gatsby which uses react under the hood. I hope I find the time to write some posts about javascript, functional programming, our smarthome and the control panel I'm developing for it using react.

The blog uses uses the following gatsby plugins:

They all were easy to setup according to their READMEs.

A small change I added in this first version of the blog is the published attribute of blog posts. I want to posts only to be visible if they have a published: true metadata entry. In the local development version of the blog, I want to see all posts - no matter if they are published or not.

To achieve this, I added the published-tag to one of my two test posts like this:

---
path: "/blog/the-great-gatsby"
date: "2018-03-15"
title: "The great Gatsby"
published: true
---

This blog now runs ...

Now I have to tell gatsby to show draft posts in development mode. To achieve this, I added a file .env.development in the root of the project (this is described in the docs as well). gatsby makes these environment variables available automagically. My .env.development looks like this:

GATSBY_SHOW_DRAFTS=true

To fetch and display the blog posts I used the exact code from the gatsby docs. To enable the use of the published tag, I modified it just a litte and added another filter:

IndexPage = ({ data: { allMarkdownRemark: { edges } } }) => {
  const Posts = edges
    .filter(edge => !!edge.node.frontmatter.date)
    .filter(
      edge =>
        process.env.GATSBY_SHOW_DRAFTS || !!edge.node.frontmatter.published
    )
    .map(edge => <Post key={edge.node.id} post={edge.node} />);

  return <div>{Posts}</div>;
};

If the environment variable GATSBY_SHOW_DRAFTS=true is set to true, this filter rejects no posts all posts are shown in dev mode. If the variable is not set, it only returns posts where published is true.

Lastly, we have to change the graphql query to fetch the published tag:

export const pageQuery = graphql`
  query IndexQuery {
    allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) {
      edges {
        node {
          id
          html
          excerpt(pruneLength: 250)
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            path
            title
            published
          }
        }
      }
    }
  }
`;