Hyperapp: RFC: Hyperapp 2.0

Created on 1 Apr 2018  ·  160Comments  ·  Source: jorgebucaran/hyperapp

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 https://github.com/hyperapp/hyperapp/issues/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 https://github.com/hyperapp/hyperapp/issues/499 or https://github.com/hyperapp/hyperapp/pull/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

Discussion Outdated

Most helpful comment

Hey @etylsarin! Thank you for your interest. I'm releasing the 2.0 alpha on the first week of July! :)

All 160 comments

Sounds exciting !

However, in the code example for the "Dynamic Imports" section you use const Hello twice, one right after the other. You meant to have named them differently, right ?

@icylace Yes, that's mistake and I just fixed it. Thanks for spotting it! 🙇

Will there be a way to provide your own custom managed fx? I'd also like to see a managed fx version of lifecycle events and ideally import too (if that's even possible). I really like the inspiration this takes from @hyperapp/fx 😊 🔌

Will there be a way to provide your own custom managed fx?

Yes.

I'd also like to see a managed fx version of lifecycle events

I need your help here! 😉

...ideally import too

Me too! But we're going to need a webpack/parcel guru to help with it; because of how they work by default.

I really like the inspiration this takes from @hyperapp/fx

Absolutely. 💯

@jorgebucaran I need your help here! 😉

Executing existing actions/fx from lifecycle events should be trivial to support, this is already handled by @hyperapp/fx/fxapp.

To make the DOM element available for interop like in the existing oncreate/onupdate/ondestroy lifecycle events, we could provide that as part of the data given to that action. Or if we'd rather keep side effects quarantined to be only within fx, we could require writing a custom effect in order to perform direct DOM manipulations (this is more the mantra of fxapp).

So where does this leave Ultradom? EOL?

By the way, cudos on the rad redesign plan. Sounds like Hyperapp going into overdrive. 😉

@rbiggs Good question!

So where does this leave Ultradom? EOL?

Fine. In good standing. I can maintain two similar projects. Now three was too many.


Having said that, we can continue exploring using ultradom in HA 2.0. A few things I want to add: the new HA will be purer, and this may drastically change how lifecycle events are used — also components are going to be lazy by default (a good thing!) and I want to see how much we can improve areas like inline CSS, CSS in JS, concatenating classes using classList (instead of string concatenation), etc.

Some of these changes should make it to ultradom anyway, but maybe some will be "conceptually" incompatible with it.

Sound good to me. By the way, about inline CSS. You could support string and object values with just a check like this:

function updateAttribute(element, name, value, oldValue, isSvg) {
...
   } else if (name === "style" && typeof value !== 'string') {

That would let you have your cake and eat it to 🍰🍴You could then use normal inline CSS or a JavaScript object in case you need to make some style calculations based on a dynamic value. Not being able to using string-based inline styles has been a major pain point for me because I use a lot of SVG images. These invariably have tons of inline styles scatter around in all their various parts. The first time I used a bunch of SVG images with Hyperapp I spent hours trying to figure out why they weren't rendering. And then quite a bit of time digging through their code to convert the inline styles into JavaScript objects. It's so much easier to just support both. Please? All it requires is the above type check to have both ways of supporting inline CSS styles.

@rbiggs Definitely. Just a simple node.style.cssText = value and boom.

Wow, hyperapp is evolving :smile:

Do Time, Http, and Mouse need to be part of hyperapp? It seems odd to me to have that kind of functionality dependent on the library.

Also, would you mind elaborating on the decision to remove the actions parameter and add a dispatch method? Is this mainly to allow dynamic actions?

@developerdizzle Do Time, Http, and Mouse need to be part of hyperapp? It seems odd to me to have that kind of functionality dependent on the library.

No, they definitely don't. I'll update the examples to better reflect what I have in mind.

Also, would you mind elaborating on the decision to remove the actions parameter and add a dispatch method? Is this mainly to allow dynamic actions?

It seems you misunderstood dispatch. I am not removing actions for a dispatch method. In fact, dispatch is only mentioned in the interop section above. The dispatch function will be the raw way to interop with Hyperapp from another program, e.g., a legacy Backbone app.

  • In 1.0, wired actions serve two functions: (1) interop and (2) subscriptions.
  • In 2.0 dispatch will serve only _one_ function: interop.

I'll update the examples to better reflect what I have in mind.

That would be much appreciated!

It seems you misunderstood dispatch. I am not removing actions for a dispatch method. In fact, dispatch is only mentioned in the interop section above. The dispatch function will be the raw way to interop with Hyperapp from another program, e.g., a legacy Backbone app.

I think I do understand, but feel free to correct me here:

To provide actions - in 1.0 you have the actions parameter of the app function; in 2.0 you simply call the functions that would be properties of actions.

To call actions externally - in 1.0 the app function returns an object with methods we can call from other programs; in 2.0 we have to use dispatch to accomplish the same thing.

Basically the code goes from:

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

const myApp = app(state, actions, view, document.body);

myApp.up();

to

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

const myApp = app(state, view, document.body); // Or is this now all one parameter?

myApp.dispatch(up);

Without fully understanding the decision, my personal preference would be to continue using the 1.0 technique as I feel it's cleaner and doesn't introduce new API for other programs to use.

@developerdizzle In 2.0, dispatch will only serve to interop, so to call functions externally you will definitely use dispatch as you showed above.

Now, because in 1.0 you often do interop (since there are no subscriptions) this sounds inconvenient, but in 2.0 you will rarely do interop, because there will be a subscription API.

For example, in 1.0 you would use interop to implement the following code. In 2.0 you don't need to.

import { h, app } from "hyperapp"
import Mouse from "@hyperapp/mouse"

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

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

Can I ask about the rationale behind this one? I really like the way this works now, it makes nested actions a lot less verbose than if I would have to merge myself in each action. I guess the current behaviour might be surprising if you haven’t read the docs carefully though.

EDIT: just to not come out as purely negative: I really enjoy using HyperApp and love the fact that it is under heavy development! The ideas you have for 2.0 are really interesting, so I am in fact looking forward to it ;)

If custom fx are passed the current state tree then you could make a merge effect that can do merges as shallow or nested as your heart desires.

(this is how fxapp handles state updates)

@bendiksolheim I am still open to debate those little details. One thing I want to remove in 2.0 is slices, and for that reason, a shallow merge is not really that helpful.

I'm glad to see subscriptions coming back. I was sad to see them go way back when.

You mention the things you want to work on and the last item is types. Do you mean something like React's PropTypes?

@rbiggs Fixed it. I am saying our current typings are that very good and the reason: slices and wired actions. With that gone, getting the types right (while at the same time keeping your sanity intact) will be achievable.

Not really sure what you mean about slices, @jorgebucaran , I am not completely into all the concepts of Hyperapp yet :)

But thinking about it a bit more, it makes sense to return the full state when actions no longer follow the same structure that the state does. I don’t see any obvious way around it, actually. Maybe an alternative is a helper function to easily update nested state? Something like a more powerful version of Object.assign. It would also make it easier for people to upgrade from 1.x to 2.0.

This all looks neat!

The main things still missing for me is reuse: components.

For example, what if it were possible to have managed, auto-mounted app instances as components, by not initially specifying a container?

const Clock = app({
  init: { time: new Date() },
  view: state => (
    ... 
  ) 
});

app({
  view: state => (
    <Counter time={ new Date(...) }/>
  ),
  container: document.body
});

Component attributes would merely override any state properties set by init.

Component reuse is by far the biggest missing thing missing for me in any of the micro frameworks - and the techniques I've seen where lifecycle hooks are being used to create and mount more apps is not user-friendly enough for productive daily use.

@mindplay-dk Have you given up on the single state tree architecture? 🤔😉

Have you given up on the single state tree architecture?

It doesn’t provide the convenience, modularity or encapsulation that components provide - all of those factors are critical for efficient daily authoring and reuse.

I still work with a single application state model for self-contained applications - but components, in practice, have internal state details that are of no relevance to the application; the application shouldn’t have to manage the life-cycle of a date-picker, for example, anymore than it has to manage the life-cycle of a regular DOM input.

(Custom Elements may address this need in the future, but we’re not quite there yet.)

@mindplay-dk Your opinion and comments are very valuable to me but have you considered that maybe the problem is that you are too conditioned to think in terms of "reusable components"?

Component reuse is by far the biggest missing thing missing for me in any of the micro frameworks - and the techniques I've seen where lifecycle hooks are being used to create and mount more apps is not user-friendly enough for productive daily use.

I don't know of any other micro-frameworks embracing the single state tree architecture. Sounds like Hyperapp to me. I know I haven't properly solved this problem yet in 1.0, but I want to with 2.0.

I love this quote from @wintvelt:

When you delegate state management to a component, you will often find that you still need outside read/ write access to the delegated state, which destroys the benefit of delegation, and often makes things worse.

maybe the problem is that you are too conditioned to think in terms of "reusable components"?

I was not conditioned - quite the contrary.

I was initially very open to the idea of a single state tree - it’s what I was planning and hoping for after learning about the SAM pattern and when I first discovered Hyperapp and Picodom.

It’s an appealing idea, because it appears simple. I just don’t find it to be practical in reality. Real world components do have internal private state that is of no interest to the consumer, and I don’t want to manage these details by hand. It’s verbose, error-prone and distracting - as a consumer of a date-picker, for example, navigation state while paging through months is of no interest to me; I only care about the selected date. There are countless examples like that.

When you delegate state management to a component, you will often find that you still need outside read/ write access to the delegated state, which destroys the benefit of delegation, and often makes things worse.

Yes, this is why React distinguishes between “props” for state that is relevant to the consumer, and “state” which is private and internal to the component instance and of no interest to the consumer.

You should never need read access to a component’s state - that shouldn’t even be a thing, and won’t be, if your component is designed properly. Instead, as a component consumer, you have write-access to an event hook, and you receive a notification that e.g. a new date has been picked, and apply it to your own state.

It’s exactly the same way you use a regular text input element - it also technically has internal state, such as focus, cursor position, selection etc. which are of no direct interest to the consumer; but that state still needs to exist internally in the element instance.

If you think of the relationship you have with DOM elements (inputs in particular) then the relationship you should have with a component is completely similar.

I think it’s a common mistake to try to build parts of your application using components and trying to make them responsible for governing parts of the total application state - from my point of view, that’s just a mistake, and not what I’m trying to achieve with components. It’s literally no different from how I expect to use DOM inputs - the only difference is components provide additional functionality for which we don’t have a DOM element.

I don’t want to manage the internal state, creation and destruction of a date-picker, anymore than I want to manage the state, creation and destruction of DOM inputs - which is where your library comes in.

But the moment I need a reusable control like a date-picker, I’m right back where we started, having to manage every instance and it’s state manually.

So the problem is only half solved, in a sense - because the library itself just creates a new layer with exactly the same problems you had with the DOM. (I’m emphasizing this, because I believe that’s the core of the problem, and the real real reason components exist anywhere.)

Components are a way to address those same problems, consistent with the way we address the problems with DOM elements - it’s the same problem and therefore the same solution, just at a higher level.

@mindplay-dk Perfect. You spoke exactly what I am also facing and expect something smart and out of the box. I am with you to have some mechanism to promote abstraction via state full component as well as something simple to get the state management done right. I am just wondering if we think in direction of Mobx for state management of entire app and stateful component for abstraction of complex component like datepicker, etc. I just read this https://medium.com/@botverse/enjoying-mobx-jsx-and-virtual-dom-621dcc2a2bd5 and though of sharing. I am not sure how much relevant it is but it take care of one part of the problem. Refer this too https://github.com/francesco-strazzullo/mobx-virtual-dom-example. Its old repo just for the sake of discussion.

To share some input in the other direction - I like hyperapp because it's essentially a minimalist React + Redux in a single framework with less magic (no providers, connect functions, etc). If you remove the single state tree and add component state what would make hyperapp any different than React or VueJS? Why not just use those (or if you want something smaller Preact) instead?

I'm very interested in a lot of the 2.0 changes, but worry a little hyperapp will turn into just another React clone like Preact or Inferno.

@jorgebucaran I've re-read this a few times, and let it sink in a bit before I responded. My initial thought was _"here we go again"_ :laughing:

After letting your ideas sink in, however, I think they will address many of my pain points when developing in hyperapp, and really brings back the elm spirit that first attracted me to this framework.

A couple concrete examples should be easier/improved in your proposed 2.0:

  • On app startup, I fire off an http request to see if the user's credentials are still valid. If so, I connect to a websocket endpoint for notifications. I need to do the same thing after a regular login. Right now, with state/action slices, you have to do some tricky startup logic or involve the view layer since it has access to all actions. In this 2.0 world, I can just make that an effect returned while saving the auth credentials. :+1:
  • Passing actions down through view layers. The "lazy/wired" components helped with this, but did not eliminate the need. The new way of just using plain functions will make this so much cleaner. It's very much like how elm lets you fire off Msg and Cmd without having to pass them into the view, and without resorting to strings :+1:

Misc thoughts:

  • I will miss the slices & automatic partial-state merging, but with object-spread syntax and Object.assign, it's not a big deal. On the plus side, our application bundles won't have to include your custom clone function in addition to whatever babel or typescript include.
  • Being able to dynamically create actions will make dealing with data sets/arrays a lot nicer.
  • It seems likely that our non-gzipped application sizes could get smaller, since you won't have to keep typing things like actions.namespace1.dosomething which can't be minified as well as a plain in-scope function.
  • Built-in support for code splitting will be nice. It's not crucial to my app yet, since it's only ~38KB gzipped (Full-on SPA for the size of React itself), but it could be a nice micro-optimization as my app expands.
  • I would prefer keeping the effects and subscriptions in separate npm packages. @hyperapp/fx will be a perfect fit for this new version instead.
  • Init, subscriptions, and middleware are welcome additions/re-introductions (I missed the hooks/events!)

Regarding stateful components

I'm still very much in favor a single state tree. Is it a perfect approach? Certainly not, particularly when trying to do re-usable components like @mindplay-dk was talking about. However, the single state tree gives you extreme predictability and a simple mental model.

The single state tree didn't stop me from implementing re-usable multi-select & image upload components. State/action slices helped, but they weren't strictly necessary.

With these hyperapp 2.0 changes, you no longer _need_ to pre-configure the initial state and sets of actions for each individual component, so you can use the flexibility of javascript to avoid some of the boilerplate related to these "more complex" components. Elm can't avoid the boilerplate, but we can! It's just going to take some creativity.

What about actions that need the event data from the view, such as @dangvanthanh/hyperapp-todomvc/blob/master/src/views/todo-input.js#L8-L9 or multiple actions in @dangvanthanh/hyperapp-todomvc/blob/master/src/views/todo-item.js?

@brodybits DOM events, like effects, produce a result or have data associated with them: http fetch response, onclick, onmousemove, oninput, ….

import { merge } from "./utils"

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

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

@jorgebucaran I just saw it, thanks. As a nit it looks like textChanged is using e when it should be using event:)

So would the "Hyperapp 2.0" library always pass a second argument with some form of event data object or only if there is actually some relevant event data?

Fixed! @brodybits 👍

In 2.0, actions will receive whatever data was produced by your effect, subscription or virtual DOM event handler as the second argument. The standard signature will be simply: (state, data), it could be the other way around too, but we need the state anyway as we'll be doing merges manually (or wrapping the action with a function that does the merge for you).

For DOM events this means the event object, always.

@mindplay-dk I agree with @tomparkp. If we had something like local component state, then why even use Hyperapp when you can go mainstream with React/Inferno/Preact/Nerve/Rax/Dio/ or whatever this week's React's clone is. I quote @rtfeldman's:

Local component state is the new two-way data binding.

At least in Hyperapp, I'm largely responsible for creating this problem, as I've often encouraged creating pseudo stateful components using lifecycle events as you described. One of the reasons for that was a lack of a proper subscriptions API.

I think this section of the Elm documentation does a good job of describing the alternative approach things like Elm and hyperapp take over reusable components:

https://guide.elm-lang.org/reuse/

@tomparkp It could have more examples, but I know documentation is hard. This is another good source:

If you remove the single state tree and add component state what would make hyperapp any different than React or VueJS? Why not just use those (or if you want something smaller Preact) instead?

I do use Preact instead - mainly (if not only) because of the component abstraction.

Would Hyperapp still be different if it had components? I think in plenty of ways. I am actually not fond of the React class-component model, life-cycle approach, etc. - the element-level life-cycle approach is the main thing that interests me about Hyperapp and Picodom; something neither React or Vue have.

I think you need to have a better argument against components besides “not different enough”. That’s not really a reason for or against anything.

As explained, the main reason I see a need for components is, if you’re going to create a date-picker and call that an “app”, and manage its state and life-cycle and relationship to the main app manually, that’s just you doing exactly what we were doing with DOM before, just at a higher level; it just recreates the original problem, now for your so-called “apps” which are really components by any other name.

I’d like to see the problems addressed consistently at the low level (DOM) and at any higher level.

Components are one way to do that. Not saying it’s the only or best way, it’s just the one way I’ve seen that actually does work.

Having nothing also “works”, of course - it just doesn’t provide any high level encapsulation, modularity or reuse, and when you need something more high level, you’re once again manually solving all the problems you were solving for DOM elements before virtual DOM.

So my preference would be a top-level component concept that can nest, with instance life-cycle management similar to what we provide for DOM elements. Your root app can be treated like any other component quite naturally - it’s much less natural to try to treat an app as a component.

In my opinion, it should feel natural to replace <input type=“datetime-local”> with <DatePicker> and shouldn’t require writing a bunch of code. Using a DOM element shouldn’t be drastically different from reusing your own custom components.

@mindplay-dk There are more in depth reasons for a single state tree than just making it different, its just too much to go into here and is documented elsewhere: Elm Architecture, Redux Principles, etc.

I just don't think that there has to be one universal approach that is better than another, and component-based state would seem contrary to the philosophy that Hyperapp is designed around (Elm Architecture, Redux), something like Inferno sounds similar to what you're describing (it allows you to have lifecycle hooks without using classes).

@mindplay-dk I think you need to have a better argument against components besides “not different enough”. That’s not really a reason for or against anything.

That's not my reason for choosing this architecture, but we wouldn't be having this conversation if I had chosen a different one. Hyperapp probably wouldn't even exist.

I think you've given up too soon, chosen the path of least resistance. I am going full single state tree and want to concentrate resources on creating tools to improve the developer experience within this paradigm. If you want to keep debating this, please create an issue or join us on Slack! 😄

To chip in on this, while the idea of reusable components with their own states sound nice, I can't think of any concrete situation where I'd do this. If I wanted to re-use some widget from one project to an other, I'd prefer re-using the view, (which should work just fine since we're making pure functions) and feed it it's params. Then it's just a matter plugging back the actions and fixing the CSS if it wasn't done properly.

In the same fashion as Web Components and Shadow DOM related stuff, being able to drop in some widgets around sound very nice on paper, but I personally always found it to be an extra hassle if you want to change anything about it afterwards. An long term hassle that easily outweighs the initial setup time required to reuse your code, which isn't that hard with the current implementations of HyperApp.

I think this feeling about Web Components is relevant here, and it seems to be shared a lot in the web dev community.

And I've also never personally encountered a situation where component could be directly re-used independently, unless it was meticulously planned ahead, which I believe defeats the purpose of making the extra effort to build reusable components.

Unless this is extremely easy to implement, with little to no side effects, then sure why not, but I don't think it should be a focus in 2.0

I too am still in favor for the single state tree! 👍 Whatever else happens, we can adapt 😂

However will we still be able to extract actions (the const main = app(...);) as we do now?

This pattern saves a lot of headache for certain situations 🎉

It seems that there is a concept of subscriptions but unclear if that solves some of the problems that exposing actions does as elegantly 🙏

To say nobody needs components because you never did is a bad argument. A lot of developers use components from the many component libraries out there. Google, Facebook, Apple, Microsoft, IBM, Amazon, Ebay, Paypal, Airbnb, Uber, and any other larger company with many domains to build and manage is going to want reusable components. They may have hundreds of components that they use. If you're only building smallish sites or a simple and focused PWA, you probably won't be running into the problems that big business encounter.

Having said that, I'm not advocating for or against components. Like the expression says: if all you have is a hammer, everything looks like a nail. So if you pick a side in this argument, you'll fail to see the needs of the other.

Several years ago I tried to port a component library to Elm. It was a total failure. Elm works great for the use cases it addresses, but when I tried to create reusable components with it, it was like a straightjacket. After two months of trying different approaches, I gave up. Then I discovered Hyperapp when it was really new. I'm a JavaScript kind of guy. I never liked the Elm syntax, so Hyperapp was a perfect choice for me. I did look into to the idea of using it as the basis of a component library, but ran into the same problems I had with Elm. Still, I like Hyperapp a lot. It's minimal and just the parts of the Elm architecture to make it really smart and so easy to use.

I've ported components to many frameworks, Angular 1 & 2, Vue, React, Inferno, Preact. Besides dealing with the quirks of each, it was no big deal because they all have something in place for components.

Jorge created Hyperapp to offer the Elm architecture in JavaScript. I think this is brilliant. Is it a perfect fit for every development need? It doesn't have to be. It just needs to solve the types of problems it was designed for. If it does not cover your needs, choose the tool that does. No hard feelings. There are many tools, not just a hammer.

When subscriptions are implemented, I could see, for example, having a Preact stateful calendar component on the same page as Hyerapp and using an event bus to send the calendar state to Hyperapp.

If you're going functional and miss stateful class components, you can create a simple state object that a functional component can consume while still using the single state tree. I've been experimenting with reusable functional components and was surprised how much I was able to accomplish. It does require different approaches than class-based stateful components, but it is possible.

In the end, I believe Hyperapp should stay true to its mission to be a purely functional library. Class-based stateful components don't make sense for it. That means Hyperapp won't be a good platform for reusable components. If that's something you want, but like the element-level lifecycle hooks that Hyperapp offers, there is Ultradom. Create a component class that allows local state that uses Ultradom to render and update the component's tree. It's not that complicated, really. That's why Ultradom exists.

When subscriptions are implemented, I could see, for example, having a Preact stateful calendar component on the same page as Hyerapp and using an event bus to send the calendar state to Hyperapp.

Another thing I think we should be able to see is a custom calendar app built with Hyperapp that exposes a subscription which allows you to interact with it from another Hyperapp app without leaving the functional paradigm and without an event bus.

I've been thinking... We're unwiring actions from the state. But we're wiring the vdom so actions attached to event-handlers will be called with the internal dispatch function.

Wouldn't it be nice if we could somehow customize the dispatch that the vdom is wired to? Even better: if we could have different custom dispatches for different branches of the vdom?

I don't know how this might look exactly (I'll be able to experiment when the fringe-branch is published), but if we can find a good api for it, it would be a great place for userland solutions to partial state updates, slices/deep-state updates, modular apps... even stateful components, perhaps (haven't thought that last one through yet).

Perhaps this concept of "middleware" (which I don't fully grasp) is something like that?


Edit: Instead of customizing dispatch, you could just make sure to wrap all your actions in a function that does your custom-dispatching for you. So what I'm suggesting isn't making something previously impossible possible. Just maybe a little nicer. Maybe.

I think you've given up too soon, chosen the path of least resistance. I am going full single state tree and want to concentrate resources on creating tools to improve the developer experience within this paradigm

I am not closed off to the idea at all, I’d definitely give it another try! Just that what I have seen and tried did not work for me. Examples from others who claimed to understand the idea didn’t look good to me either. Maybe you can figure out a way to make it more palatable, we’ll see :-)

Closing thought: perhaps modularity doesn’t have to come in the form of components? But reuse does have to be easy and convenient. Perhaps there’s a better way?

Another thing I think we should be able to see is a custom calendar app built with Hyperapp that exposes a subscription which allows you to interact with it from another Hyperapp app without leaving the functional paradigm and without an event bus.

Aka a component? ;-)

Seriously though, if you can find a better and simpler way to providee modularity and reuse, I’m all in!

@mindplay-dk Working on it!

@selfup However will we still be able to extract actions (the const main = app(...);) as we do now?

This pattern saves a lot of headache for certain situations :tada:

It seems that there is a concept of subscriptions but unclear if that solves some of the problems that exposing actions does as elegantly :pray:

See the const dispatch = app(...) examples for how that will work. Since hyperapp 2.0 will not require you to pass in your full set of actions to the app call, it really has no knowledge of what actions you have available, so the current const main = app() thing can't work.

You'll already have your action functions defined somewhere, so now you'll just pass them into dispatch to run them outside of hyperapp. Anything which happens inside of init, view (events), or subscriptions will be run through a similar function automatically from the way it looks.

@selfup See this comment.

tl;dr: In 1.0 we use the wired actions outside the app for _two_ things, subscriptions and interop. I am willing to bet that 99% of all used of wired actions outside the app() call is to make up for the lack of subscriptions. In 2.0, because there will be a subscriptions API, using dispatch(myAction) outside the app will be as rare as using ports in Elm.

I would be very excited to see some of those changes in Hyperapp! Dispatching pure functions is a powerful concept, as they're flexible and easily composable. And we could probably use a library like https://github.com/reactjs/reselect to reach deeper into the state tree.

From what I understand, the event handlers of DOM elements (e.g. onclick) will dispatch the object / function passed and update the internal state tree accordingly. It would be great if you could also use Hyperapp or Ultradom with an external state manager!

I could then use this very minimal store that I wrote today:
https://github.com/overstate/overstate/blob/fp/fp.ts
https://github.com/overstate/overstate/blob/fp/fp.test.ts

One peculiar feature I was thinking about was that when a function is called or a promise - resolved, the result is dispatched on and on until an object is received. This is meant to help with asynchronous programming, although I'm unsure how to handle the fact that the state might've changed in the meantime.

I haven't used @hyperapp/fx enough to understand it, and I'm curious to see how this problem will be solved in Hyperapp 2.0. Will all functions basically need to accept state as the first parameter in order to be dispatched?

It would be great if you could also use Hyperapp or Ultradom with an external state manager!

In 2.0 the ability to dispatch actions/effects is built-into the view. You don't onclick={()=>dispatch(action})} but → onclick={action}. For this reason alone, the stateman and virtualdom will be closer than ever, so while your suggestion is still possible, I don't find it that much appealing.

Ultradom with an external state manager!

You can use ultradom with an external state manager, it's the first thing you read when you go to the repo.

screen shot 2018-04-04 at 1 54 01


One peculiar feature I was thinking about was that when a function is called or a promise - resolved, the result is dispatched on and on until an object is received.

No need for that. All effects should be handled via the effects runtime.

Will all functions basically need to accept the state as the first parameter in order to be dispatched?

Actions will receive the state as the first argument, yes. This is irrelevant to how they are dispatched, but it's needed because otherwise how can we calculate the next state? Automatic merges like in 1.0 are not planned, but that's a "minor" point still open to debate.

(I haven't read through all the replies on here, and I'll save a bigger overview post for when I do, so forgive this one-off thing that may not be relevant to the last few messages)

So there are a few debuggers/integrations that I think we need to think through. Specifically, I'm thinking about @andyrj 's redux integration, and my hyperapp-debug.

If actions will be just ad-hoc reducers (which is super cool, by the way), how can debuggers serialize/replay these? I don't think we get the ability to actually see the name of the function/module that is being called from inside JS, and this can really hurt our ability to live debug.

One solution is that each module that creates actions does something like:

import { actionable } from 'hyperapp'; // ???
const actions = actionable({
  someAction: state => { ...state, thing: true },
})

Something like actionable() would let middleware apply a proxy to actions, but it _is_ code overhead, and probably isn't necessary for the vast majority of use cases. With that in mind, it's not a good, low friction solution.

Allowing middleware on dispatch internally may not be great either, because I don't think javascript would let us inspect the function to get a safely serializable reference.


TLDR: Will v2 have a good way to serialize/identify actions for debugging tools?

I've updated the OP with more information about how to pass data into actions.


How to pass data into the action?

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

I am open to suggestions about this one. I like downBy(10) a lot, but it creates a arguably useless middle function that we could always avoid.

Another possibility is Action.run(downBy, 10) or simply dispatch(downBy, 10).

@mrozbarry Allowing middleware on dispatch internally may not be great either, because I don't think javascript would let us inspect the function to get a safely serializable reference.

Using middleware and action.name?

The proposed changes overall, look great to me! Keeping it _hyperapp_ gives everyone involved a opportunity to learn the tools and techniques required to make better (more pure, at least) apps.

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.

This statement is great, and whats better is that doing more for the user and being minimal and pragmatic aren't contradictory statements! People seem to be worrying about added complexity and/or increased API footprint but from what has been demonstrated so far nothing looks _harder_ to learn. If anything, things seem to piece together better; less code, more intent.

perhaps modularity doesn’t have to come in the form of components? But reuse does have to be easy and convenient. Perhaps there’s a better way?

This quote from @mindplay-dk I agree, modularity and reusability is important and one thing that 1.x does not lend itself very well to, but 2.0 looks a little more functional and like elm say:

We do not think in terms of reusable components. Instead, we focus on reusable functions.

It all sounds very promising! I especially like (in no particular order):

  • Return data from events onclick: { some: 'update' }
  • Effect tuples [up(state), delay(1000, down)]
  • Dynamic views dynamic({ loader, loading })
  • Subscription model subscribe: [Mouse.enter(up), Mouse.leave(down)]
  • The omission of slices ☠️

One thing I am not 100% on is not being able to return a partial state and have it merged (as this is how setState works and people seem reasonably happy with it) but as it has been said already.. there is a function for that.

I look forward to seeing that fringe branch 🙇🎉

Another one off comment that I think will pop up eventually is this: If we're just returning updated states from events, what do we do with <form>s, and specifically, onsubmit? The default browser behaviour is to force developers to return false to cancel the submission. React does some fancy behind-the-scenes code so developers can use event.preventDefault(), but I don't think we want to change how this event actually works.

Using middleware and action.name?

Probably not, because there is no way to map it for replay, especially if replay is not happening in the same instance it was originally run (ie someone shares a debug session to reproduce an app state to another browser or computer).

I think for debug, we either need to allow an update method to be registered with the app which works similar to Elm/Redux. That would allow { type: 'nameOfAction', ... } to be replayed and safely serialized, at the cost of more code.

The other option is to keep actions as part of the app initialization process, but that's going to make dynamic component/actions more painful, even if it's not pre-wired.

@mrozbarry Jorge has said that all DOM events will get their event object passed to your action as an argument, so you can handle form submissions however you do now in 1.0.

I think it will look something like this?

// In reality I would probably make helper functions to update the nested objects
function textChanged(state, event) {
    return {
        ...state,
        myForm: {
           ...state.myForm,
           text: event.target.value
        }
    }
}

function onFormSubmit(state, event) {
   event.preventDefault()
   // return an effect to perform the POST
   // probably don't need to pass it the form state object if it's specific to this form
   return [ state, SendFormHttpRequest(state.myForm) ] 
}

<form onsubmit={onFormSubmit}>
    <input oninput={textChanged} />
    <button type="submit">Submit</button>
</form>

Jorge has said that all DOM events will get their event object passed to your action as an argument, so you can handle form submissions however you do now in 1.0.

Not possible, because event.preventDefault() doesn't actually stop the submission, and will result in a page (re)load, even with the action="" attribute omitted. The only way in vanilla js to prevent a form submission is by returning false - unless I'm crazy.

The basic problem being that I can't return false and have it actually used by the DOM properly if hyperapp is intercepting it, assuming it is a state update.

Okay, did I take crazy pills? I was 100% sure that onsubmit required return false to cancel, but that doesn't seem to be the case in codepen.io.

@mrozbarry Not possible, because event.preventDefault() doesn't actually stop the submission, and will result in a page (re)load, even with the action="" attribute omitted.

This is how I handle 100% of my form submissions, so I think it works well & is very standard.

Edit: After looking, in Hyperapp 1.0+ I don't return anything from the onsubmit handler in some cases. Snippet from a Typescript project which is submitting an uncontrolled form:

    function submitForm(event: any): void {
        event.preventDefault()
        const formData = new FormData(event.target as HTMLFormElement)
        actions.submitForm(formData)
    }

Another snippet from MDN docs, saying it's good enough to just call preventDefault

form.onsubmit = function(e) {
  if (fname.value === '' || lname.value === '') {
    e.preventDefault();
    para.textContent = 'You need to fill in both names!';
  }
}

I am open to suggestions about this one. I like downBy(10) a lot, but it creates a arguably useless middle function that we could always avoid.

downBy(10) would be my preference. As there are no tuples in JavaScript, onclick={[downBy, 10]} looks weird in a JS context. Also, how would you pass the event here?

If Ultradom will not be merged in hyperapp 2.0 we should be sure that the components are compatible for both views libraries, @zaceno realised that there was some problem with hyperapp/transitions with ultradom when it worked with Picodom.

I am not sure subscriptions are 100% perfect for what I am building right now, but I guess i can find out once I use em 🎉

@zimt28 downBy(10) is just JavaScript and you can do it if you want — nothing blocks you.

The recommended should be onclick={[downBy, 10]}. It reuses the concept of tuple that we're also going to use for effects, init, etc.

Also, how would you pass the event here?

You don't. The browser does.

@dancespiele Basic components will, but anything slightly more complex _will not_ be compatible because in 2.0 the view layer will have dispatch built-in.

@selfup Subscriptions are the natural progression from virtual DOM. It took me a while to understand it, but when it clicked I was mindblown. Similarly to how you are using a virtual DOM as a declaratively view layer and not the DOM imperative API (createElement, removeChild, etc), subscriptions will give you a declaratively pub/sub layer to DOM global events or other DOM APIs that are based on registering an event listener.

@mrozbarry What's the difference between nameOfAction.name === "nameOfAction" and ({ type: "nameOfAction" }).type === "nameOfAction"?

@mrozbarry About this comment https://github.com/hyperapp/hyperapp/issues/672#issuecomment-378364109. Elm struggled with the same thing for a while and eventually came up with onWithOptions. We should be able to get away with it easily as suggested by @SkaterDad, but we may have onWithOptions too at some point.

@zaceno About: https://github.com/hyperapp/hyperapp/issues/672#issuecomment-378219988

There _will_ be a middleware system, to let you wrap dispatch, it might be something as simple as app({ middleware }) or app(props, middleware), we'll see exactly _how_ later. That will allow us to create integrations like redux dev tools, @mrozbarry's debug, etc.

To create reusable components, this is what I have in mind.

// counter.js

export const state = { value: 1 }
export const actions = {
  increment: state => ({ value: state.value + 1 })
}
export const Counter = ({ map, state }) => (
  <main>
    <h1>{state.value}</h1>
    <button onclick={map(actions.increment)}>+</button>
  </main>
)

// main.js

import { state as counter, Counter } from "./counter.js"

app({
  init: {
    counterA: counter,
    counterB: counter
  },
  view: state => (
    <div>
      <Counter
        state={counter.counterA}
        map={action => ({ counterA: action })}
      />
      <Counter
        state={counter.counterB}
        map={action => ({ counterB: action })}
      />
    </div>
  )
})

I played around with this a bit more and came up with a more minimal implementation.

// counter.js

Same code as counter.js in https://github.com/hyperapp/hyperapp/issues/672#issuecomment-378550340.

// main.js

import * as counter from "./counter.js"

const Counter = scope({ counterA: counter.Counter })
const Counter = scope({ counterB: counter.Counter })

app({
  init: {
    counterA: counter,
    counterB: counter
  },
  view: state => (
    <div>
      <Counter />
      <Counter />
    </div>
  )
})

Looking good, but is there a typo in the scope example above? const Counter assigned two times.

Should that be:

// main.js
import * as counter from "./counter.js"

const CounterA = scope({ counterA: counter.Counter })
const CounterB = scope({ counterB: counter.Counter })

app({
  init: {
    counterA: counter,
    counterB: counter
  },
  view: state => (
    <div>
      <CounterA />
      <CounterB />
    </div>
  )
})

?

@jorgebucaran it would be great if you can share your work so far (along with a small working sample) in a PR or demo repository, for the sake of hands-on analysis, experimentation, and critique from the user community.

P.S. Congratulations on reaching 12K stars, which I think are well deserved for your work!

The recommended should be onclick={[downBy, 10]}. It reuses the concept of tuple that we're also going to use for effects, init, etc.

I disagree. I think vanilla JS downBy(10) is the way to go.

I think you can get rid of the tuples concept all together by allowing actions to return a Promise. If you need to do multiple things, then its up to you to write Promise.all([...]).

I also think you can get rid of the subscription concept. Its just a list of actions called on very tick....

Ok I think I get what you are saying with subscriptions. Looking forward to it 🙏

2.0 changes look awesome!

Components: I'm a beginner hyperapper but I'm planning on using it to tie in stenciljs components. For me, if it's not in the state tree it's not in hyperapp.

Actions return a new state (not a partial state): sounds like a perfect opportunity to use immer in one's app - it fixed bugs for us on day one.

Is there any place for Observables in hyperapp? That's the one thing I love about Angular (which I use daily). The ngrx memoized selectors in particular are just awesome - they keep computed state out of the tree and only recompute when their inputs change. Having selectors as rxjs Observables allows to mix and match them and makes complex derivative state (or faking a state graph) easy.

@tkivela @jdh-invicara Thank you, yes, my mistake. It should be and counterA: counter.state. I think this will ultimately be a matter of style. I personally prefer this, but the later example using the scope function is not bad either. We'll see what the community wants.


@brodybits Will do as soon as I can! Refer to my OP in the meantime or ask me anything.


@Pyrolistical I disagree. I think vanilla JS downBy(10) is the way to go.

I like downBy(10) more too, but I still think both should be possible; we'll see.

I think you can get rid of the tuples concept all together by allowing actions to return a Promise.

No. In 2.0 all side-effects will be managed, so there is no room or need for anything like Promises, callbacks, async/await. This part is non-negotiable. 😉

If you need to do multiple things, then its up to you to write Promise.all([...]).

We don't need that. There should be a custom FX for that.

I also think you can get rid of the subscription concept. Its just a list of actions called on very tick....

Absolutely, not.


@russoturisto Is there any place for Observables in hyperapp?

In core? No. In userland? Why not? :)

Side-effects will be managed like Elm.

But please give me an example of side effects you’re talking about here.

@infinnie The OP has a few examples, what would you like to see in particular?

@russoturisto Is there any place for Observables in hyperapp?

Would it be better to write a subscription that dispatches from an Observable stream of actions, or pass the dispatch function to a library that wires this up outside of Hyperapp?

@okwolf Or writing a custom subscription, but I think we'll need _none_ of that thanks to managed FX and the subscriptions API.

@jorgebucaran to clarify, I meant a custom subscription, not one built into Hyperapp.

@russoturisto @jorgebucaran I'm also interested into the Observables support in Hyperapp 2.0, as I have an app that intercepts the actions and performs async processing for organizing the business logic. I guess the middleware and the dispatcher will help me remove a lot of boilerplate code I have currently, but still I would like to reuse some of the logics if I intend to start using Hyperapp 2 instead of rewriting everything from scratch.

See my comment in https://github.com/hyperapp/hyperapp/issues/533#issuecomment-378906643

Update: I was talking about another way of injecting actions and views using the existing APIs, and that synchronous actions could be enough for asynchronous effects. Sorry for my being confusing.

@infinnie I have no idea what you are talking about.

@lucianlature Are you familiar with Elm? Effects in 2.0 will work a lot like commands in Elm. There will be no need for observables.

@jorgebucaran Not really, going to learn more about it the next days :)

@lucianlature 2.0 will make it even simpler. Here is an example with http.

import { h, app } from "hyperapp"
import { http } from "@hyperapp/fx"
import { URL } from "./contants"

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

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

2018-04-05 22 30 19

I was just demonstrating that asynchronous effects and dynamic data manipulations could be implemented by a few ordinary, synchronous actions without any other device, albeit a little more verbose.

Sorry for that misunderstanding.

@infinnie Okay, sorry for the haste. I had a closer look and I definitely don't want to take that direction. Thanks :)

It was my fault.

And just by the way: vanilla on-event handlers are expected to take an event as the argument and return an optional Boolean. Maybe it should be considered that the event handlers with the new signature be renamed like _event or $event to avoid confusion.

Just my opinion.

There will be no vanilla on-events in 2.0.

I thought I should add that because I finally split up the vDOM and stateMAN parts of Hyperapp, in 2.0 it will be possible to do tree-shaking/dead-code elimination even for people who want to use Hyperapp as a state manager layer to target non-browser rendering engines. 😆

@jorgebucaran use Hyperapp as a state manager layer to target non-browser rendering engines

I’m looking forward to seeing this. Could you reuse the same fx and just rewrite the view layer integration, or would you need all custom fx?

@okwolf Hmm, let's see, the point of FX is to abstract specific platform APIs, so a complete port to a different architecture would need to have the FX implementation rewritten for each specific platform (evidently). Application code wouldn't need to change though.

@jorgebucaran so does that mean that you will still be able to use the fx that @hyperapp/* exports, but those will be rewired to do something different on that platform?

Thinking about the problem some more, wouldn't this be more of an issue with subscriptions than the ones that are like Elm commands?

import { delay, http } from "@hyperapp/fx"

vs.

import { Mouse } from "@hyperapp/subs"

I am going to paste here part of what was discussed on slack, in case it is useful for someone.

Since handlers, such as onclick, accept a function state => newState it is possible to write a deep merge function that may apply any action in a nested part of the state, or replace part of the state:

const merge = value => (state = {}) => {
  if(value === state) {
    return value
  }
  if(typeof value === "function") {
    return value(state)
  }
  if(typeof value === "object" && !Array.isArray(value)) {
    const updated = {}
    Object.keys(value).forEach(key => {
      updated[key] = merge(value)(state[key])
    })
    return {
      ...state,
      ...updated
    }
  }
  return value
}

const button = (
  <button onclick={merge({ 
    nested: { 
      value: val => (val + 1), 
      nested2: { value: 5 } 
    }
  })}/>
)

@jorgebucaran

FWIW, I really like the vision behind 2.0

I don't actual use hyperapp (because of the state management) but rather my own naive virtual DOM implementation that has many of same principles expressed in the OP and Elm Architecture

My view functions return data only and my "update" functions return a tuple with a state and an effect object (aka Cmd)

I've been follow the project for awhile, but this really piqued my interest

@efleming969 Are you representing tuples with arrays or are you using a custom Tuple type?

The OP mentions what's coming in 2.0, but it doesn't say much about what features will be removed or what's breaking, so I've updated it to reflect this important point. See the section titled "What's breaking?".

The TL;DR is that slices will be removed and that actions causing side effects will need to be upgraded to the new managed FX API.

Tuples are just arrays. Here is what my API fundamentally looks like

const create = function ( context ) {

    const init = function () {
        return [
            { message: "loading data", items: [] },
            Command.query( "http://localhost:8081/items", context.send( actions.ItemsLoaded ) )
        ]
    }

    const updater = {
        [ actions.ItemsLoaded ]: function ( message, state ) {
            return [
                Utils.merge( state, {
                    message: "done loading",
                    items: message.data
                } ),
                Command.none
            ]
        },
        [ actions.AddItem ]: function ( message, state ) {
            const formData = Utils.currentForm( message )

            return [
                Utils.merge( state, {
                    message: "adding item"
                } ),
                Command.execute( "http://localhost:8081/add-item",
                    { text: formData.text },
                    context.send( actions.ItemAdded ) )
            ]
        },
        [ actions.ItemAdded ]: function ( message, state ) {
            return [
                Utils.evolve( { items: Utils.append( message.data.text ) }, state ),
                Command.none
            ]
        }
    }

    const render = function ( state ) {
        return main( {}, [
            h1( {}, [ state.message ] ),
            form( { onsubmit: [ context.send( actions.AddItem ) ] }, [
                input( { type: "text", name: "text" } ),
                button( {}, [ "add item" ] )
            ] ),
            ul( {}, state.items.map( i => li( {}, [ i ] ) ) )
        ] )
    }

    return { init, updater, render }
}

@efleming969 Thank you for sharing. Yes, we're going to use arrays too. Wouldn't be awesome if JavaScript had a tuple type? 😢

@efleming969 are you using your own element creator functions here? Like h1, form, etc. or are you using @hyperapp/html? If you are using the html package then props are optional so you can write h1(state.message) if you prefer.

Sorry for off topic; just thought I would mention it.. your API looks cool!

The direction of Hyperapp 2.0 is irking me, so I've been doing some thinking. I understand the two main drivers for HA2 is easier typing + dynamic imports. I could not careless about typing, so I threw out that requirement 😛

So thinking back to all the discussions we had regarding state vs actions vs modules vs views, I've decided to take a stab at combining all those concepts to get dynamic import to work.

Everything is a module. Every module only requires one required function, view(state). All other properties of the module are just state/actions. A module can depend on another module. The parent module owns the entire child. It can read the state and call actions on the child. State slice is basically the same thing as a child module.

Actions return the state update, but can also return a promise of the state update. State updates can even include defining new actions or 🎉 loading new child modules 🎉 .

Here is the sample app of my concept. I create a Counter module which is used twice in the parent Application. The parent application also shows how dynamic imports work.

import {h, Module, Render} from 'hyperapp';

const Counter = Module({
  name: '',
  count: 0,
  down(value, state) {
    return {
      count: state.count - value
    };
  },
  up(value, state) {
    return {
      count: state.count + value
    };
  },
  view({name, count, down, up}) {
    return <div>
      <h1>{name} {count}</h1>
      <button onclick={() => down(1)}>-</button>
      <button onclick={() => up(1)}>+</button>
    </div>;
  }
});

const Application = Module({
  Left: Counter,
  Right: Counter,
  async loadMore() {
    return {
      Stuff: await import('./stuff')
    };
  },
  view({Left, Right, loadMore, Stuff}) {
    return <div>
      <Left name="Left" />
      <Right name="Right" />
      <p>Sum {Left.count + Right.count}</p>
      <button onclick={loadMore}>Dynamic load</button>
      {Stuff && <Stuff/>}
    </div>;
  }
});

Render(Application, document.getElementById('app-entry'));

I feel this direction captures the spirit of Hyperapp which we all fell in love with.

@Pyrolistical The main driver for 2.0 is easier testing and safer/purer code, dynamic imports and typings are just nice to have.

I feel this direction is captures the spirit of Hyperapp which we all fell in love with.

Thanks for your opinion, but I am not sure I agree with this narrative.

Modules make it easy to test too

@Pyrolistical Testing in 1.0 is not much harder than in most other frameworks, but it's still very difficult. It's not about modularization, it's about controlling side effects. The only player in town doing this right is Elm. I aspire no less than that.

@jorgebucaran proposal for a shorter name for subscriptions: abos. This is the short (and plural) for abonnement (https://en.wiktionary.org/wiki/abonnement), widely used in French and German as well. Just popped up in my head when I read the RFC, feel free to ignore it 🙈

@qur2 Abonar is also a word in Spanish with a few meanings, one of which is to subscribe, but it's so rare I didn't know it until now that I checked. TIL 😄

Ha ha, In México abono is fertilizante orgánico and abonar is usually about spreading manure on crops. But that's just Mexican usage. Although it has been a while since I've hung out with Mexican farmers, so usage could be different these days.

@rbiggs I can confirm that _abono_ is also the term used for manure in Latin America, don't know about España.

@dancespiele ? 😆

Abono in España means fertilizante orgánico too and also abonarse means register in some service which you will pay monthly or yearly.

Isn't abos an offensive, racial slur in Australia? I don't think we need a shorter term for subscriptions because you would only really use it one time. And after minification it will be a single letter or two, right? Besides, its quite clear for the user what the word subscriptions indicates, whereas a shorter term may not indicate clear what it does.

@jorgebucaran and friends, curious if you saw what developit did with Preact. He create a function reactive light weight version: preact-cycle, obviously inspired by Cyclejs, which was inspired by Elm.

Got a question (after researching reselect). Right now I can plugin reselect into a component (because a selector is just a function that takes the state object):

export const view = (state, actions) => (
  <div>
    <h1>Todo</h1>
    <ul>
      {state.todos.map(({ id, value, done }) => (
        <TodoItem 
                id={id}
                value={value}
                done={done}
                someDerivedValue={someDerivedValueSelector(state)}
                toggle={actions.toggle}
         />
      ))}
    </ul>
  </div>
)

Will that still be possible in 2.0?

Thanks, :)

@russoturisto Right now do you mean in 1.0?

In 2.0 the view layer will only change so slightly to incorporate the ability to dispatch actions, but other than that the VDOM will be the same, so _yes_.

Great, I have another beginner question:

I'm still learing the code but based on my current understanding patch gets called recursively on the VDom tree. Hence, given that any VDom node that didn't change is skipped in re-rendering, I'm pretty sure that the below technique will also work:

const TodoItem = memoizeVdom(attrs, ({ id, value, done, toggle }) => (
  <li
    class={done && "done"}
    onclick={() =>
      toggle({
        value: done,
        id: id
      })
    }
  >
    {value}
  </li>
))

where "memoizeVdom" keeps a copy of the last VDom tree returned by this component as well as the last attrs. On the next invocation it does a pointer equality check on the attrs and if they are the same simply returns the last copy of the VDom for this Component (and all child Components):

export function memoizeVdom(attrs, callback) {
    let lastAttrs, lastVdom

    return function (attrs) {
        if(attrsChanged(attrs, lastAttrs)) {
            lastVdom = callback(attrs)
        }
        lastAttrs = attrs
        return lastVdom
    }
}

function attrsChanged(attrs, lastAttrs) {
    if(!lastAttrs) {
        return true
    }
    for(const attrName in attrs) {
        if(attrs[attrName] !== lastAttrs[attrName]) {
            return true
        }
    }
    return false
}

Am I correct in the assumption that this would still work in 2.0 (just want to be super sure)?

@russoturisto memoizeVdom should still work, but TodoItem will look more like this:

import { toggle } from "./todo/actions"

const TodoItem = memoizeVdom(attrs, ({ id, value, done }) => (
  <li
    class={done && "done"}
    onclick={
      toggle({
        value: done,
        id: id
      })
    }
  >
    {value}
  </li>
))

Great, thank you!

PS

If we had 2.0 (and polymer 3.0) a year ago I wouldn't be coding Angular now!

Can someone enlighten me on rationality behind declarative effects in JS?

I mean – I understand that pure functions are easier to test. Basically, you don't need mocks. But the readability of main code starts to remind me raw lambda calculus (you know what I mean).
From the position of neutral observer, who never used HyperApp, but used tens of other frameworks, I can tell you that 1.0 API seems much cleaner and simplier than those 2.0 samples.

In Haskell and PureScript we have a great syntax sugar called do-notation to make the effectful code look imperative(!). Because imperative code describes natural sequences the best (the reason Go and Ruby dominate devops...). Here we do the opposite – wrap basic imperative code in a layer of declarative proxies. And all that to achieve... what?

Personally, I don't see a reason to trade a few lines of mocks in tests for drastically decreased readability and increased size of the main code. But maybe I miss something – feel free to correct me.

@ivan-kleshnin Can someone enlighten me on rationality behind declarative effects in JS?

We are already using a declarative API (virtual DOM) to express DOM changes and events (onclick, oninput, etc). What we are missing is a declarative API to express global DOM events (mousemove, keyup, etc) and things like setTimeout, setInterval, random, fetch, etc.

2.0 is the last piece of the puzzle.

But the readability of main code starts to remind me raw lambda calculus (you know what I mean).

No it doesn't. Let's take this example in 2.0.

import { h, app, fx } from "hyperapp"
import { URL } from "./constants"

const quoteFetched = (state, [{ content }]) => ({ quote: content })
const getNewQuote = fx.fetch(URL, quoteFetched)

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

Now let's assume 1.x also had the ability to run an action during initialization, then the only major difference would be how getNewQuote is written.

// 1.0
const getNewQuote = () =>
  fetch(URL)
    .then(data => data.json())
    .then(quoteFetched)

// in 2.0 → const getNewQuote = fx.fetch(URL, quoteFetched)

The 2.0 version is consistent with the rest of the declarative architecture (the result of fx.fetch is an object, it doesn't run any code), consists of less code and it's simpler to test.

In 2.0 everything is more reliable because we can write our actions using pure functions. Pure functions take an input, do some math, and return an output. In 1.0 only reducers are pure functions, but even those sometimes aren't because they can cause side effects like writing to local storage, returning a promise, etc.

In 2.0 we can still make mistakes, after all this is JavaScript and we are not protected from runtime errors like Elm is, but that's also our strength, because this is JavaScript we can pierce the bubble any time we want to as long as we assume the cost of doing so. In 2.0 there will be less room to do things wrong since we are not the ones producing side effects, Hyperapp is.

Here is how you would test getNewQuote in 2.0 by the way.

// tests/actions.js
import { deepEqual } from "assert"
import { fx } from "hyperapp"
import { quoteFetched } from "../actions"
import { URL } from "../constants"

deepEqual(getNewQuote(URL, quoteFetched), fx.fetch(URL, quoteFetched))

Testing the 1.0 action is a lot more complicated and you are confronted with a lot of options, fetch-mock, nock, etc. getNewQuote is a fairly simple action, it calls fetch only once, but as the action becomes more complicated, so will become your test code! In 2.0 it's always a single deepEqual.

wrap basic imperative code in a layer of declarative proxies. And all that to achieve... what?

Break up the dangerous code from the safe code. Then I write the test for the dangerous code only once. I intend to offer 99.9% of all the effects you will ever need in 2.0, just like Elm.

If you ever have the need to create your own custom effect, then you can do that too. You'll need to test it, of course. Users of your custom effect will never need to test the dangerous stuff, only their business logic.

Win-Win-Win

I've been pondering exactly the same things as @ivan-kleshnin . I see the advantages going declarative, but the problem with declarative approaches (especially for those devs that are new to the framework) is that they must learn a new declarative API for doing things, instead of using the API (/tools) they're already familiar with. This problem applies to all going declarative route, not just hyperapp. If one wants to use some library which is has not it's own fx yet, then one must write a new fx proxy for it and adding new abstraction layer always comes at a cost (time spent developing the custom fx, new API learning for consumers, keeping it up-to-date if encapsulated library API changes etc).

So in my opinion it's not all win-win-win, there's definately some weighting to do for devs if it's worth it for them in their own use case.

Regarding lifecycle events, you might want look at finite state machines as a formalism (like how https://github.com/jakesgordon/javascript-state-machine works).

@jorgebucaran

I really like this

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

but immediately wonder how we handle the non "optimal" real world flows - eg the fetch times out, we get a server error etc etc? We need to handle it or alert the user some how and that complicates our nice clean declarative code.

I wouldn;t want to use innerHTML either.

@tkivela ...they must learn a new declarative API for doing things, instead of using the API (/tools) they're already familiar with...

@tkivela If one wants to use some library which has not it's own fx yet, then one must write a new fx proxy...

This is a valid concern and it will be one of those things we'll need to work on moving forward, better documentation, helping newcomers, etc.

Hyperapp 1.0 is already using a declarative API (VDOM) to express DOM changes and Element events (onclick, oninput, etc). What we are missing is a declarative API to express Global Events (mousemove, keyup, etc) and effectful code like setTimeout, setInterval, random, fetch, etc.

@SteveALee It's just a simple example. The http/fetch effects can handle errors too, obviously: fx.fetch(url, action, options) or fx.fetch(url, options). Possibly the former to be consistent with other effects.

@mhr Can you elaborate?

I have read and now also thought about everything that came up so far in this thread and I really like most of the ideas you have, @jorgebucaran! Great vision that you have here :)

One thing that I think would be worth putting more focus on is the idea of a scope function that you teased in https://github.com/hyperapp/hyperapp/issues/672#issuecomment-378550340 and the following.

Let's assume that we want to implement multiple autocompleted text inputs in our application and we would like to make them all look the same and behave in the same way while working in completely different contexts. Some of the autocompletes appear at a deeply nested point in the view tree and most of the views in between the autocomplete and the root are not dependent on any state.

Sticking to the Elm architecture, we now need to specify a point in the app state where each of the autocompletes can put their state.

With this setup, to introduce the autocomplete into a previously state-independent view, I now need to do one of the following so that the autocomplete state is initialized correctly:

  • Refactor that view and all its ancestors to also follow the pattern of re-exporting the (possibly scoped) initial child state as seen in your examples. Like that, it is eventually indirectly included in the app call. This means sticking to separation of concerns but making all components on the way much more complex; sort of an inverse prop-drilling problem
  • Skip all ancestors and directly initialize the state in the app call through a direct import from the view that uses the autocomplete, ignoring separation of concerns and introducing a rather unintuitive dependency but keeping the other components simple

I would argue that both approaches are unsatisfactory and it would be worth the time to think about an easier-to-use API for this problem. To me at least, this seems like a pretty common use-case.

What do you think?

@Cryza The way I want to build things like these is using the same approach I proposed in the reusable counter. If pushed to say, this approach is similar to the first bullet point on your list (definitely not the second), but can I argue it is without the problem you described? 😄

The trick lies in the map/scope/supercalifragilisticexpialidocious function. Through this function, you are able to transmit paths/slices (to.a.partial.state.location) through the view tree. These functions can be composed naturally so that intermediate views only need to teach the scope they are aware of to their children but the result is the full correct path. Total reusability.

Any proposals on middleware yet? I've seen some chatter on Slack about this, but nothing updated here yet. We need to make sure this new API will still support robust dev/debug tools.

Two of the possibilities I've heard:

  • Maybe subscriptions can do the job?
  • API to provide wrapper for dispatch function

@okwolf Nope, subscriptions couldn't do the job, because they are run when the state changes. Middleware should be a hook into dispatch, so executed at the same time actions run.

@jorgebucaran so subscriptions aren't an option because by the time they are run the state has already changed?

Right, but it doesn't need to be like that. The reason subscriptions don't help here is because we want middleware to enhance dispatch and there is no way to do that with subscriptions.

A possible API could be:

app({
  enhancers: action => newAction
})

Essentially an action reducer (as opposed to a state reducer).

@jorgebucaran thanks for your reply! I agree that the pattern is completely composable and that it is not limited to a single level of parent-child relations like your example. And I think it is a really valuable approach!

The problem I expect to appear however is the following:
A regular view that does not introduce new state does not need to export any initial state like you did in the example. It does however have to re-export the initial state of its children (and its ancestors recursively) if they have one so that it can eventually be applied in the app call. That means all ancestors of a view that introduces new state now have to be refactored because an implementational detail of the leaf view has changed.

I will skip the alternative of breaking encapsulation and directly importing the leaf state in the app file since it feels unintuitive to me.

So to avoid this kind of refactoring, maybe it would make sense to introduce some hyperapp-managed logic for automated scoping again? Or maybe promote the example pattern including child state to be the default so that every view exports the states of its children by default, and maybe they start out as null?

I hope this was understandable 🙄

@Cryza That means all ancestors of a view that introduces new state now have to be refactored because an implementational detail of the leaf view has changed.

Why, no. That's the point of map, it hides the details (the path). If the details changed, you only need to change the map function of the view that changed, not their children or any of their ancestors.

To simplify the discussion, let's pretend there is no map function, but a path array property with the path to a partial state. Any view that receives a path property can pass path.concat("more", "path") as a path prop to their children.

If any of its ancestors changes the map function, the view receiving map does not need to change as it will have the correct path up to that point.

To give my 2 cents I think this is definitely a step in the right direction. Great to see lot of careful thought going into this area. I have also done a lot of thinking in this area with my framework https://github.com/tjdavies/copal.

Ill give you my takeaway from building large app this way. The main one was actions should be composable. You quickly run into situations where you want to some actions in others with some additional functionality.

an example here spells that out
https://codesandbox.io/s/github/tjdavies/copal/tree/master/examples/reddit
where the refresh action is reused again when calling selectReddit

To start with I took the elm route much as stipulated here. But I found composing together actions tricky. In the end I settled on a functor based approach you can see in copal although its not something i'm totally happy with.

Like may of these things solving one problem well opens up higher level ones. Still hopefully some food for thought.

@tjdavies I had a glance and noticed you pass the raw actions into copal and use them to create dispatch and also in middleware. This is not unlike Hyperapp 1.0.

In 2.0, however, I'm moving away from this approach and having actions decoupled from everything, this is possible because dispatch is built-into the view layer, which we can afford since Hyperapp is both the state manager and virtual DOM in one.

I believe this unwiring of actions will give us high composability and reusability. Dynamically importing actions will be another win for this approach.

In 2.0 I'm also going to promote a style of reusable components as described here. I think it is "similar" to your a.map function.

Copal looks cool, maybe I'll borrow a few ideas. 😄💡

@jorgebucaran I'm not talking about the path inside the state, I'm talking about the fact that the initial partial state that the component introduces has to find its way to the app call.

Let's assume we have an app like the following:

// page.js
export const Page = state => ({ content }) => (
  <main>
    {content}
  </main>
)
// main.js
import { app, h } from "hyperapp"
import { Page } from "./page.js"

app({
  init: {},
  view: state => (
    <Page content="Hello World!" />
  )
})



md5-d96f18b98851e77615a5e6a49ae28d8b







md5-c357f3c44d7e7a801fc6bf237783d6ab



```javascript
// main.js
import { app, h } from "hyperapp"
import { Page, state as pageState } from "./page.js"

app({
  init: {
    page: pageState // This needs to be done so that the nested counter state is initialized
  },
  view: state => (
    <Page
      content="Hello World!"
      state={state.page}
      map={action => ({ page: action })} // Even though Page has no actions itself, its children do
    />
  )
})

I tried to highlight the points that I think need investigation with comments. So I am positive that it is possible to nest stateful views in this way, but I am unsure if this is the best way of doing it, especially in larger, more deeply nested apps.

@Cryza what if rewrite your example without global initial state definition:

// main.js
import { app, h } from "hyperapp"
import Page from "./page.js"

app({
  init: {},
  view: state => (
    <Page
      content="Hello World!"
      map={action => ({ page: action })}
      state={state.page}
    />
  )
})

```js
// page.js
import Counter from "./counter.js"

const initialState = { title: 'Hello' }

const Page = ({ content, map, state = initialState }) => (


{content}
map={action => ({ counter: action })}
state={state.counter}
/>

)

export default Page

```js
// counter.js
const initialState = { value: 0 }

const increment = state => ({ value: state.value + 1 })

const Counter = ({ map, state = initialState }) => (
  <main>
    <h1>{state.value}</h1>
    <button onclick={map(increment)}>+</button>
  </main>
)

export default Counter

or maybe introduce some mechanism for component level state initialization like oninit lifecycle event..

@Cryza I would try to avoid nesting views like that and keep everything as flat as possible (one-level), as it will lead to more complicated code. I am not making this advice up. It's the same advice given by the developers of Redux. It's why Elm's Html.map tells you "it should not come in handy too often. Definitely read this before deciding if this is what you want".

But you get the gist of it, and that's great. Here is the actual working code based on the code you wrote above.

counter.js

import { h } from "hyperapp"

export const state = { value: 1 }
export const actions = {
  increment: state => ({ value: state.value + 1 })
}

export const Counter = ({ state, map }) => (
  <main>
    <h1>{state.value}</h1>
    <button onclick={map(actions.increment)}>+</button>
  </main>
)

page.js

import { h } from "hyperapp"
import { Counter, state as counterState } from "./counter.js"

export const state = {
  content: "Hello",
  counter: counterState
}

export const Page = ({ state, map }) => (
  <main>
    {state.content}
    <Counter state={state.counter} map={map} />
  </main>
)

index.js

import { h, app } from "hyperapp"
import { Page, state as page } from "./page.js"

app({
  state: { page },
  view: state => <Page state={state.page} map={action => ({ page: action })} />
})

@frenzzy interesting idea! I am normally a big fan of default values, but in this case I think I would refrain from it since it makes the actual structure of the application state really hard to determine by looking at code. Even at runtime, one would need to make sure that every view that contributes to the state has been mounted at least once and has executed an action that writes to the state before the complete state is visible. I can imagine terribly hard to debug issues resulting from collisions and inconsistent state structures here.

@jorgebucaran Thanks for the explanation!

If I understood you correctly, what you are saying is that you do not promote separation of concerns by scoping in the state, even if it is something of local concern only like the content of an input field or the state of a (custom) dropdown.
This necessarily means that the main.js will need to know directly about every view that contributes a default state and give it a place in the first level of the state tree, resulting in something like the following for the previous example:

// page.js
import { h } from "hyperapp"
import { Counter } from "./counter.js"

export const Page = ({ state, map }) => (
  <main>
    <Counter
      state={state.counterOnPage}
      map={action => ({ counterOnPage: action })}
    />
  </main>
)
 // index.js
import { h, app } from "hyperapp"
import { state as initialCounterState } from "./counter.js"
import { Page } from "./page.js"

app({
  state: {
    counterOnPage: initialCounterState
  },
  view: state => <Page />
})

Which means the following:

  • counterOnPage has to be kept in sync over two files, which might result in refactoring errors when people forget about one of them (this applies even if only the view is reused and each instance has its own actions and default state)
  • the index.js will become a central hub for investigating application state, making it really easy to argue about the state structure but also possibly making the file confusing because it might get really long for apps with a complex state

Regarding your other two references:
I don't think the Redux advice applies here as it was made in a different context. With Redux, you can always assume that there is the possibility to keep certain state local, which is not possible here. I am pretty sure nobody would advise to put e.g. the current content of an arbitrary comment box of a newsfeed item on the topmost level in the Redux state. Especially with combineReducers being used in most Redux applications, the term "one-level" needs to be taken with a grain of salt.

Don't get me wrong, I completely love the idea of keeping the complete framework pure and as easy to argue about as possible, but I would simply love to have similar tools for the abstraction and encapsulation of state and actions that we already have for views. To me, that would be an immense argument towards advanced-size usability.

I definitely promote separation of concerns, so you're definition must differ from mine. 💯

@Cryza This necessarily means that the main.js will need to know directly about every view that contributes a default state and give it a place...

Of course! Why make it sound as if it was a bad thing? It's transparent and simple to reason about. I wish it was my original idea. 😄

Children encapsulate implementation details, for example, what button increments the counter and how to increment it. The parent knows nothing of these things and is only responsible to teach its children their state slice and action map (what they don't know since it is the parent who is responsible for _wiring_ their state). You could go one step further and create "connected" components that act on a specific state slice where you only need to pass a map and the component factory can return a component that knows how to pick up and update the state. I am not a big fan of this approach since I prefer the flexibility of the example I wrote you.

I am pretty sure my references are relevant, we don't have a combineReducers in Hyperapp and we don't need it. In 1.0 we have slices, in 2.0 we'll have map. Both approaches are somewhat opinionated, but map is more flexible as it lets you decide what events/actions to map or not.

Don't get me wrong, I appreciate your feedback, but I am not trying to create another React.

@jorgebucaran I think I'm not following your thoughts anymore now. The way I read your last comment, you are bringing up the positive arguments about both approaches and ignoring the concerns I brought up. Which of the two approaches do you promote now, non-encapsulated state or hierarchical mapping?

Please correct me if I'm wrong (and if I'm right this could be blasphemy)
but since a component is just a function with it's own scope one could very
well maintain local state in it and whap every outgoing call in a nested
function that can do just about anything?

Thanks, :)

On Mon, Apr 16, 2018, 9:52 AM Manuel Hornung notifications@github.com
wrote:

@jorgebucaran https://github.com/jorgebucaran I think I'm not following
your thoughts anymore now. The way I read your last comment, you are
bringing up the positive arguments about both approaches and ignoring the
concerns I brought up. Which of the two approaches do you promote now,
non-encapsulated state or hierarchical mapping?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/hyperapp/hyperapp/issues/672#issuecomment-381653950,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AHwSnciuwLcbJGnFKg1EoClvCOak0P2Aks5tpL5CgaJpZM4TCud3
.

@russoturisto Every time we render your app, we compute the virtual DOM from scratch, so there's usually no straightforward way to maintain local state on a component basis.

@Cryza Hierarchical mapping. And I don't really see a problem with refactoring in this example. It seems you want immutability with a single state tree and React's local state at the same time but without any of the issues from either paradigm. Good luck finding that! 😜

@jorgebucaran I think we both hit on a very similar API with the creation of dispatchable actions. convergent evolution maybe. I think your right to remove it in 2.0 it makes things a lot cleaner. It does couple the view to the state management but i think its the right choice for hyperapp.

To clarify what I was trying to get at when talking about with composable actions. Its not in regards to components although I think you example does look like a good approach (I use lens heavily to do this type of state access http://ramdajs.com/docs/#lens) but to function composition.

e.g a selectReddit action can be composed of two other actions.
selectReddit = compose(refresh, setReddit)

if the signature of the action function is
state => [state, ...effects]
then composition is more difficult.
copal wraps state + effects together in a single object so that actions can be "chained" together easily.

Im not sure thats the right approach but I wanted to get you thinking about this ;)

Still very positive about the direction this is going.

@tjdavies I believe the signature for actions can only have a single effect: state => [state, effect] This effect may be a itself a composition of multiple effects.

Thanks, @tjdavies! 🙇

I want to make sure I understand all of this, so let me just ask the simplest question: what does it mean to compose refresh and setReddit? What do you achieve by "composing" two functions?

@jorgebucaran Essentially an action reducer (as opposed to a state reducer).

How about a dispatch reducer? That would give your enhancer freedom to inject logic before or after your original dispatch.

What do you think of something like this?

const logger = dispatch => action => {
  const getState = () => dispatch(state => state)
  console.log('before state: ', getState())
  console.log('action: ', action)
  dispatch(action)
  console.log('next state: ', getState())
}

app({
  enhancer: logger
})

@okwolf TBH I'm still not sure. I don't want to give users too much power, but I do want a way to "hook into" the dispatch function, so that you can, e.g., record actions for later replay.

@jorgebucaran so you will not take part in fxapp?

@willin so you will not take part in fxapp?

@jorgebucaran has reviewed some of the work I've done on fxapp, which has served as a playground for trying out a few similar ideas, although closer to the original @hyperapp/fx. I still plan to keep the project around, and evolve it in response to the direction Hyperapp 2.0 is headed.

I've been thinking about the local state problem and I think the following user-space plugin would get around rebuilding of virtual dom everytime:

export interface IWarpBubble {

    warp(
        BubbleConstructor: WarpBubbleConstructor,
        ...constructorParams: any[]
    ): WarpBubble;

}

export interface WarpBubbleConstructor {
    new(...args: any[]): WarpBubble;
}

export class BubbleMap extends WeakMap<any, any> {
}

export class WarpBubble implements IWarpBubble {

    childBubbleMap: BubbleMap | null = null;

    warp(
        BubbleConstructor: WarpBubbleConstructor,
        ...constructorParams: any[]
    ): WarpBubble {
        if (!this.childBubbleMap) {
            this.childBubbleMap = new BubbleMap();
        }

        if (!constructorParams.length) {
            throw new Error('Cannot engage new warp bubbles without keys');
        }

        let bubbleMap = this.childBubbleMap;

        for (let i = 0; i < constructorParams.length - 1; i++) {
            let key = constructorParams[i];

            let nextBubbleMap = bubbleMap.get(key);
            if (nextBubbleMap && nextBubbleMap instanceof Map) {
                bubbleMap = nextBubbleMap;
            } else {
                nextBubbleMap = new Map();
                bubbleMap.set(key, nextBubbleMap);
                bubbleMap = nextBubbleMap;
            }
        }

        const lastKey = constructorParams[constructorParams.length - 1];
        let childBubble = bubbleMap.get(lastKey);
        if (!childBubble) {
            childBubble = new BubbleConstructor(...constructorParams);
            bubbleMap.set(lastKey, childBubble);
        }

        setTimeout(() => {
            this.sweep();
        });

        return childBubble;
    }

}

Then it could be used like:

class RootWarpBubble extends WarpBubble {
//...
}

class ChildWarpBubble extends WarpBubble {

  constructor(
     private typeInScope,
     private index
  ) {}

//...

}

const root= new RootWarpBubble();

export const view = (state, actions) => (
  <div>
    <h1>Todo</h1>
    <ul>
      {state.todos.map(({ id, value, done }) => (
        <TodoItem
                bubble={root.warp(ChildWarpBubble, "TodoItem", id)}
                id={id}
                value={value}
                done={done}
                someDerivedValue={someDerivedValueSelector(state)}
                toggle={actions.toggle}
         />
      ))}
    </ul>
  </div>
)

It seems that OO approach would be appropriate for local state. In case this is useful I started a ts project for this (with more conservative gc since I don't know how aggressive vms are with WeakMap).

https://github.com/russoturisto/warp-bubbles

It compiles but unfortunately I'm too swamped to actually test it, but it seems like this should work just fine. It is a constrained but strait forward approach (I think). Hopefully this helps someone.

Thanks, :)

I'm very fond of Redux Devtools (http://extension.remotedev.io/) - save's me lots of time when debugging.

So, here is yet another beginner question: Would 2.0 be able support a user space plugin (possibly though middleware) where I wrap each action and record a unique type for it (probably the name of the function being called)?

If I understand correctly, it would then be theoretically possible to hook the HA store into the Redux Devtools plugin (not unlike what ngrx guys did - https://github.com/ngrx/platform/blob/master/docs/store-devtools/README.md).

Thanks, :)

@russoturisto You are looking for #120! :) tl;dr: Yes. We just need to decide what the middleware API is going to look like.

Awesome, thank you! :)

On a related devtools topic, will it be possible to inspect/manipulate the VDOM from the browser like the React devtools in 2.0? I guess we would need to provide an API for this or attach the vnode data to the actual DOM nodes. This could become a dev build only thing. (See #417)

@okwolf Interesting. DOM nodes will play a bigger role in a future patch rewrite #499, so we should revisit this then.

Am I the only one who think that this onclick={{ count: 0 }} is weird?

I haven't used Hyperapp yet, but I have some experience in React/Vue/Angular(1) and to me it doesn't looks like a good idea - onclick prop should take a function, right? Why it takes an object?

EDIT: Chances are this little feature will not make it to 2.0, two reasons why:

  1. How do we get the name of the action while debugging? A variable is just a reference to an object, and the same object can be referenced by multiple variables. Functions have .name which allow us to find out the name of the action, but not objects.
  2. We need the state to calculate the next state, e.g., { ...state, count: 0} if we decide to merge by replacing instead of shallow merging.

@emil14 Actions are usually a function, but when I wrote that example I was thinking they could be an object too. I'm still not sure whether _that feature_ will make it to the 2.0 cut or _not_, but I'll explain what I had in mind when I originally wrote it.

For example:

const reset = { count: 0 }
const increment = state => ({ count: state.count + 1 })

then use them like so:

<button onclick={reset}>Reset</button>
<button onclick={increment}>Increment</button>

Let's see another example. In the typical ToDo list app you usually have a way to filter items by completion, todo, etc.

import { filter } from "./actions"

//...

<div>
  {Object.keys(Filters)
    .filter(key => Filters[key] !== state.filter)
    .map(key => (
      <a href="#" onclick={actions.filter({ value: Filters[key] })}>
        {key}
      </a>
    ))}
</div>

The relevant part is the onclick handler:

onclick={filter({
  value: Filters[key]
})}

filter just needs to set a flag in the state. It doesn't need the entire state. Therefore it's defined simply as:

const filter = ({ filter }) => ({ filter })

API details are still subject to change as I finish wrapping 2.0. So take this with a grain of salt! 🌊

Hi @jorgebucaran,
I was just about starting a new project utilizing the Hyperapp framework as our company standard. But now, when I read this thread, I'm getting a bit nervous. Do you have any timetable for 2.0? Would be better to start with all the benefits you described out of the box.
Anyway, thank you so much for running this awesome project!

Hey @etylsarin! Thank you for your interest. I'm releasing the 2.0 alpha on the first week of July! :)

726 🎉

TL;DR

I've created a series of issues to document the decision process behind the upcoming V2 API changes rendering this issue now obsolete.

Thanks to everyone who contributed their feedback. _If_ you stumbled upon this issue today, refer to the ones below to get the most up-to-date and accurate information, otherwise enjoy the discussion above!

   👉 V2 Branch #726
   👉 Actions #749
   👉 Effects #750
   👉 Subscriptions #752
   👉 Middleware #753
   👉 Lazy Lists #721

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamen picture jamen  ·  4Comments

jbrodriguez picture jbrodriguez  ·  4Comments

VictorWinberg picture VictorWinberg  ·  3Comments

dmitrykurmanov picture dmitrykurmanov  ·  4Comments

jscriptcoder picture jscriptcoder  ·  4Comments