1kB-ish JavaScript framework for building hypertext applications
Hyperapp 2.0 brings a host of new features and improvements, empowering you to build more efficient and feature-rich web apps:
h()
function creates virtual DOM nodes (VNodes) for defining views, while text()
generates text content nodes for insertion.memo()
function to optimize rendering performance.An immense thank you to @zaceno, @okwolf, @icylace, @sergey-shpak, @SkaterDad, and @lukejacksonn. Your contributions have been invaluable. :heart:
spellcheck
, draggable
, translate
, etc., is credited to @frenzzy. This improvement is discussed in issue (#629).Hyperapp 1.2.0 introduces exciting new features, bug fixes, performance enhancements, and improved documentation. Notably, Lazy Components revolutionize component creation by effortlessly connecting them to the global state and actions.
// Just a component
const MyComponent = (props) => <div>{props.value}</div>
// Lazy component
const MyComponent = (props) => (state, actions) =>
<div>{state.counter.value}</div>
<datalist>
: Now you can use <datalist>
(#622).Thanks go to @frenzzy for their work on @hyperapp/render. Thank you @okwolf for @hyperapp/fx and @zaceno for @hyperapp/transitions! 🎉
Fix bug caused when removing empty text nodes from a list (#584).
name
to nodeName
and props
to attributes
. This brings Hyperapp's virtual nodes in line with Preact's, facilitating easier integration.key
, improving their compatibility with a variety of libraries.@O4epegb, @ChristianHarms, @sndstudy, @CanyonTurtle, @eschaefer, and @dance2die, thank you! 🙌
Hyperapp is a JavaScript library designed for building high-performance web applications in the browser. It offers an out-of-the-box combination of state management and a Virtual DOM engine, all without any external dependencies.
Getting started is easy:
import { app } from "hyperapp"
import { state, actions, view } from "./app"
const main = app(state, actions, view, document.body)
For comprehensive documentation and live examples, visit our documentation and the official CodePen account.
With the release of version 1.0, Hyperapp's API has evolved to a significant milestone in its development journey. We extend our gratitude to the contributors who have dedicated their time and expertise to this project. Thank you for your invaluable contributions!
To stay updated and join the conversation, check out the discussion on /r/javascript and read the accompanying blog post!
In this release, we've enhanced Hyperapp with several improvements. Immutable state management ensures efficient memoization and debugging capabilities, while short-circuiting Vnodes optimizes component rendering. Additionally, a refined action signature enhances API clarity, making Hyperapp even more accessible for everyone.
With the addition of modules, the state was no longer immutable. When an action, nested action, or an action in a module was called, its result merged with the current state (or state slice for nested actions/modules). This prevented users from using memoize functions on the view with simple ===
checks for equality and forced us to use deep equality, which can be expensive or challenging to implement.
The benefits of having cost-effective memoize functions based on immutable state outweigh the cost of maintaining an immutable state (both in terms of performance and source code size). This change allows:
===
).Now, when an action gets executed, the result is merged into the state slice/module's state this action operates on and creates a new instance of the state, reusing the part of the state that hasn't been touched.
This change introduces a check in patch()
to return early when oldNode
equals newNode
. This covers two use cases. The first is when both nodes are text nodes, and there is no need to change the element's text content. The second is when both nodes refer to the same object reference, which may occur when memoizing components.
Memoizing components saves vnode creation CPU and, as a bonus, skips patching the element (updating its children). This optimization is similar to React's shouldComponentUpdate
optimizations but provided out of the box and for free.
The reason this is possible is that our components are always pure, and thus memoizing them involves a straightforward prop check.
Read more about this change and see screenshots here.
Change the signature of actions from (state, actions, data)
to (state, actions) => (data)
.
This change makes it easier to distinguish Hyperapp's pre-wired state and actions from your action implementation (data). It also improves API elegance.
// Reducers
const actions = {
setValue: (state) => (value) => ({ value }),
incrementValue: (state) => (value) => ({ value: state.value + value }),
}
// Effects
const actions = {
downloadStuff: (state, actions) => (url) => {
fetch(url)
.then((data) => data.json())
.then(actions.setValue)
},
}
To explain the rationale behind this, let's remember how we used to define actions before.
myAction: (state, actions, data) => { ... }
And then how we used to call those actions somewhere else.
actions.myAction(data)
In other words, the signature of the implementation was different from the action called.
(data) => { ... }
Our new API ameliorates the situation. It doesn't eliminate any possible confusion that could arise completely, but we believe it helps you better reason about actions and as a bonus, it improves API symmetry.
Thunks, a feature that allowed unlocking a special update
function by returning a function inside actions, has been removed. You can achieve the same by calling one or more actions inside other actions. These kinds of actions are usually referred to as "effects."
Thunks were introduced to enhance Hyperapp when the events API was in place. With events now gone, thunks became less prominent and were mainly used as a secondary mechanism to update the state. We believe there should be one great way to do things, not many ways.
Goodbye thunks! 👋
setTimeout
Instead of requestAnimationFrame
for Debouncing of Actions Fired in SuccessionUsing requestAnimationFrame
(rAF) for debouncing causes issues with apps running in the background or inactive tabs. Because an interval keeps running even when a tab is blurred, we've switched to setTimeout
.
When setTimeout
is called, our render
function is placed on a queue and scheduled to run at the next opportunity, not immediately. More importantly, the currently executing code will complete before functions on the queue are executed. This allows us to debounce sync actions called in rapid succession.
You can still use rAF
directly in your application when you need to optimize animations, etc.
While still experimental, this feature allows you to call actions inside the view function while the new node is being computed, but before we patch the DOM.
This means that when you are done computing the vnode, you may have an invalid state (if you called actions inside the view). This feature allows us to skip patching in this situation, because we know we'll be back immediately.
init
FunctionWe've removed props.init
and returned to using the actions
object returned by the app()
call to subscribe to global events, etc.
const actions = app({
state,
actions,
view,
})
// Subscribe to global events, start timers, fetch stuff, and more!
actions.theWorldIsYours()
For example, try it here:
const { tick } = app({
state: {
time: Date.now(),
},
view: (state) => <Clock time={state.time} />,
actions: {
tick: () => ({
time: Date.now(),
}),
},
})
setInterval(tick, 1000)
The lifecycle/vdom event onremove
now receives a done
function as the 2nd argument. You may call this function to inform Hyperapp that you are done with your business and it can remove the element. If you don't call the function, the element will not be removed.
function MessageWithFadeout({ title }) {
return (
<div onremove={(element, done) => fadeout(element).then(done)}>
<h1>{title}</h1>
</div>
)
}
Using Hyperapp as an ES module was already possible, but now it's easier because the entire source code, all 300 lines of it, resides in a single file. This means you can import hyperapp/hyperapp/src/index.js from a service like rawgit that serves directly from GitHub with the right Content-Type headers.
<html>
<head>
<script type="module">
import {
h,
app,
} from "https://rawgit.com/hyperapp/hyperapp/master/src/index.js"
app({
view: (state) => h("h1", {}, "Hello World!"),
})
</script>
</head>
</html>
@Mytrill @Swizz @vdsabev @andyrj @SahAssar @pockethook @okwolf @SkaterDad @Pyrolistical @rajaraodv @zaceno
In this release of Hyperapp, we bring you substantial breaking changes, improvements, bug fixes, and, believe it or not, a reduced bundle size (1397B).
We've introduced the init(state, actions)
function, which serves as a replacement for handling actions, subscribing to global events, or initializing your app. It simplifies the setup process and allows self-contained libraries like routers or interop interfaces to be exposed as modules without imposing excessive boilerplate on users. You can use the init
function to subscribe to global events, start timers, fetch resources, and more.
app({
init(state, actions) {
// Subscribe to global events, start timers, fetch resources, and more!
},
})
Modules provide a way to encapsulate your application behavior into reusable parts, making it easier to share or organize your code. They are similar to mixins but without their drawbacks. Modules are scoped to a specific state/action slice, preventing implicit dependencies and namespace clashes. This feature promotes code transparency and maintains the benefits of a single state tree architecture.
const foo = {
state: { value: 1 },
}
app({
init(state) {
console.log(state) // => { foo: { value: 1 } }
},
modules: { foo },
})
Modules can also have their own modules, creating a nested structure:
const bar = {
state: { value: 1 },
}
const foo = {
modules: { bar },
}
app({
init(state) {
console.log(state) // => { foo: { bar: { value: 1 } } }
},
modules: { foo },
})
Actions inside a module can only call actions within their module or those exposed by modules underneath. This provides a structured approach to managing dependencies, similar to passing props from parent to child components.
Built-in support for HOAs has been removed in favor of a DIY approach. This change offers greater flexibility and diversifies the ecosystem while simplifying the core of Hyperapp. HOAs are no longer a core feature, allowing us to focus on improving Hyperapp itself.
// Before
app(A)(B)(C)({ ... })
// Now
C(B(A(app)))({ ... })
To specify a different rendering element than document.body
, pass the element to the app(props, container)
function in the second argument.
app(props, container)
If you were using props.root
, you'll need to update your code (#410).
app({
view,
state,
actions,
- root: document.getElementById("app"),
},
+ document.getElementById("app")
)
We extend our gratitude to everyone who contributed to this second release before 1.0! @Mytrill @Swizz @okwolf @pspeter3 @lukejacksonn @zaceno @johanalkstal @selfup @vdsabev @Kenota
Simplified state management with state slices, replaced events with direct DOM event handling, introduced HOAs for extensibility, and added built-in hydration for interactivity. Streamlined element removal in the onremove
event and removed mixins for code clarity.
Hyperapp traditionally used a single state tree, which means that all your application-level state resides in a single object, serving as the single source of truth. This approach simplifies state management and debugging. However, updating deeply nested state immutably could be challenging without functional lenses or advanced techniques.
State slices address this challenge by providing a slice of the state tree via actions, corresponding to the namespace where both state and action are declared. Here's an example:
actions: {
hello(state) {
// The state is the global `state`.
},
foo: {
bar: {
howdy(state) {
// The state is: `state[foo][bar]`
}
}
}
}
State slices make it easy to update deeply nested state immutably. For instance, updating a value like this:
state: {
foo: {
bar: {
value: 0,
anotherValue: 1
}
}
}
Previously required updating the entire record, including siblings. With state slices, you can update value
more simply:
state: {
foo: {
bar: {
value: 0,
anotherValue: 1
}
}
}
And have a corresponding action inside a matching namespace:
actions: {
foo: {
bar: {
updateValue(state) {
// State is `state[foo][bar]`
return { value: state.value + 1 }
}
}
}
}
State slices also work with components. For example:
/* counter.js */
import { h } from "hyperapp"
export const counter = {
state: {
value: 0
},
actions: {
up(state, actions) {
return { value: state.value + 1 }
}
}
}
export function Counter(props) {
return (
<main>
<h1>{props.value}</h1>
<button onclick={props.up}>1UP</button>
</main>
)
}
/* index.js */
import { counter, Counter } from "./counter"
app({
state: {
counter: counter.state
},
actions: {
counter: counter.actions
},
view: (state, actions) => (
<Counter value={state.counter.value} up={actions.counter.up} />
)
})
This release bids farewell to events. Instead, app()
now returns your actions wired to the state update mechanism, ready to go. You can register global DOM event listeners, fetch data, create socket connections, and perform tasks you would typically use events for.
For example:
const actions = app({
// Your app here!
})
You can also handle events directly within your app using actions:
app({
view(state, actions) { /* ... */ },
state: {
repos: [],
isFetching: false,
org: "hyperapp"
},
actions: {
toggleFetching(state) { /* ... */ },
populate(state, actions, repos) { /* ... */ },
load(state, actions) {
actions.toggleFetching()
fetch(`https://api.github.com/orgs/${state.org}/repos?per_page=100`)
.then(repos => repos.json())
.then(repos => actions.populate(repos) && actions.toggleFetching())
}
}
}).load({...})
Higher Order Apps (HOAs) are a way to extend Hyperapp's functionality. A HOA is a function that receives the app
function and returns a new app
function. It allows tool authors to enable features that were previously possible using events.
Here's how it works:
function doNothing(app) {
return props => app(props)
}
And it's used like this:
app(doNothing)({
// Your app here!
})
In practice, HOAs can be used to enhance app functionality:
function doNothing(app) {
return props => {
return app(enhance(props))
function enhance(props) {
// Enhance your props here.
}
}
}
Hydration is now built-in and free in Hyperapp. It allows you to turn statically rendered DOM nodes into an interactive application. Hydration works transparently with server-side rendering (SSR) and pre-rendered HTML, improving SEO optimization and time-to-interactive.
The onremove
lifecycle/VDOM event can now return a function that takes a remove
function, simplifying element removal.
function AnimatedButton() {
return (
<div
onremove={element => remove => fadeout(element).then(remove)}
/>
)
}
Mixins have been removed in this release. Instead, we recommend a more explicit approach to state and actions management to improve code clarity and avoid implicit dependencies.
For example:
// burger.js
export const burger = {
state: {
isVegan: 0
},
actions: {
toggleVegan(state, actions) {
return { isVegan: !state.isVegan }
}
}
}
// index.js
import { burger } from "./burger"
app({
state: {
burger: burger.state
},
actions: {
burger: burger.actions
}
})
This approach may be more verbose but is clear and transparent, preventing implicit dependencies and making your code easier to understand.
Thanks to @okwolf, @andyrj, @rajaraodv, @Mytrill, @Swizz, @lukejacksonn, @zaceno. 🎉
element.nodeValue
, which reduces unnecessary garbage collection in scenarios like DBMon. Thanks to @andyrj for this improvement.
set element.nodeValue = node
update
function in thunks to accept a reducer function, enhancing its usefulness in asynchronous processes. This allows for state adjustments during an action's execution.
actions: {
someAsyncAction(state) {
return update => {
longTaskWithCallback(someData, () =>
update(state => ({ value: state.value + 1 }))
)
}
}
}
const simpleMixin = {
events: {
load() {
console.log("It works!")
}
}
}
Thanks to @okwolf, @Swizz, @zaceno, @lukejacksonn, @SkaterDad, and @andyrj for their contributions.