Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 Middleware API #703

Closed
jorgebucaran opened this issue May 30, 2018 · 33 comments
Closed

V2 Middleware API #703

jorgebucaran opened this issue May 30, 2018 · 33 comments
Labels

Comments

@jorgebucaran
Copy link
Owner

jorgebucaran commented May 30, 2018

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.

@jorgebucaran jorgebucaran added discussion meta News and communication labels May 30, 2018
@jorgebucaran jorgebucaran mentioned this issue May 30, 2018
10 tasks
@okwolf
Copy link
Contributor

okwolf commented May 30, 2018

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 🍛

@infinnie
Copy link
Contributor

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

@sergey-shpak
Copy link
Contributor

Is internal render function also middleware?

@jorgebucaran
Copy link
Owner Author

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

@okwolf
Copy link
Contributor

okwolf commented Jun 5, 2018

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.

@jorgebucaran
Copy link
Owner Author

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

@okwolf
Copy link
Contributor

okwolf commented Jun 5, 2018

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

A typo. 😒 See the correction above.

@okwolf
Copy link
Contributor

okwolf commented Jun 5, 2018

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. 🤔

@infinnie
Copy link
Contributor

infinnie commented Jun 5, 2018

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

@jorgebucaran
Copy link
Owner Author

@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?

@okwolf
Copy link
Contributor

okwolf commented Jun 5, 2018

@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...

@Swizz
Copy link
Contributor

Swizz commented Jun 5, 2018

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 ?

@SkaterDad
Copy link
Contributor

@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.

@okwolf
Copy link
Contributor

okwolf commented Jun 7, 2018

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.

@okwolf
Copy link
Contributor

okwolf commented Jun 30, 2018

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

@jorgebucaran
Copy link
Owner Author

@okwolf I still don't know. 😥

@okwolf
Copy link
Contributor

okwolf commented Jul 11, 2018

🙏

@okwolf
Copy link
Contributor

okwolf commented Aug 5, 2018

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

@jorgebucaran
Copy link
Owner Author

@okwolf I don't think so.

@okwolf
Copy link
Contributor

okwolf commented Aug 14, 2018

@jorgebucaran think of the devtools!

@jorgebucaran
Copy link
Owner Author

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

@okwolf
Copy link
Contributor

okwolf commented Aug 14, 2018

@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.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 14, 2018

@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.

@jorgebucaran
Copy link
Owner Author

@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.

@selfup
Copy link
Contributor

selfup commented Aug 18, 2018

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 😄

@okwolf
Copy link
Contributor

okwolf commented Aug 19, 2018

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. 🤷‍♀️

@jorgebucaran jorgebucaran changed the title Hyperapp 2.0 Middleware API V2 Middleware API Aug 27, 2018
@okwolf
Copy link
Contributor

okwolf commented Aug 27, 2018

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?

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 27, 2018

@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.

@okwolf
Copy link
Contributor

okwolf commented Aug 27, 2018

@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? 🤔

@jorgebucaran
Copy link
Owner Author

@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 jorgebucaran added the outdated Forgotten lore label Aug 27, 2018
@okwolf
Copy link
Contributor

okwolf commented Aug 28, 2018

@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. 🤷‍♂️

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 28, 2018

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.

@jorgebucaran jorgebucaran removed the meta News and communication label Aug 31, 2018
@jorgebucaran
Copy link
Owner Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants
@jorgebucaran @Swizz @okwolf @sergey-shpak @infinnie @selfup @SkaterDad and others