0.14.0
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.
State Slices
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} />
)
})
Events
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
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
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.
Lifecycle
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
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.
Acknowledgements
Thanks to @okwolf, @andyrj, @rajaraodv, @Mytrill, @Swizz, @lukejacksonn, @zaceno. 🎉