Thank you everyone who contributed to this huge release! Shoutout to @okwolf, @andyrj, @rajaraodv, @Mytrill, @Swizz, @lukejacksonn, @zaceno and everyone else I forgot to mention! 🎉🙇
Hopefully, this will be our last release before the better-late-than-never, long-awaited, infamous & scandalous 1.0.
What's new?
A lot has changed and here's everything you need to know to get up to speed with the latest and greatest Hyperapp.
- Remove events, emit and mixins.
- Introduce state slices.
- Easier immutable and deeply nested state updates.
- Out-of-the-box hydration.
- Less lifecycle boilerplate.
Hyperapp uses a single state tree — that is, this single object contains all your application level state and serves as the single source of truth. This also means, if you are coming from Redux/Flux, that you have only one store for each app. A single state tree makes it straightforward to locate a specific piece of state, and allows for incredibly easy debugging.
A single state tree is not free from struggle. It can be daunting to update a part of the state deeply nested in the state tree immutably and without resorting to functional lenses / setters or advanced martial arts.
State slices attempt to address this issue by giving you via actions, a slice of the state tree that corresponds to the namespace where both state and action are declared.
actions: {
hello(state) {
// The state is the global `state`.
},
foo: {
bar: {
howdy(state) {
// The state is: `state[foo][bar]`
}
}
}
}
State slices allow you to update deeply nested state easily and immutably.
For example, before when you had something like this:
state: {
foo: {
bar: {
value: 0,
anotherValue: 1
}
}
}
...and wanted to update value
, you had to update an entire record (including siblings), since there is no way to single out value
from a nested state.
In other words, you had to write something like the following in order to update the tree immutably.
actions: {
updateValue(state) {
return {
foo: {
bar: {
value: state.foo.bar.value + 1,
anotherValue: state.foo.bar.anotherValue
}
}
}
}
}
With state slices, it's possible to update value
more simply. In order to do this, your state must look like this.
state: {
foo: {
bar: {
value: 0,
anotherValue: 1
}
}
}
And have a corresponding action inside a namespace that matches the state you want to update.
actions: {
foo: {
bar: {
updateValue(state) {
// State is `state[foo][bar]`
return { value: state.value + 1 }
}
}
}
}
Here is another example with a component.
/* 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} />
)
})
The counter is defined completely oblivious of the rest of your app. It exports an object with the state and actions that describe how it can be operated on and a component, Counter
that describes how it should look like.
On the app side, your job is just to wire things up and kick it off. It's alive!
This release bids farewell to events. So, what's life going to look like without them? The app()
now returns your actions, wired to the state update mechanism, ready to go.
const actions = app({
// Your app here!
})
Register global DOM event listeners using addEventListener
, download / fetch stuff from a remote end point, create a socket connection and essentially do the things you would normally use events.load
for right here.
actions.doSomethingFunky()
Can you show me a real example? Yes, try it online here.
const { move } = app({
state: { x: 0, y: 0 },
view: state => state.x + ", " + state.y,
actions: {
move: (state, actions, { x, y }) => ({ x, y })
}
})
addEventListener("mousemove", e =>
move({
x: e.clientX,
y: e.clientY
})
)
What if you prefer keeping all your logic inside your app? That's possible too, just use 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({...})
In the old days, your app had the ability to bootstrap itself up. Now, your app is just a model describing what happens when something else happens exposing a list of "instructions" (we call 'em actions) to the world.
Fair enough! But what about the other events we used to have? Where did events.action
, events.resolve
, events.update
, events.render
, etc. go?
See Higher Order Apps for the answer.
A higher order app (HOA) is an escape hatch for times when app()
doesn't cut it, but mostly a pattern for tool authors to enable some of the things that were previously possible using the now-gone events
.
A HOA is not a new concept and it was very much possible to create them before this release, but it's now simpler. For starters, a HOA is a function that receives the app
function and returns a new app
function.
It looks like this.
function doNothing(app) {
return props => app(props)
}
And it's used like this.
app(doNothing)({
// Your app here!
})
Calling app()
with doNothing
returns a new app()
function that can be used in the same way as usual, as well as to create a new HOA.
app(doNothing)(doThis)(doThat)({
// Your app here!
})
In practice, if you are authoring a HOA you'll use something like this.
function doNothing(app) {
return props => {
return app(enhance(props))
function enhance(props) {
// Enhance your props here.
}
}
}
The props
argument refer to the same properties that are passed to the app, the usual suspects: state
, actions
, view
and root
.
Hydration is a perceived performance and search engine optimization technique where you can turn statically rendered DOM nodes into an interactive application.
In the old days, to enable hydration you used events.load
to return a VNnode that corresponded to a server-side-rendered DOM tree. Now, you just sit down and do nothing. Hydration is now built-in & free. Hyperapp now works transparently with SSR and pre-rendered HTML, enabling SEO optimization and improving your sites time-to-interactive. The server-side part of the equation still consists of serving a fully pre-rendered page together with your application.
How does it work? We check if there are any children elements in the supplied root
(or look in document.body
if none is given) and assume you rendered them on the server.
If your root
is already populated with other elements we don't know about, you will have to provide a different root
otherwise Hyperapp will obliterate them. 🔥🎉
The onremove
lifecycle/VDOM event can return a function that takes a remove
function, so you don't have to remove the element inside onremove
by yourself anymore. See #357 for the origin story.
function AnimatedButton() {
return (
<div
onremove={element => remove => fadeout(element).then(remove)}
/>
)
}
The tl;dr is that mixins have been removed from this release. They are gone & done.
Mixins had their fair share of supporters and were not always considered harmful, but they were often abused and used to introduce implicit dependencies.
Say you had two mixins, Cheese
and Burger
. You wire them to your app and end up with new state and actions. So far so good. But because actions received the global state (and global actions), it was easy to abuse the system.
Burger
's actions could call any actions defined in a Cheese
. In this way, Burger
depends on Cheese
. This kind of dependency is known as an implicit dependency. If you take out the Cheese
, Burger
breaks. Not to mention, how hard it is to test and debug Burger
.
But this release also introduces State Slices, which would effectively prevent mixin from communicating with each other and begs the question: what harm can they cause now? Have a look at the following code.
const Burger = {
state: {
burger: {
isVegan: true
}
},
actions: {
burger: {
toggleVegan(state, actions) {
return { isVegan: !state.isVegan }
}
}
}
}
app({
mixins: [Burger]
})
The problem with this mixin is that the author would be forced to define the mixin's namespace and the state ends up looking more complex than it should, defeating the purpose of state slices. This approach is also prone to name collisions. It also makes it impossible for users to rename the mixins's namespace or have multiple burgers.
So how does life look like after mixins?
// 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
}
})
But this is more verbose. True. But it's also transparent and clear where and how we are wiring Burger
's state and actions to our app's state and actions. There is no internal magic to merge your state with Burger
's and anyone looking at the code can guess what's happening without having to look at Burger
's source code or documentation.
Source code(tar.gz)
Source code(zip)