Releases: jorgebucaran/hyperapp
2.0.0
Hyperapp 2.0 brings a host of new features and improvements, empowering you to build more efficient and feature-rich web apps:
- We're excited to introduce our new Tutorial, enhanced Examples, and comprehensive Reference.
- New Features: Hyperapp 2.0 introduces several new features, including Effects, Subscriptions, and an enhanced Dispatch mechanism.
- Explore the flowchart to gain a clearer insight into Hyperapp's workflow. 🔍
Features
- h() and text(): The
h()
function creates virtual DOM nodes (VNodes) for defining views, whiletext()
generates text content nodes for insertion. - Memoization: Hyperapp 2.0 offers a memoization feature through the
memo()
function to optimize rendering performance. - App Initialization: You can initialize your app by setting the initial state, running a list of effects, or directly invoking an action.
- Mounting: Hyperapp 2.0 allows you to specify the DOM element where the virtual DOM will be mounted.
- Effects: Effects in Hyperapp 2.0 provide a structured way to manage impure code, such as making API calls, handling animations, and more. Effects enhance the predictability and maintainability of your applications.
- Subscriptions: Subscriptions simplify handling external events and resource management, ensuring safe, pure, and immutable interactions with the outside world.
- Custom Subscriptions: Create custom subscriptions when official packages do not meet your app's requirements.
- Dispatch: The dispatch function controls the core dispatching process, including executing actions, applying state transitions, and running effects.
- Custom Dispatch: You can create custom dispatch initializers to customize and augment the dispatching process for debugging and telemetry.
- Custom JSX Function: Although Hyperapp 2.0 does not natively support JSX, you can use a custom JSX function to enable JSX syntax.
Acknowledgements
An immense thank you to @zaceno, @okwolf, @icylace, @sergey-shpak, @SkaterDad, and @lukejacksonn. Your contributions have been invaluable. ❤️
1.2.9
- We have added support for both object and string values in the style attribute, addressing this change in issue (#756).
- Achieving more consistent behavior between client and server rendering for reserved DOM attributes like
spellcheck
,draggable
,translate
, etc., is credited to @frenzzy. This improvement is discussed in issue (#629). - The performance of the keyed algorithm has been enhanced, resulting in fewer "insertBefore" calls when a keyed item is removed, but other items remain in the same order. This enhancement was contributed by @SkaterDad and is detailed in issue (#663).
1.2.0
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.
- CSS Variables Support: Added support for CSS variables in inline styles (#612)
- Improved source readability: Multiple contributors enhanced the code's readability.
- Delegated DOM events: Enhanced performance by delegating DOM events to a proxy (#640).
- Introduced lazy components: New feature for treating functions as virtual nodes (#606).
// Just a component const MyComponent = (props) => <div>{props.value}</div> // Lazy component const MyComponent = (props) => (state, actions) => <div>{state.counter.value}</div>
- Enabled
<datalist>
: Now you can use<datalist>
(#622). - Improved recycling/hydration behavior: Invoked oncreate instead of onupdate during recycling/hydration (#614).
- Avoided mutating initial state: Initial state is no longer mutated (#614).
Acknowledgements
Thanks go to @frenzzy for their work on @hyperapp/render. Thank you @okwolf for @hyperapp/fx and @zaceno for @hyperapp/transitions! 🎉
1.1.2
1.1.0
- Rewrote and consolidated the documentation into a single README file for easier reference and understanding.
- Updated the internal VNode schema, including renaming
name
tonodeName
andprops
toattributes
. This brings Hyperapp's virtual nodes in line with Preact's, facilitating easier integration. - Enhanced VNodes to now store the
key
, improving their compatibility with a variety of libraries.
Acknowledgments
@O4epegb, @ChristianHarms, @sndstudy, @CanyonTurtle, @eschaefer, and @dance2die, thank you! 🙌
1.0.0
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!
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
0.15.0
In this release of Hyperapp, we bring you substantial breaking changes, improvements, bug fixes, and, believe it or not, a reduced bundle size (1397B).
Init Function (#406)
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 (#406)
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.
Higher-Order Apps (HOAs)
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)))({ ... })
Container
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")
)
Acknowledgements
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
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. 🎉
0.12.1
- Improved the updating of text nodes by directly setting
element.nodeValue
, which reduces unnecessary garbage collection in scenarios like DBMon. Thanks to @andyrj for this improvement.set element.nodeValue = node
- Updated the
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 })) ) } } }
- Enabled mixins without options to be defined as simple objects. This simplifies the extension of application state, actions, or events.
const simpleMixin = { events: { load() { console.log("It works!") } } }
Acknowledgements
Thanks to @okwolf, @Swizz, @zaceno, @lukejacksonn, @SkaterDad, and @andyrj for their contributions.