Hyperapp: The 1 KB JavaScript Library for Building Front-End Apps

Share this article

A vaulted cellar

Hyperapp is a JavaScript library for building feature-rich web applications. It combines a pragmatic Elm-inspired approach to state management with a VDOM engine that supports keyed updates & lifecycle events — all without dependencies. Give or take a few bytes, the entire source code minified and gzipped sits at around 1 KB.

In this tutorial, I’ll introduce you to Hyperapp and walk you through a few code examples to help you get started right away. I’ll assume some familiarity with HTML and JavaScript, but previous experience with other frameworks is not required.

Hello World

We’ll start with a simple demo that shows all the moving parts working together.

You can try the code online too.

import { h, app } from "hyperapp"
// @jsx h

const state = {
  count: 0
}

const actions = {
  down: () => state => ({ count: state.count - 1 }),
  up: () => state => ({ count: state.count + 1 })
}

const view = (state, actions) => (
  <div>
    <h1>{state.count}</h1>
    <button onclick={actions.down}>-</button>
    <button onclick={actions.up}>+</button>
  </div>
)

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

This is more or less how every Hyperapp application looks like. A single state object, actions that populate the state and a view that translates state and actions into a user interface.

Inside the app function, we make a copy of your state and actions (it would be impolite to mutate objects we don’t own) and pass them to the view. We also wrap your actions so they re-render the application every time the state changes.

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

The state is a plain JavaScript object that describes your application data model. It’s also immutable. To change it you need to define actions and call them.

const state = {
  count: 0
}

Inside the view, you can display properties of the state, use it to determine what parts your UI should be shown or hidden, etc.

<h1>{state.count}</h1>

You can also attach actions to DOM events, or call actions within your own inlined event handlers.

<button onclick={actions.down}>-</button>
<button onclick={actions.up}>+</button>

Actions don’t mutate the state directly but return a new fragment of the state. If you try to mutate the state inside an action and then return it, the view will not be re-rendered as you might expect.

const actions = {
  down: () => state => ({ count: state.count - 1 }),
  up: () => state => ({ count: state.count + 1 })
}

The app call returns the actions object wired to the state-update view-render cycle. You also receive this object inside the view function and within actions. Exposing this object to the outside world is useful because it allows you to talk to your application from another program, framework or vanilla JavaScript.

const main = app(state, actions, view, document.body)

setTimeout(main.up, 1000)

A note about JSX

I’ll be using JSX throughout the rest of this document for familiarity, but you aren’t required to use JSX with Hyperapp. Alternatives include the built-in h function, @hyperapp/html, hyperx and t7.

Here is the same example from above using @hyperapp/html.

import { app } from "hyperapp"
import { div, h1, button } from "@hyperapp/html"

const state = { count: 0 }

const actions = {
  down: () => state => ({ count: state.count - 1 }),
  up: () => state => ({ count: state.count + 1 })
}

const view = (state, actions) =>
  div([
    h1(state.count),
    button({ onclick: actions.down }, "–"),
    button({ onclick: actions.up }, "+")
  ])

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

Virtual DOM

A virtual DOM is a description of what a DOM should look like, using a tree of nested JavaScript objects known as virtual nodes.

{
  name: "div",
  props: {
    id: "app"
  },
  children: [{
    name: "h1",
    props: null,
    children: ["Hi."]
  }]
}

The virtual DOM tree of your application is created from scratch on every render cycle. This means we call the view function every time the state changes and use the newly computed tree to update the actual DOM.

We try to do it in as few DOM operations as possible, by comparing the new virtual DOM against the previous one. This leads to high efficiency, since typically only a small percentage of nodes need to change, and changing real DOM nodes is costly compared to recalculating a virtual DOM.

To help you create virtual nodes in a more compact way, Hyperapp provides the h function.

import { h } from "hyperapp"

const node = h(
  "div",
  {
    id: "app"
  },
  [h("h1", null, "Hi.")]
)

Another way to create virtual nodes is with JSX. JSX is a JavaScript language extension used to represent dynamic HTML.

import { h } from "hyperapp"

const node = (
  <div id="app">
    <h1>Hi.</h1>
  </div>
)

Browsers don’t understand JSX, so we need to compile it into h function calls, hence the import h statement. Let’s see how this process works using babel.

First, install dependencies:

npm i babel-cli babel-plugin-transform-react-jsx

Then create a .babelrc file:

{
  "plugins": [
    [
      "transform-react-jsx",
      {
        "pragma": "h"
      }
    ]
  ]
}

And compile the code from the command line:

npm run babel src/index.js > index.js

If you prefer not to use a build system, you can also load Hyperapp from a CDN like unpkg and it will be globally available through the window.hyperapp object.

Examples

Gif Search Box

In this example, I’ll show you how to update the state asynchronously using the Giphy API to build a Gif search box

To produce side effects we call actions inside other actions, within a callback or when a promise is resolved.

Actions that return null, undefined or a Promise object don’t trigger a view re-render. If an action returns a promise, we’ll pass the promise to the caller allowing you to create async actions like in the following example.

Live Example

import { h, app } from "hyperapp"
// @jsx h

const GIPHY_API_KEY = "dc6zaTOxFJmzC"

const state = {
  url: "",
  query: "",
  isFetching: false
}

const actions = {
  downloadGif: query => async (state, actions) => {
    actions.toggleFetching(true)
    actions.setUrl(
      await fetch(
        `//api.giphy.com/v1/gifs/search?q=${query}&api_key=${GIPHY_API_KEY}`
      )
        .then(data => data.json())
        .then(({ data }) => (data[0] ? data[0].images.original.url : ""))
    )
    actions.toggleFetching(false)
  },
  setUrl: url => ({ url }),
  setQuery: query => ({ query }),
  toggleFetching: isFetching => ({ isFetching })
}

const view = (state, actions) => (
  <div>
    <input type="text"
      placeholder="Type here..."
      autofocus
      onkeyup={({ target: { value } }) =/> {
        if (value !== state.query) {
          actions.setQuery(value)
          if (!state.isFetching) {
            actions.downloadGif(value)
          }
        }
      }}
    />
    <div class="container">
      <img src={state.url}
        style={{
          display: state.isFetching || state.url === "" ? "none" : "block"
        }}
      />
    </div>
  </div>
)

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

The state stores a string for the Gif URL, the search query and a boolean flag to know when the browser is fetching a new Gif.

const state = {
  url: "",
  query: "",
  isFetching: false
}

The isFetching flag is used to hide the Gif while the browser is busy. Without it, the last downloaded Gif would be shown as another one is requested.

<img src={state.url}
  style={{
    display: state.isFetching || state.url === "" ? "none" : "block"
  }}
/>

The view consists of a text input and an img element to display the Gif.

To handle user input, the onkeyup event is used, but onkeydown or oninput would work as well.

On every keystroke actions.downloadGif is called and a new Gif is requested, but only if a fetch is not already pending and the text input is not empty.

if (value !== state.query) {
  actions.setQuery(value)
  if (!state.isFetching) {
    actions.downloadGif(value)
  }
}

Inside actions.downloadGif we use the fetch API to request a Gif URL from Giphy.

When fetch is done, we receive the payload with the Gif information inside a promise.

actions.toggleFetching(true)
actions.setUrl(
  await fetch(
    `//api.giphy.com/v1/gifs/search?q=${query}&api_key=${GIPHY_API_KEY}`
  )
    .then(data => data.json())
    .then(({ data }) => (data[0] ? data[0].images.original.url : ""))
)
actions.toggleFetching(false)

Once the data has been received, actions.toggleFetching is called (which allows further fetch requests to be made) and the state is updated by passing the fetched Gif URL to actions.setUrl.

TweetBox Clone

In this example, I’ll show you how to create custom components to organize your UI into reusable markup and build a simple TweetBox clone.

Live Example

import { h, app } from "hyperapp"
// @jsx h

const MAX_LENGTH = 140
const OFFSET = 10

const OverflowWidget = ({ text, offset, count }) => (
  <div class="overflow">
    <h1>Whoops! Too long.</h1>
    <p>
      ...{text.slice(0, offset)}
      <span class="overflow-text">{text.slice(count)}</span>
    </p>
  </div>
)

const Tweetbox = ({ count, text, update }) => (
  <div>
    <div class="container">
      <ul class="flex-outer">
        <li>
          <textarea placeholder="What's up?" value={text} oninput={update}></textarea>
        </li>

        <li class="flex-inner">
          <span class={count > OFFSET ? "overflow-count" : "overflow-count-alert"}
          >
            {count}
          </span>

          <button onclick={() => alert(text)}
            disabled={count >= MAX_LENGTH || count < 0}
          >
            Tweet
          </button>
        </li>
      </ul>

      {count < 0 && (
        <OverflowWidget
          text={text.slice(count - OFFSET)}
          offset={OFFSET}
          count={count}
        />
      )}
    </div>
  </div>
)

const state = {
  text: "",
  count: MAX_LENGTH
}

const view = (state, actions) => (
  <tweetbox text={state.text}
    count={state.count}
    update={e => actions.update(e.target.value)}
  />
)

const actions = {
  update: text => state => ({
    text,
    count: state.count + state.text.length - text.length
  })
}

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

The state stores the text of the message and the number of remaining characters count, initialized to MAX_LENGTH.

const state = {
  text: "",
  count: MAX_LENGTH
}

The view consists of our TweetBox component. We use the attributes/props, to pass down data into the widget.

const view = (state, actions) => (
  </tweetbox><tweetbox text={state.text}
    count={state.count}
    update={e => actions.update(e.target.value)}
  />
)

When the user types in the input, we call actions.update() to update the current text and calculate the remaining characters.

update: text => state => ({
  text,
  count: state.count + state.text.length - text.length
})

The subtracting the length of the current text from the length of the previous text tells us how the number of remaining characters has changed. Hence the new count of remaining characters is the old count plus the aforementioned difference.

When the input is empty, this operation is equal to (MAX_LENGTH - text.length).

When state.count becomes less than 0, we know that state.text must be longer than MAX_LENGTH, so we can disable the tweet button and display the OverflowWidget component.

<button onclick={() => alert(text)} disabled={count >= MAX_LENGTH || count < 0}>
  Tweet
</button>

The tweet button is also disabled when state.count === MAX_LENGTH, because that means we have not entered any characters.

The OverflowWidget tag displays the unallowed part of the message and a few adjacent characters for context. The constant OFFSET tells us how many extra characters to slice off state.text.

<overflowwidget text={text.slice(count - OFFSET)}
  offset={OFFSET}
  count={count}></overflowwidget>

By passing OFFSET into OverflowWidget we are able to slice text further and apply an overflow-text class to the specific overflowed part.

<span class="overflow-text">{text.slice(count)}</span>

Comparison with React

At a conceptual level, Hyperapp and React have a lot in common. Both libraries use a virtual DOM, lifecycle events, and key-based reconciliation. Hyperapp looks and feels a lot like React and Redux, but with less boilerplate.

React popularized the idea of a view as a function of the state. Hyperapp takes this idea a step further with a built-in, Elm-inspired state management solution.

Hyperapp rejects the idea of local component state relying only on pure functional components. This translates to high reusability, cheap memoization, and simple testing.

Final Thoughts

Because Hyperapp is so tiny, it is faster to transfer over the network and faster to parse than virtually any alternative out there. This means fewer concepts to learn, fewer bugs and more framework stability.

I’ve never been a fan of big frameworks. Not because they aren’t great, but because I want to write my own JavaScript, not the JavaScript a framework wants me to use. The meat of it is I want transferable skills. I want to grow skills in JavaScript, not skills into frameworks.


To learn more about Hyperapp check out the official documentation and follow us on Twitter for updates and announcements.

Frequently Asked Questions (FAQs) about Hyperapp 1KB JavaScript Library

What is the Hyperapp 1KB JavaScript Library?

Hyperapp is a modern, lightweight, and highly efficient JavaScript library for building web applications. It’s just 1KB in size, making it an excellent choice for developers who want to create fast, responsive web applications without the bloat of larger frameworks. Hyperapp combines the simplicity of declarative programming with the efficiency of a Virtual DOM engine, resulting in a user-friendly, high-performance solution for web development.

How does Hyperapp compare to other JavaScript libraries?

Hyperapp stands out from other JavaScript libraries due to its minimal size and simplicity. Despite being just 1KB, it offers a complete solution for building web applications, including state management and a Virtual DOM engine. This makes it a great alternative to larger, more complex libraries like React or Vue, especially for smaller projects or for developers who prefer a more straightforward approach.

How do I get started with Hyperapp?

To get started with Hyperapp, you first need to install it via npm using the command npm install hyperapp. Once installed, you can import it into your JavaScript file using import { h, app } from "hyperapp". From there, you can start building your application using Hyperapp’s simple, declarative syntax.

What are the main features of Hyperapp?

Hyperapp includes several key features that make it a powerful tool for web development. These include a Virtual DOM engine for efficient rendering, a state management system for managing application state, and a simple, declarative syntax that makes it easy to understand and use. Additionally, its small size makes it incredibly fast and efficient, resulting in a smoother user experience.

Can I use Hyperapp with other JavaScript libraries or frameworks?

Yes, Hyperapp can be used alongside other JavaScript libraries or frameworks. However, given its comprehensive feature set, you may find that you don’t need to use other libraries or frameworks in conjunction with Hyperapp. It’s designed to be a complete solution for building web applications, so it includes everything you need out of the box.

Is Hyperapp suitable for large-scale projects?

While Hyperapp’s simplicity and small size make it an excellent choice for smaller projects, it’s also capable of handling larger, more complex applications. Its efficient Virtual DOM engine and state management system ensure that your application remains fast and responsive, even as it grows in size and complexity.

How does Hyperapp handle state management?

Hyperapp includes a built-in state management system that makes it easy to manage and track the state of your application. This system allows you to define a state object that represents the current state of your application, and then update that state in response to user actions or other events.

What is the Virtual DOM in Hyperapp?

The Virtual DOM in Hyperapp is a lightweight copy of the actual DOM. It allows Hyperapp to make changes to the application’s state and UI in a highly efficient manner. When a change is made, Hyperapp updates the Virtual DOM first, then compares it to the actual DOM and makes the necessary updates. This results in faster, more efficient rendering.

Can I use Hyperapp for mobile app development?

While Hyperapp is primarily designed for web development, it can also be used for building mobile applications using technologies like Cordova or PhoneGap. However, for more complex mobile applications, you may want to consider using a dedicated mobile development framework.

Is there a community or resources for learning more about Hyperapp?

Yes, there’s a vibrant community of developers using Hyperapp, and there are many resources available for learning more about it. The official Hyperapp GitHub page is a great place to start, and there are also numerous tutorials, guides, and articles available online.

HyperappHyperapp
View Author

1kB-ish JavaScript framework for building hypertext applications.

frameworkHyperAppnilsonj
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week