Hyperapp is a JavaScript library for building web applications.
- Minimal — Hyperapp was born out of the attempt to do more with less. We have aggressively minimized the concepts you need to understand while remaining on par with what other frameworks can do.
- Functional — Hyperapp's design is inspired by The Elm Architecture. Create scalable browser-based applications using a functional paradigm. The twist is you don't have to learn a new language.
- Batteries-included — Out of the box, Hyperapp combines state management with a Virtual DOM engine that supports keyed updates & lifecycle events — all with no dependencies.
Our first example is a counter that can be incremented or decremented. Go ahead and try it online here.
import { h, app } from "hyperapp"
const state = {
count: 0
}
const actions = {
down: value => state => ({ count: state.count - value }),
up: value => state => ({ count: state.count + value })
}
const view = (state, actions) => (
<div>
<h1>{state.count}</h1>
<button onclick={() => actions.down(1)}>-</button>
<button onclick={() => actions.up(1)}>+</button>
</div>
)
app(state, actions, view, document.body)
This example assumes you are using a JavaScript compiler like Babel or TypeScript and a module bundler like Parcel, Rollup, Webpack, etc. Usually, all you need to do is install the JSX transform plugin and add the pragma option to your .babelrc file.
{
"plugins": [["transform-react-jsx", { "pragma": "h" }]]
}
JSX is a language syntax extension that lets you write HTML tags interspersed with JavaScript. Because browsers don't understand JSX, we use a compiler to transform it into hyperapp.h function calls (hyperscript).
const view = (state, actions) =>
h("div", {}, [
h("h1", {}, state.count),
h("button", { onclick: () => actions.down(1) }, "-"),
h("button", { onclick: () => actions.up(1) }, "+")
])
Note that JSX is not required for building applications with Hyperapp. You can use hyperscript syntax without a compilation step as shown above. Other alternatives to JSX include @hyperapp/html, hyperx and t7.
Install with npm or Yarn.
npm i hyperapp
Then with a module bundler like Rollup or Webpack, use as you would anything else.
import { h, app } from "hyperapp"
If you don't want to set up a build environment, you can download Hyperapp from a CDN like unpkg.com and it will be globally available through the window.hyperapp object. We support all ES5-compliant browsers, including Internet Explorer 10 and above.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/hyperapp"></script>
</head>
</html>
Hyperapp applications consist of three interconnected parts: the State, View, and Actions.
Once initialized, your application executes in a continuous loop, taking in actions from users or from external events, updating the state, and representing changes in the view through a Virtual DOM. Think of an action as a signal that notifies Hyperapp to update the state and schedule the next view redraw. After processing an action, the new state is presented back to the user.
The state is a plain JavaScript object that describes your entire program. It consists of all the dynamic data that is moved around in the application during its execution. The state cannot be mutated once it is created. We must use actions to update it.
const state = {
count: 0
}
Like any JavaScript object, the state can be a nested tree of objects. We refer to nested objects in the state as partial state. A single state tree does not conflict with modularity — see Nested Actions to find out how to update deeply nested objects and split your state and actions.
const state = {
top: {
count: 0
},
bottom: {
count: 0
}
}
The way to change the state is via actions. An action is a unary function (accepts a single argument) expecting a payload. The payload can be anything you want to pass into the action.
To update the state, an action must return a partial state object. An action can also return a function that takes the current state and actions and returns a partial state object. Under the hood, Hyperapp wires every function from your actions to schedule a view redraw whenever the state changes.
const actions = {
down: value => state => ({ count: state.count - value }),
up: value => state => ({ count: state.count + value })
}
If you mutate the state within an action and return it, the view will not be redrawn as you expect. This is because state updates are always immutable. When you return a partial state object from an action, the new state will be the result of a shallow merge between this object and the current state.
Immutability enables time-travel debugging, helps prevent introducing hard-to-track-down bugs by making state changes more predictable, and allows cheap memoization of components using shallow equality === checks.
Actions used for side effects (writing to databases, sending a request to a server, etc.) don't need to have a return value. You may call an action from within another action or callback function. Actions which return a Promise, undefined or null will not trigger redraws or update the state.
const actions = {
upLater: value => (state, actions) => {
setTimeout(actions.up, 1000, value)
},
up: value => state => ({ count: state.count + value })
}
An action can be an async function. Because async functions return a Promise, and not a partial state object, you need to call another action in order to update the state.
const actions = {
upLater: () => async (state, actions) => {
await new Promise(done => setTimeout(done, 1000))
actions.up(10)
},
up: value => state => ({ count: state.count + value })
}
Actions can be nested inside namespaces. Updating deeply nested state is as easy as declaring actions inside an object in the same path as the part of the state you want to update.
const state = {
counter: {
count: 0
}
}
const actions = {
counter: {
down: value => state => ({ count: state.count - value }),
up: value => state => ({ count: state.count + value })
}
}
The app function returns a copy of your actions where every function is wired to changes in the state. Exposing this object to the outside world can be useful to operate your application from another program or framework, subscribe to global events, listen to mouse and keyboard input, etc.
To see this in action, modify the example from Getting Started to save the wired actions to a variable and try using them. You should see the counter update accordingly.
const main = app(state, actions, view, document.body)
setInterval(main.up, 250, 1)
setInterval(main.down, 500, 1)
Every time your application state changes, the view function is called so that you can specify how you want the DOM to look based on the new state. The view returns your specification in the form of a Virtual DOM and Hyperapp takes care of updating the actual DOM to match it.
This operation doesn't replace the entire DOM tree, but only update the parts of the DOM that changed. Incremental updates are calculated by diffing the old and the new Virtual DOM, then patching the DOM to reflect the new version.
const view = (state, actions) =>
h("div", {}, [
h("h1", {}, state.count),
h("button", { onclick: () => actions.down(1) }, "-"),
h("button", { onclick: () => actions.up(1) }, "+")
])
It is important to understand that the result of the view is a new Virtual DOM. It may seem wasteful to throw away the old Virtual DOM and re-create it entirely on every update — not to mention the fact that at any one time, Hyperapp is keeping two Virtual DOM trees in memory, but as it turns out, browsers can create hundreds of thousands of objects very quickly. On the other hand, modifying the DOM is several orders of magnitude more expensive.
A Virtual DOM is a description of what a DOM should look like using a tree of nested JavaScript objects known as Virtual Nodes. The Virtual DOM is a lightweight representation of the DOM.
{
nodeName: "div",
attributes: {},
children: [
{
nodeName: "h1",
attributes: {},
children: 0
},
{
nodeName: "button",
attributes: {
onclick: function() {/*...*/}
},
children: "-"
},
{
nodeName: "button",
attributes: {
onclick: function() {/*...*/}
},
children: "+"
}
]
}
The Virtual DOM allows us to write code as if the entire document is redrawn on each change, while we only update the parts of the DOM that actually changed. We try to do this in the least number of steps 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 the Virtual DOM.
To help you create Virtual Nodes in a more compact way, Hyperapp provides a hyperscript-style h function. The h function takes an element's name or a function that returns a Virtual Node (see Components), optional attributes and optional array of children elements.
import { h } from "hyperapp"
const view = (state, actions) =>
h("div", {}, [
h("h1", {}, state.count),
h("button", { onclick: () => actions.down(1) }, "-"),
h("button", { onclick: () => actions.up(1) }, "+")
])
Supported attributes include HTML attributes, SVG attributes, DOM events, Lifecycle Events, and Keys. Note that non-standard HTML attribute names are not supported, onclick and class are valid, but onClick or className are not.
Hyperapp does not support inline styles as strings, but as an object with style declarations. Each declaration consists of a style written in camelCase and a value.
import { h } from "hyperapp"
export const HelloDiv = (
<div
style={{
color: "white",
margin: "20px",
textAlign: center,
backgroundImage: `url(${imgUrl})`
}}
>
Hello World
</div>
)
If for any reason you don't use the Virtual DOM mechanism and decide to set the innerHTML in an element, you run the risk of cross-site scripting (XSS) vulnerabilities. Specifically you must sanitize any user provided data before writing it out to the DOM. We suggest creating your own replacement function to explicitly state the intent of performing an "unsafe" operation
const dangerouslySetInnerHTML = html => element => {
element.innerHTML = html
}
const ItemContent = ({ item: { url, summary } }) => (
<div class="content">
<a href={url} oncreate={dangerouslySetInnerHTML(summary)} />
</div>
)
A component is a pure function that returns a Virtual Node. Unlike the view function, components are not wired to your application state or actions. Components are dumb, reusable blocks of code that encapsulate markup, styles and behaviors that belong together. Note that when using JSX, components must be capitalized or contain a period in the name.
import { h } from "hyperapp"
const TodoItem = ({ id, value, done, toggle }) => (
<li
class={done && "done"}
onclick={e =>
toggle({
value: done,
id: id
})
}
>
{value}
</li>
)
export const view = (state, actions) => (
<div>
<h1>Todo</h1>
<ul>
{state.todos.map(({ id, value, done }) => (
<TodoItem id={id} value={value} done={done} toggle={actions.toggle} />
))}
</ul>
</div>
)
If you don't know all the attributes that you want to place in a component ahead of time, you can use the spread syntax. Note that Hyperapp components can return multiple elements as in the following example. This technique lets you group a list of children without adding extra nodes to the DOM.
import { h } from "hyperapp"
const TodoItem = ({ id, value, done, toggle }) => (
<li
class={done && "done"}
onclick={e =>
toggle({
value: done,
id: id
})
}
>
{value}
</li>
)
const TodoList = ({ todos, toggle }) =>
todos.map(todo => <TodoItem {...todo} toggle={toggle} />)
export const view = (state, actions) => (
<div>
<h1>Todo</h1>
<ul>
<TodoList todos={state.todos} toggle={actions.toggle} />
</ul>
</div>
)
Components receive their children elements via the second argument.
const Box = ({ color }, children) => (
<div class={`box box-${color}`}>{children}</div>
)
This lets you and other components pass arbitrary children down to them.
const HelloBox = ({ name }) => (
<Box color="green">
<h1 class="title">Hello, {name}!</h1>
</Box>
)
You can be notified when elements managed by the Virtual DOM are created, updated or removed via lifecycle events. Use them for animation, data fetching, wrapping third party libraries, cleaning up resources, etc.
This event is fired after the element is created and attached to the DOM. Use it to manipulate the DOM node directly, make a network request, create a slide/fade in animation, etc.
const Textbox = ({ placeholder }) => (
<input
type="text"
placeholder={placeholder}
oncreate={element => element.focus()}
/>
)
This event is fired every time we update the element attributes. Use oldProps inside the event handler to check if any attributes changed or not.
const Textbox = ({ placeholder }) => (
<input
type="text"
placeholder={placeholder}
onupdate={(element, oldProps) => {
if (oldProps.placeholder !== placeholder) {
// Handle changes here!
}
}}
/>
)
This event is fired before the element is removed from the DOM. Use it to create slide/fade out animations. Call done inside the function to remove the element. This event is not called in its child elements.
const MessageWithFadeout = ({ title }) => (
<div onremove={(element, done) => fadeout(element).then(done)}>
<h1>{title}</h1>
</div>
)
This event is fired after the element has been removed from the DOM, either directly or as a result of a parent being removed. Use it for invalidating timers, canceling a network request, removing global events listeners, etc.
const Camera = ({ onerror }) => (
<video
poster="loading.png"
oncreate={element => {
navigator.mediaDevices
.getUserMedia({ video: true })
.then(stream => (element.srcObject = stream))
.catch(onerror)
}}
ondestroy={element => element.srcObject.getTracks()[0].stop()}
/>
)
Keys help identify which nodes were added, changed or removed from a list when a view is rendered. A key must be unique among sibling-nodes.
const ImageGallery = ({ images }) =>
images.map(({ hash, url, description }) => (
<li key={hash}>
<img src={url} alt={description} />
</li>
))
By setting the key property on a virtual node, you declare that the node should correspond to a particular DOM element. This allow us to re-order the element into its new position, if the position changed, rather than risk destroying it.
Don't use an array index as key, if the index also specifies the order of siblings. If the position and number of items in a list is fixed, it will make no difference, but if the list is dynamic, the key will change every time the tree is rebuilt.
const PlayerList = ({ players }) =>
players
.slice()
.sort((player, nextPlayer) => nextPlayer.score - player.score)
.map(player => (
<li key={player.username} class={player.isAlive ? "alive" : "dead"}>
<PlayerCard {...player} />
</li>
))
Hyperapp works transparently with SSR and pre-rendered HTML, enabling SEO optimization and improving your sites time-to-interactive. The process consists of serving a fully pre-rendered page together with your application.
<html>
<head>
<meta charset="utf-8">
<script defer src="bundle.js"></script>
</head>
<body>
<div>
<h1>0</h1>
<button>-</button>
<button>+</button>
</div>
</body>
</html>
Then instead of throwing away the server-rendered markdown, we'll turn your DOM nodes into an interactive application out of the box.
Hyperapp is MIT licensed. See LICENSE.