Skip to content

RFC: Hyperapp 2.0 #672

Description

@jorgebucaran

Note: This is not an April Fool's Day prank! 😄

Background

After a lot of debate and enduring weeks of inner turmoil, I've decided to move forward with a series of drastic changes that will be eventually released as Hyperapp 2.0.

Predictable as it may be, my plan was to leave things the way they are here and create a new JavaScript framework. A direct competitor to Hyperapp, but pretty much the same under the hood. I am calling that off and going to focus on making a better Hyperapp instead!

Breaking changes again? I am afraid so. I'm unsatisfied with how some things work in Hyperapp and want to fix them to the bone, not start a brand new project. I don't think I will be able to fairly concentrate on two projects that have exactly the same goal when what differentiates them are just some subtle (but important) API differences.

What will change?

With 2.0 I intend to fix several pain points I've experienced with Hyperapp due to its extreme DIY-ness. In the words of @okwolf:

Hyperapp holds firm on the functional programming front when managing your state, but takes a pragmatic approach to allowing for side effects, asynchronous actions, and DOM manipulations.

Hyperapp is minimal and pragmatic and I don't want to change it but in order to improve, we need to do more for the user. So, this is what I am going to focus on:

  • Easy to test — By making actions and effects pure, testing will be a piece of cake.
  • All Things Dynamic — First class support for code splitting and dynamically loading actions and views using import(), e.g., dynamic(import("./future-component")). Fix Dynamic actions: How to add new actions at runtime? #533.
  • Cleaner Action API — Eliminate the confusing concept of "wired actions". The new actions will be regular, unwired & untapped JavaScript functions.
  • Subscriptions — Introduce a subscriptions API inspired by Elm.
  • Make Hyperapp more suitable for multi-app design (running multiple apps on a single page).
  • Types — Make Hyperapp typing simpler and easier to get right.

Show me the money!

The Simple Counter

Let's begin with a simple counter and kick it up a notch afterward.

import { h, app } from "hyperapp"

const down = state => ({ count: state.count - 1 })
const up = state => ({ count: state.count + 1 })

app({
  init: { count: 0 },
  view: state => (
    <div>
      <h1>{state.count}</h1>
      <button onclick={down}>-1</button>
      <button onclick={up}>+1</button>
    </div>
  ),
  container: document.body
})

The biggest surprise here is that you no longer need to pass the actions to the app() call, wait for them to be wired to state changes and receive them inside the view or any of that wacky stuff. It just works.

How to pass data into the action? Just use JavaScript.

You can create a closure that receives the data and returns a function that HA expects num => state => ({ count: state.count + num }) or use the tuple syntax (preferable) as shown below.

const downBy = (state, num) => ({ count: state.count - num })

const view = state => (
  <div>
    <h1>{state.count}</h1>
    <button onclick={down}>-</button>
    <button onclick={up}>+1</button>
    <button onclick={[downBy, 10]}>-10</button>
  </div>
)

This looks very similar to 1.0, the difference is that the curried function is completely justified now — not built-in or forced upon you.

Here's an interesting way you will be able to reset the count.

const view = state => (
  <div>
    <h1>{state.count}</h1>
    <button onclick={{ count: 0 }}>Reset</button>
    <button onclick={down}>-1</button>
    <button onclick={up}>+1</button>
  </div>
)

Yes, you just put the value { count: 0 } there and you're done.

Side Effects

Ok, time to cut to the chase. How are going to do async stuff now?

import { h, app } from "hyperapp"
import { delay } from "@hyperapp/fx"

const up = state => ({ count: state.count + 1 })

// This is how we define an effect (Elm calls it commands).
const delayedUp = delay(1000, up)

app({
  init: { count: 0 },
  view: state => (
    <div>
      <h1>{state.count}</h1>
      <button onclick={up}>+1</button>
      <button onclick={delayedUp}>+1 with delay</button>
    </div>
  ),
  container: document.body
})

What if I want to set the state to something and cause a side effect to happen at the same time? I have you covered (almost). In Elm, they have tuples, but in JavaScript we only have arrays. So, let's use them.

const down = state => ({ count: state.count - 1 })
const up = state => ({ count: state.count + 1 })
const delayedUp = delay(1000, up)
const eventuallyDidNothing = state => [down(state), delayedUp]

Notice that creating a function for actions and effects is A Good Practice, but nothing prevents you from doing this:

const eventuallyDidNothing = state => [
  { count: state.count - 1 },
  delay(1000, state => ({
    count: state.count + 1
  }))
]

What about a full example that fetches some information on initialization? The following example is ported from Hyperapp + Hyperapp FX here.

import { h, app } from "hyperapp"
import { http } from "@hyperapp/fx"
import { url } from "./utils"

const quoteFetched = (state, [{ content }]) => ({ quote: content })
const getNewQuote = http(url, quoteFetched)

app({
  init: () => [{ quote: "Loading..." }, getNewQuote],
  view: state => <h1 onclick={getNewQuote} innerHTML={state.quote} />,
  container: document.body
})

Handling data from DOM events?

DOM events, like effects, produce a result or have data associated with them (http fetch response, DOM event, etc).

const textChanged = (state, event) => ({ text: event.target.value })

app({
  init: { text: "Hello!" },
  view: state => (
    <main>
      <h1>{state.text}</h1>
      <input value={state.text} oninput={textChanged} />
    </main>
  )
})

Interoperability

Absolutely. The app function will now return a dispatch function (instead of "wired actions") so you can execute actions or effects at will.

const { dispatch } = app(...)

// And then later from another app or program...

dispatch(action)
// or
dispatch(effect) // Time.out, Http.fetch, etc.

Dynamic Imports

What about dynamic imported components out of the box?

import { h, app, dynamic } from "hyperapp"

const Hello = dynamic({
  loader: () => import("./Hello.js"),
  loading: <h1>Loading...</h1>
})

app({
  init: { name: "Bender" },
  view: state => <Hello name={state.name} />,
  container: document.body
})

Subscriptions

There's one more important new feature: Subscriptions. Aptly ripped off Elm's subscriptions, this is how we are going to listen for external input now.

import { h, app } from "hyperapp"
import { Mouse } from "@hyperapp/subscriptions" // I am open to a shorter name.

const positionChanged = (state, mouse) => ({ x: mouse.x, y: mouse.y })

const main = app({
  init: {
    x: 0,
    y: 0
  },
  view: state => `${state.x}, ${state.y}`,
  subscriptions: state => [Mouse.move(positionChanged)],
  container: document.body
})

What's breaking?

  • Slices will be gone. There will be other mechanism to easily update props deeply nested, but other than that bye bye slices.
  • Obviously, actions that cause side effects will need to be upgraded to use managed FX, which should be a joy — you'll love FX. I don't believe this will be a particular difficult feat, but we'll see. For 1.0 migration help and support I've created a #1to2 channel in Slack, please join! 🎉

Other

  • Middleware

  • Some things are still undecided. Should Hyperapp export all side effects? Perhaps we can reuse @hyperapp/fx for that?

  • I am going to remove slices! But now that actions are decoupled from the state update mechanism, we should be able to come up with similar patterns doing something similar to Redux's combineReducers.

  • Actions return a new state (not a partial state), so you need to merge the current state with whatever you return, similar to redux.

  • Lifecycle events? I am still not sure how to handle these. For now, I'll keep them the way they are.

  • Bundle size? All the core stuff is, in fact, less than current Hyperapp, but adding effects to that will makes us closer to 2 kB.

  • My interests have shifted to care more code readability and less about code golfing. A tiny code base is still a big priority for me, but not at the expense of making the code unreadable.

  • Server side rendering? I'm torn on this one. Should it be available out of the box or should we continue using @hyperapp/render? I am going to need @frenzzy to chime on this one.

  • JSX, @hyperapp/html, hyperx, h. Nothing is changing about here.

  • The router will continue to be an external module.

  • I'm still working hard to improve the performance and 2.0 will not affect Rewrite patch algo / improve diffing performance #499 or Keyed children diff optimization #663. I suspect 2.0 will have slightly better performance out of the box because of how DOM events are going to be handled now.

When is this going to be available?

Very soon. I'll be pushing all the stuff to the fringe branch and publish as [email protected] as soon as I can so we all can start playing with this.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions