Skip to content
2.0.0
6fa24d3
Compare
Choose a tag to compare

2.0.0

Pre-release
Pre-release

👋 This is a pre-release. As nothing is set in stone yet, there won't be release notes until 2.0 is officially released.

bb0d38c
Compare
Choose a tag to compare

Patch version packing few but solid improvements, including more consistent DOM attribute behavior, and improvements to the diffing algorithm! 🎉

Summary

  • 18bfb9b Support both object and string as value for style attribute. #756 @jorgebucaran
  • aa2ce46 More consistent behavior between client and server rendering for reserved DOM attributes such as spellcheck, draggable, translate, etc. #629 @frenzzy
  • f16f7fc Improve keyed algorithm performance. Results in fewer "insertBefore" calls when a keyed item is removed but other items are in the same order. #663 @SkaterDad
  • 4638e39 Minor improvements to the documentaion. Because grammar matters.
40e662b
Compare
Choose a tag to compare

Thanks

Thank you, everyone, for your ongoing support and interest in the project. Special thanks go to @frenzzy for contributing several new features and his work on @hyperapp/render. Also, thank you @okwolf for @hyperapp/fx and @zaceno for @hyperapp/transitions! 🎉

I'm also excited to report that Hyperapp has been the 3rd trendiest UI framework on GitHub at a rate of 52 stars a day since the beginning of this year! Wow! ⭐️

Summary

This release packs some juicy new features I can't wait for you to try out. 1.2.0 comes with CSS variables (#612) support in inline styles, several bug fixes (notably #614 and #595), a moderate performance boost via delegation of DOM events (#638), extensive documentation & code readability improvements — and a game-changing new feature: Lazy Components.

Lazy components (a.k.a subviews) refers to Hyperapp's new ability to treat a function as a virtual node. This allows you to create components that are wired to the global state and actions effortlessly.

For example, this is a regular component: you need to feed it its state and actions as props, which can be painful if the component is deeply nested within the document hierarchy.

const MyComponent = props => <div>{props.value}</div>

Now, this is a lazy component. Notice it returns a function that receives the application state and actions (just like the top-level view).

const MyComponent = props => (state, actions) => <div>{state.counter.value}</div>

See the documentation for examples or refer to this issue for more information and background.

Changelog

b7fadb8
Compare
Choose a tag to compare

Bug Fixes

0d960dd #584 Fix bug caused when removing empty text nodes from a list. (@jorgebucaran, @mrozbarry)

a03435e
Compare
Choose a tag to compare

What's new?

ab5d899 #531 Rewrite and collapse entire documentation into a single README file. (@jorgebucaran)

Special thanks to @zaceno!

Breaking Changes

4a783a3 #578 Change internal VNode schema. Essentially: namenodeName and propsattributes.
9222f99 #578 Store key in VNode.

Why? To make Hyperapp's virtual nodes compatible with Preact's virtual nodes. This means Hyperapp & Preact support in —your— library is now easier and in some cases (it always depends) comes for free! See #578 for a fuller background justification.

Documentation Fixes

Thank you all! ❤️

4c95855 @O4epegb
b0ce980 @ChristianHarms
5cba88d @sndstudy
b2afac7 @CanyonTurtle
e713abf @eschaefer
d01bba6 @dance2die

a7ca4b4
Compare
Choose a tag to compare

Introducing Hyperapp 1.0 🎉

Hyperapp is a JavaScript library for building fast and feature-rich applications in the browser. Out of the box, Hyperapp combines state management with a VDOM engine that supports keyed updates & lifecycle events — all with no dependencies.

import { app } from "hyperapp"
import { state, actions, view } from "./app"
export const main = app(state, actions, view, document.body)

Please refer to the documentation for up-to-date information and everything else you need to get started using Hyperapp. For live examples, check out the official CodePen account.

This 1.0 milestone means the API has reached stability, and the churn is finally over. It took us almost a year to arrive at the current API and while it will never be perfect, done is better than perfect and I am extremely pleased with the result. Thank you, everyone, for contributing your precious time, feedback, and bug-free code to Hyperapp! 😉❤️

To learn more see the discussion on /r/javascript and read the blog post!

So, what’s coming next?

Some of the cool things I am planning for the first quarter of 2018 are submitting a Hacker News PWA to https://hnpwa.com and adding a new example implementation to gothinkster/RealWorld.
 
I also want to refine the documentation and work more on the ecosystem and tools: scaffolding, bundling, DevTools integration, etc. If you have ideas for improvements please get involved, hop on Slack or file a bug report. And if you’re working on something cool using Hyperapp, let us know on Twitter @HyperappJS!

Happy hacking and happy holidays! 👋😉🎁

4c3fd7f
Compare
Choose a tag to compare

Thank you, everyone, who contributed to this new release (the third one) before 1.0! Special shoutout to @Mytrill, @Swizz, @vdsabev, @andyrj, @SahAssar, @zaceno, @pockethook, @okwolf, @lukejacksonn, and @SkaterDad!

Also thank you @Pyrolistical and @rajaraodv for your proposals and/or issue findings. Thank you, everyone. 🎉🎉🎉

What's new?

Immutable state

@jorgebucaran @Mytrill (#425)

Since the addition of modules, the state was no longer immutable: when an action, or a nested action, or an action in a module was called, its result was 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 is expensive or difficult to implement.

The benefit of having cheap memoize functions based on immutable state far outweighs the cost of maintaining an immutable state (both in terms of performance and source code size).

  • Allow debugging tools to keep the state history and implement time traveling.
  • Allow performant memoization of components/views on large apps (deep equality on state can be checked simply with ===)
  • Example — Thanks @Mytrill!

Now, when an action gets executed, the result is merged to the state slice/module's state this action operates on and create a new instance of the state and re-use the part of the state that hasn't been touched.

Short circuit vnodes

@andyrj

This change adds a check in patch() to return early when oldNode equals newNode. This covers two use cases. The first one is when both nodes are text nodes, and as such, there is no need to change the element's text content. The second one is when both nodes refer to the same object reference, which may occur if you are memoizing components.

Memoize components to save vnode creation CPU, and as a bonus we'll skip patching the element (updating its children). This is similar to React's shouldComponentUpdate optimizations, only that we get it out of the box, and free, like in a free beer. 🍻

The reason we can accomplish this easily is only that our components are always pure, and as such memoizing, them is a trivial prop check.

Read the issue here and check out the screenshots!.

Return a function in order to access the data argument inside actions

@jorgebucaran @Swizz @vdsabev

Change the signature of actions from (state, actions, data) to (state, actions) => (data).

This makes it easier to distinguish which part is Hyperapp's pre-wired state and actions and which part is your action implementation (data). In addition, make the API more elegant! 💅

   hyperapp          user
                     
(state, actions) => (data) => ({ ... })

Reducers

const actions = {
  setValue: state => value => ({ value })
}
const actions = {
  incrementValue: state => value => ({ value: state.value + value })
}

Effects

const actions = {
  downloadStuff: (state, actions) => url => {
    fetch(url)
      .then(data => data.json())
      .then(actions.setValue)
  }
}

Discussion

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.

(#448)

Remove thunks

@jorgebucaran

Thunks was a feature that allowed you to unlock a special update function by returning a function inside actions. You could use this function to update the state as you pleased, usually asynchronously. You can still achieve the same by calling one or more actions inside other actions. These kinds of actions are usually referred to as "effects".

For a little bit of history, thunks were introduced to help enhance Hyperapp when the events API was in place. With events now gone too, 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 and not many ways.

Goodbye thunks! 👋

Use setTimeout and not requestAnimationFrame for debouncing of actions fired in succession

@jorgebucaran

Using rAF for debouncing causes issues with apps running in the background / inactive tags. Because an interval will keep 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. But 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

@jorgebucaran

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

@jorgebucaran

We've removed props.init and gone back 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 & more!

actions.theWorldIsYours()

For example (try it online 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

@jorgebucaran @zaceno @okwolf

The lifecycle/vdom event onremove now receives a done function as the 2nd argument you may call to tell hyperapp 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>
  )
}

Fix sourcemaps (add missing --in-source-map cli flag)

1e0cca4 @zaceno

Make Hyperapp easier to use as an ES module

@SahAssar @jorgebucaran

Using Hyperapp as an ES module was already possible, but now it's easier because the entire source code, the whole 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>

Remove duplicated code

@pockethook

Doc updates

@okwolf

Refactor codebase for improved readability

@jorgebucaran

a818b6c
Compare
Choose a tag to compare

Thank you everyone who contributed to this second big release before 1.0! Shoutout to @Mytrill, @Swizz, @okwolf, @pspeter3, @lukejacksonn, @zaceno, @johanalkstal, @selfup, @vdsabev and everyone else I keep forgetting to mention! 🔥🎉🙇

What's new?

Succulent breaking changes and improvements, bug fixes and believe it or not: a smaller bundle size (1397B). Let me get you up to speed right away!

This release introduces the init(state, actions) function, modules: { a, b, c } for your sandboxed encapsulation dreams, moves the root property to the second argument of the app(props, →container←) call and parts ways with our clumsy, didn't-get-much-fresh-air HOA built-in support in favor of good old DIY HOA madness.

Init

We don't have an elegant way to call actions, subscribe to global events or kickstart ourselves when our app starts. We may call actions after we return from app() (and there's nothing wrong with that), but self-contained libraries like the router or a mouse / keyboard interop interface, etc., are better exposed as modules and without an init function, it would impose too much boilerplate and manual setup on users.

The new init function works like our good ol' events.load. Use it to subscribe to global events, start timers, fetch resources, etc.

See #406 for details.

app({
  init(state, actions) {
    // Subscribe to global events, start timers, fetch resources & more!
  }
})

Modules

Modules allow you encapsulate your application behavior into reusable parts, to share or just to organize your code. They are similar to mixins, but without any of their pitfalls. This is possible because modules are scoped to a state / action slice and there is no way to use a module without supplying the slice key.

const foo = {
  state: { value: 1 }
}

app({
  init(state) {
    console.log(state) // => { foo: { value: 1 } }
  },
  modules: { foo }
})

This means authors need not worry about namespaces and users can't mistakenly introduce implicit dependencies as it was commonplace when mixins roamed wild all over.

Modules make it easy to share and modularize your app, but without losing transparency or any of the benefits of single state tree architecture.

As you would expect, modules are allowed to have modules too.

const bar  = {
  state: { value: 1 }
}

const foo = {
  modules: { bar }
}

app({
  init(state) {
    console.log(state) // => { foo: { bar: { value: 1 } } }
  },
  modules: { foo }
})

Inside your module's actions, you can only call actions your actions or those exposed in modules under you. This is similar to how a parent component can pass props down to a child component or how a top level action can access the entire state.

See #406 for details.

Higher-Order Apps

Built-in support for HOA was removed in favor of a DIY approach.

Before

app(A)(B)(C)({ ... })

Now

C(B(A(app)))({ ... })

This not only gives you more flexibility, but it opens the door to a more diverse ecosystem. It also helps keep core simpler and helps us focus on making Hyperapp better and not worry of HOAs as a "core feature".

Container

To render your app to a different element thandocument.body, pass the element to the app(props, container) function in the second argument.

app(props, container)

If you were using props.root, you will need to update your code. See #410 for details.

Before

app({ 
  view, 
  state, 
  actions, 
  root: document.getElementById("app") 
}) 

Now

app(
  { 
    view, 
    state, 
    actions 
  }, 
  document.getElementById("app")
)

Other

bf61dc0
Compare
Choose a tag to compare

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.

Summary

  • Remove events, emit and mixins.
  • Introduce state slices.
  • Easier immutable and deeply nested state updates.
  • Out-of-the-box hydration.
  • Less lifecycle boilerplate.

State Slices

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!

Events

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.

Higher Order Apps

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

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. 🔥🎉

Lifecycle

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)}
    />
  )
}

Mixins

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.

eeb9930
Compare
Choose a tag to compare

Thank you, everyone, as usual! Shoutout to @okwolf, @Swizz, @zaceno, @lukejacksonn, @SkaterDad, and @andyrj!

What's new?

This patch introduces a ton of documentation fixes and some improvements, more examples, and a few delicious core changes! 🍰

Set element.nodeValue to replace text nodes. 🎉

When both oldNode and node are text nodes:

set element.nodeValue = node

...instead of creating a new text node and removing the old one.

Doing otherwise causes extra GC in situations like described in DBMon (#342).

Thanks, @andyrj. 💪

Allow update to take a reducer. 🎉

Inside a thunk, update usually takes a partial state and merges it with the previous state producing a new state.

This commit extends update to also take a function that we will immediately call with the latest state and like an action/reducer, allows you to update the state by returning a partial state.

You'd want to use this feature when you need to update the state inside an async process and you are not sure whether the state that was passed to the action that started the async process has become out-of-date or not.

A reducer is a function that receives the state and returns a partial state (or a reducer :mindblow:).

actions: {
  someAyncAction(state) {
    return update => {
      longTaskWithCallback(someData, () =>
        update(state => ({ value: state.value + 1 }))
      )
    }
  }
}

This allows you to get the current & latest state inside the callback of some async call.

Allow mixins to be simple objects. 🎉

Mixins that simply take no options can be written as a plain old JavaScript object now. The mixin, as usual, is an object that extends your application state, actions or events.

const simpleMixin = {
  events: {
    load() {
      console.log("It works!")
    }
  }
}

Bonus