0.16.0
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.
Immutable State (#425)
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:
- Debugging tools to keep the state history and implement time traveling.
- Performant memoization of components/views on large apps (deep equality on state can be checked simply with
===
). - Check out this Example by @Mytrill!
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.
Short Circuit Vnodes
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.
Return a Function to Access Data Argument Inside Actions (#448)
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.
Remove Thunks
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! 👋
Use setTimeout
Instead of requestAnimationFrame
for Debouncing of Actions Fired in Succession
Using 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.
Don't Lock Patching Inside View Function
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.
Remove init
Function
We'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)
Pass Done/Remove Function to OnRemove as the 2nd Argument
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>
)
}
Easier to Use as an ES Module
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>
Acknowledgments
@Mytrill @Swizz @vdsabev @andyrj @SahAssar @pockethook @okwolf @SkaterDad @Pyrolistical @rajaraodv @zaceno