Hyperapp: V2 Middleware API

Created on 30 May 2018  ยท  33Comments  ยท  Source: jorgebucaran/hyperapp

A middleware API will allow you to easily create modules like a logger for Hyperapp 2.0.

Let's use this issue to discuss how middleware could look like in 2.0.

Please share your best ideas.

Discussion Outdated

Most helpful comment

How about a Redux-inspired middleware? Add a middleware option to the app that's conceptually similar to Redux.applyMiddleware:

import logger from "@hyperapp/logger"

app({
  // ...
  middleware: [logger]
  // ...
})

For the implementation of a middleware you could have something like:

// next would be the dispatch function here
const logger = next => action => {
  console.log('before state: ', next(state => state))
  console.log('action: ', action)
  const result = next(action)
  console.log('next state: ', result)
  return result
}

Each next would call the next middleware in the chain except for the last, which would call dispatch. Currying optional but recommended ๐Ÿ›

All 33 comments

How about a Redux-inspired middleware? Add a middleware option to the app that's conceptually similar to Redux.applyMiddleware:

import logger from "@hyperapp/logger"

app({
  // ...
  middleware: [logger]
  // ...
})

For the implementation of a middleware you could have something like:

// next would be the dispatch function here
const logger = next => action => {
  console.log('before state: ', next(state => state))
  console.log('action: ', action)
  const result = next(action)
  console.log('next state: ', result)
  return result
}

Each next would call the next middleware in the chain except for the last, which would call dispatch. Currying optional but recommended ๐Ÿ›

app({
    // Your configuration here
}).hook(logger, otherMiddleware);

Is internal render function also middleware?

@sergey-shpak No, it isn't. Middleware will let you "hook" into the internal dispatch mechanism, that's all for now.

A simpler alternative (from Hyperapp's implementation perspective) would be to write middleware as a higher-order dispatch function:

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

app({
  middleware: logger
})

Essentially your middleware is passed the original dispatch function and you return a function that will replace it and allow for for adding additional logic.

The main downside to this approach is that using multiple middlewares will require the use of a compose function that either we provide (like Redux) or is on the user to bring their own.

@okwolf What is console.log('before state: ', state => state)?

@jorgebucaran What is console.log('before state: ', state => state)?

A typo. ๐Ÿ˜’ See the correction above.

An advantage/disadvantage of the higher-order dispatch approach is that we could choose to unconditionally call the middleware instead of replacing the existing dispatch. The net effect is that middleware would be unable to prevent the original action from being dispatched but only dispatch additional actions. ๐Ÿค”

@okwolf If so it would be hard to implement things like shouldComponentUpdate().

@okwolf A crazy strategy could be to publish our own modified apps that support different functionalities. The main problem with that is combinability, like if there is a withApp1 and withApp2, how to use both?

@jorgebucaran A crazy strategy could be to publish our own modified apps that support different functionalities. The main problem with that is combinability, like if there is a withApp1 and withApp2, how to use both?

You mean like how Elm supports browser navigation by publishing their own program? I can't say I'm a particularly big fan of this approach. Publishing an entire new app as the API for extension seems a bit heavy-handed for most use cases. As you mentioned, they also don't compose unless we go back to the HOA approach again...

If middlewares are an High Order disptach why it is not done in userland ? I will be able to create my own dispatchAndLog that will use dispatch under the hood.

Or I am missing something ?

@okwolf You mean like how Elm supports browser navigation by publishing their own program? I can't say I'm a particularly big fan of this approach. Publishing an entire new app as the API for extension seems a bit heavy-handed for most use cases. As you mentioned, they also don't compose unless we go back to the HOA approach again...

I agree, that's one of elm's sharp edges.

If there's a middleware system built into hyperapp, I would prefer to just pass an array of middlewares to the app() call, which get executed in order, like in @okwolf 's post above.

I think the only real advantage to wrapping the entire app is you get full control over all arguments including init, view, subscriptions, etc. That is probably too much power and also a bit too magical IMHO. I'd rather our middleware/extension mechanism just wraps dispatch and everything else be explicit.

@jorgebucaran what's the planned middleware API at this point?

@okwolf I still don't know. ๐Ÿ˜ฅ

๐Ÿ™

At this point should we even try and include middleware in the first release, or add it later?

@okwolf I don't think so.

@jorgebucaran think of the devtools!

@okwolf Maybe something like this could work. What do you think?

import { app } from "hyperapp"

const actionMiddleware = action => action

app(actionMiddleware)({
  // The usual propspects.
})

e.g.

app(action => console.log(action.name))({
  // The usual props.
})

// or also

app(action => (state, data) => newState)({
  // The usual props.
})

@jorgebucaran would this example prevent the original action from being dispatched?

app(action => console.log(action.name))({
  // The usual props.
})

If not, would this run before or after the state was updated?

Wouldn't the real logger be more like this?

app(action => (state, data) => {
  console.log('before state: ', state)
  console.log('action: ', action)
  const result = action(state, data)
  console.log('next state: ', result)
  return result
})({
  // The usual props.
})

Depending on how we handle actions that aren't functions.

@okwolf The return value must be a function, so we could check if it's indeed a function and if it is not, return the original action, otherwise let you replace the action. _Or_, just force you to return the action.

And it would run before the action is called. Your proposed logger would only work for actions that return a new state, not those that return effects.

@okwolf Ah, that is unless we invoke the middleware callback for every state change with the name of the action instead. ๐Ÿ‘

But then why would you use middleware for other than logging?

We need to answer that question in order to move forward with this issue.

What about redux dev tools? Logging is not the most effective way to debug for some people.

Time travel is important. As well as seeing diffs, not just new state ๐Ÿ™

Polluting the console can happen if a lot of actions are triggered and you are looking for specific logs that you have in your code.

So in Vue the idea of plugins is widely used, would this be subscriptions for us or middleware?

Just curious ๐Ÿ˜„

I can see how this could open somewhat of a Pandora's box if we're not careful. ๐Ÿ“ฆ

Middleware could be used to add support for features that go against the philosophy of 2.0, like dispatching Promises. Or middleware could be used for integrating state merging logic, since that's not longer included. You could also use middleware in a larger app if you want actions that are dispatched to some event sink, perhaps even another app on the same page (possibly using a different library like Redux)! Those are a few of the non-devtools use cases I've seen folks use for middleware in the past.

I don't think there's really a huge practical difference between a higher-order action or dispatch, since with the former you could always write an action that some of the time returns the original action and other times returns an effect that gives you full access to dispatch anyway. ๐Ÿคทโ€โ™€๏ธ

Two other production use cases for middleware:

  • Error handling for actions/effects.
  • Automatically persisting state.

Technically you could use subscriptions for the latter, although it would be somewhat awkward to wire up and require resubscribing to a different sub each time for a potential performance hit.

We need more voices to weigh in on this topic!
@infinnie @frenzzy @Swizz @SkaterDad?

@okwolf Automatically persisting state.

That is the job of a subscription. I really didn't understand the downside you wrote below. ๐Ÿ™ˆ


I am going to close here and create a new issue (soon / when I'm ready) to discuss the middleware API again, from scratch. The scope of the issue was too wide and while the discussion was fruitful, I want to narrow it down to the specific _kind of_ middleware that I'd like to see in V2.

The kind of middleware that I want to see is the least invasive kind, the one that allows you to build a logger or a time travel debugger, but not modify how dispatch works in order to, among other things, add Promise support to actions, etc.

@jorgebucaran That is the job of a subscription. I really didn't understand the downside you wrote below.

Then either I don't understand the philosophy behind subscriptions or we should consider changing their API. It was my impression that the effect part of subscriptions was intended to be run once, and then it would be responsible for calling dispatch to "push" actions to your app. In order to use subs for this, you would have to implement your sub in a way that it returned a.) a new effect each time or b.) different other props of your sub. This is generally a bad practice for subs because you're going to be unsubscribing and resubscribing with every state update. And then I also thought the state argument to subscriptions was only intended for controlling which subs to enable, not for passing to them. To me using subs to run a side effect on state update seems like a hack.

@jorgebucaran The kind of middleware that I want to see is the least invasive kind, the one that allows you to build a logger or a time travel debugger, but not modify how dispatch works in order to, among other things, add Promise support to actions, etc.

Do you have a proposal yet for how to have the former without the latter? ๐Ÿค”

@okwolf The effect part of subscriptions is run once when a subscription is started. Subscription can be restarted, when their props change, this means canceling it first then starting it again.

I'll create a couple of new issues to explain effects and subscriptions like I did for actions in #749.

@jorgebucaran The effect part of subscriptions is run once when a subscription is started. Subscription can be restarted, when their props change, this means canceling it first then starting it again.

I understand how subscriptions work, as I've written a few already for testing in @hyperapp/fx. What I'm getting at though is that forcing a sub to be canceled and then a new one to be subscribed with every state update feels like an abuse of subscriptions but I could be wrong. ๐Ÿคทโ€โ™‚๏ธ

I don't understand what you mean by abuse. One of the strengths of a declarative subscription system is that subscriptions can be turned on and off as a function of the state, much in the same way you can create and destroy DOM elements using a VDOM.

@okwolf Feel free to bring the subs discussion over to #752 if you have anything further to add or want to start from scratch.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbrodriguez picture jbrodriguez  ยท  4Comments

Mytrill picture Mytrill  ยท  4Comments

jamen picture jamen  ยท  4Comments

VictorWinberg picture VictorWinberg  ยท  3Comments

dmitrykurmanov picture dmitrykurmanov  ยท  4Comments