Should we keep built-in support for HOAs (higher order apps) or shall we encourage you to roll your own DIY solution? For example using a right to left function reducer like compose or good old f(g(..)).
The tradeoff is more flexibility & less bytes for one less selling point.
Some facts about HOAs:
/cc @pspeter3 @okwolf @zaceno
@JorgeBucaran would you be opposed to publishing an official @hyperapp/compose so less wheel reinventing would occur?
Probably, I'll keep that in mind. Let's see what everyone else thinks and where this issues leads us.
So, this issue was brought up from this discussion.
After realizing that in order to use multiple HOAs each HOA needs to be coded with care as to not break the middleware chain*, @pspeter3 commented:
I feel like the HOA abstraction is great but the API feels really awkward when you need to consider all the other higher order apps.
...and...
That's exactly what I'm thinking. It seems even less necessary now that there are modules.
... to not break the middleware chain involves returning (from your higher order app function) in a similar way to how app.js itself handles HOAs:
const myHOA = app => props => typeof props === "function"
? props(app)
: enhanceTheApp(props)
Question: does HOAs need access to the app()'s returned actions/modify app()'s return type?
If not, then we could instead support HOP/props enhancer: (appProps) => enhance(appProps). This would be easily composable and we may want to keep support for that in core?
@Mytrill They may need to access the actions returned by app() as well as enhance props and create a closure. Can you show an example of what you are saying?
The main proposal were to compose HOA in a curried way, is this really prerequisite to this feature ?
I mean we are not stuck at the use of this curried way app(HOA1)(HOA2)(HOA3)({ ... }).
Maybe it would be good to compose from an array of HOA app([HOA1, HOA2, HOA3])({ ... }).
HOA will still need to be aware of the returned value of the previous HOA.
@Swizz Reminds me of mixins! 馃槈
I like @Swizz's idea of an array, it makes it easier to dynamically add HOAs (e.g. depending on the environment)
Here is something that could work if we want to support it in core and access to app()'s result:
// general pattern
const doSomethingWithAppResult = actions => actions
const doSomethingWithProps = props => props
const AppEnhancer = next => props => {
// returns the enhanced app
return doSomethingWithAppResult(
// calls the next HOA/app enhancer
next(
// enhance the props
doSomethingWithProps(props)
)
)
}
// example hoas
const hoa1 = next => props => {
console.log('HOA 1 received props: ' + JSON.stringify(props))
const result = next({ ...props, hoa1: true })
console.log('HOA 1 returns app: ' + JSON.stringify(result))
return result
}
const hoa2 = next => props => {
console.log('HOA 2 received props: ' + JSON.stringify(props))
const result = next({ ...props, hoa2: true })
console.log('HOA 2 returns app: ' + JSON.stringify(result))
return result
}
// implementation of app()
function app(appProps) {
if (Array.isArray(appProps)) {
return appProps.reduce(function(prev, curr) {
return curr(prev)
}, app)
}
// rest of app()'s implementation...
// just wrap the props for this example
return { props: appProps }
}
console.log('Final app: ' + JSON.stringify(app([hoa1, hoa2])({})))
The snippet above logs:
HOA 2 received props: {}
HOA 1 received props: {"hoa2":true}
HOA 1 returns app: {"props":{"hoa2":true,"hoa1":true}}
HOA 2 returns app: {"props":{"hoa2":true,"hoa1":true}}
Final app: {"props":{"hoa2":true,"hoa1":true}}
We could also reverse the array/use reduceRight so they are called in the intuitive order...
@JorgeBucaran You meant ?
app({
ehancers: [HOA1, HOA2, HO3]
})
Looking at a basic example of what a HOA actually is supposed to do,
function hoa(app) {
return props => {
return app(enhance(props))
function enhance(props) {
// Enhance your props here.
}
}
}
We're not doing anything with app, we are manipulating the props we pass in.
It feels quite logical to do that _before_ calling app, so that your props are all good and ready by then.
Basically, one or many function (props) functions are what people really need.
And manipulating an object is like the simplest thing you can do.
So I feel that the need for app in all of this is a cause for initial confusion more than anything,
and forces the developer to have to consider something they shouldn't have to,
const myHOA = app => props => typeof props === "function"
? props(app)
: enhanceTheApp(props)
Documenting that to enhance the application, all you need is a function (props) seems like the easiest thing to do both for Hyperapp but also users of Hyperapp?
Now with init and modules landed in core and with the recent realization that the built-in support for authoring a HOA is not particularly that helpful, I propose we remove this from core and encourage users to DIY! 馃敟
@johanalkstal in this case, you are removing the ability for HOAs to enhance the value returned by app()
@Mytrill I was just using the examples provided by @JorgeBucaran to explain what HOAs are.
Are those examples incorrect?
Also, why would you want to manipulate the returned actions from app? I can't see why?
Or did I missunderstand you?
Modifying an object before passing it to a function (in this case, app) is a very common thing to do (at least for me, but I would think for most people). It's so common, in fact, that I arrived at the same solution as modules, but called it components instead. So perhaps we won't need HOAs in the next release.
@Mytrill All I am saying is let's compose HOAs outside core, not veto the bill 馃槈
Instead of :app(logger)({ ... })
logger(app)({ ... })
I agree that we take out the support for passing a HOA into app, and let people compose their enhanced apps however is appropriate for their use case. Because use cases range from simple (just using one HOA or none always) to complex (multiple HOA, some must be switched on or off depending on NODE_ENV, etc)
And as long as HOA can just be regular functions with no special tricks built in, everyone will know how to use them and have their own preferred way of dealing with them.
Besides, I think the most common scenario is zero HOA anyway, as @JorgeBucaran pointed out.
@zaceno I rewrote the HOA tests and didn't need to touch the implementation of the HOA functions (as you'd expect) and the difference is only in how you apply it to app. No need for compose and the result is more functional buddy. Having said that, a utility function to help compose multiple HOAs in particular ways as you described could be handy.
B(A(app))({
state: {
value: 0
},
init(state) {
expect(state).toEqual({
value: 2
})
done()
}
})
Ah ok, got it, thanks!
I'm all for removing the support then!
I'm in favor as well. I think we can have docs like React does.
Changed my mind too. The following A(B(app))({ ... }) is better for users, community developers and remove complexity in core.
Most helpful comment
Now with init and modules landed in core and with the recent realization that the built-in support for authoring a HOA is not particularly that helpful, I propose we remove this from core and encourage users to DIY! 馃敟