Closed jorgebucaran closed 7 years ago
An issue with modules
for me is, it basically means you could do app({ modules: {} })
and that's it? It kind of removes the point of having app({ state, actions, init })
because that's what modules gives you as well.
I mentioned it on Slack, that why add a framework API that gives me a way to do what I'd already be doing with init, state, actions
?
const state = Object.assign({}, whopperState, mouseState)
const actions = Object.assign({}, whopperActions, mouseActions)
If modules
does that for me, then just app(modules)
.
I'm confused by the proposal. :)
I like that modules gives you a clear path to convert from the old mixins structure. It makes it clean without the implicit dependency problems of mixins. And this has the init for escape hatch out of the fragmented state/actions when you need access to globals in a init.
:100:
@johanalkstal
In the case of:
app({ modules: { whopper } })
...its props would be scoped into "whopper", so it's effectively a black bock that can't talk to the outside. It's largely a convenience though.
@JorgeBucaran this is already doable with an HOA today, are you proposing adding support for this to core?
@okwolf Yes. The rationale is that while we could certainly shrug off modules, we can't ignore the need for init.
Here's the same without modules.
import { whopper } from "./whooper"
import { mouse } from "./mouse"
import { bandersnatch } from "./bandersnatch"
import state from "./myState"
import actions from "./myActions"
import view from "./myView"
const actions = app({
view,
state: {
...state,
whopper: whopper.state,
mouse: mouse.state,
bandersnatch: bandersnatch.state
},
actions: {
...actions,
whopper: whopper.actions,
mouse: mouse.actions,
bandersnatch: bandersnatch.actions
}
})
// Subscribe to global events, start timers, fetch resources & more!
actions.init()
whopper.init(actions)
mouse.init(actions)
bandersnatch.init(actions)
I don't think the above is a very good user experience and since it will be a common thing to do, we might as well give users a built-in way to do this so they don't have to reach for a HOA.
With init and modules, the above is roughly equivalent to the following.
import { whopper } from "./whooper"
import { mouse } from "./mouse"
import { bandersnatch } from "./bandersnatch"
import state from "./myState"
import actions from "./myActions"
import view from "./myView"
app({
init(state, actions) {
// Subscribe to global events, start timers, fetch resources & more!
},
view,
state,
actions,
modules: { whopper, mouse, bandersnatch }
})
@JorgeBucaran I'm on board with init
, although I'm tempted to say let's let the community try out using HOAs for modules
first and see what should make it back to core before speculating too much. Would init
be allowed to set the state like its Elm cousin, or would you be required to call actions
to do that?
Should modules be allowed to have modules in turn?
@zaceno Yes, modules should be recursive.
Thanks. @okwolf
I'll think about it and great question. I don't know. I think you can call an action for that, but what do you think?
There were some discussions about allowing state to return a function (actions) => state. If init() is allowed to set state, the thunk approach may be simpler/more consistent with other parts of app(). What do you think?
@JorgeBucaran @okwolf One of the uses of HOA as I saw it was to allow preprocessing/augmentation of initial state. It makes sense if init can do that also/instead, but I don't like the idea of calling actions before the app is done booting up. I'd like to see clean separation of setting up initial state and actual execution of the app (I e action-render-loop)
@zaceno One of the uses of HOA as I saw it was to allow preprocessing/augmentation of initial state.
Yes, but I would rather see HOAs being used for more meta things like debugging tools and maybe things like #306.
...I don't like the idea of calling actions before the app is done booting up.
Init happens after the app is done booting up. It has the same purpose as the former events.load
, so you will and want to call actions inside init!
I'd like to see clean separation of setting up initial state and actual execution of the app (I e action-render-loop)
I think it's already clearly separated. You just put your state in the state
prop. Now, modules offer an extra convenience for setting the state & actions and offer you encapsulation without introducing issues mixins had.
@okwolf If init() is allowed to set state, the thunk approach may be simpler/more consistent with other parts of app(). What do you think?
We could think of init as an action and one of the proposals was to nominate a special "init" action instead. It felt more like a special case and something in the back of my head warned me against it, so it didn't make it to Hollywood.
In the end we went with a shiny new app({ ... })
prop instead. For this reason, perhaps we should not think of init as an action.
So, what's init? It's a function where you can "kick off" your app if you need to call actions after the app() call, subscribe to global events, set timers, etc.
My previous comment was mostly against the idea of using actions inside init to set up initial state if that's what you need (because it introduces two different types of actions). Of course init will need access to the final actions object (otherwise how to set up mousemove event handlers and similar).
So it was nothing against this proposal. I am for this proposal :)
I'm especially a fan because it takes hyperapp one step further along the lines of hyperapp-partial. When this proposal makes it in I can decommission partial and just make two separate HOA (one to add an event bus, and one for prewired components in modules)
@zaceno I'm super glad you are on board with this too. It was a struggle to figure this one out!
@JorgeBucaran How close do you feel this is to making it in? I mean are you uncertain about it and want to leave it up for discussion a while, or are you busy implementing it as we speak?
(Just asking because I'm about to publish my 0.14 compatible version of hyperapp partial -- just doing the hard part writing docs. But I might as well skip that if this is coming soon)
Digging the compriomise here! IMO this will be great for the community ecosystem.
To me it sounds intuitive to be a top-level function instead of nested in actions
, but I don't really need it yet. EDIT: Yes, as was discussed in the comments so far.
While I was working on my portfolio website yesterday I had the thought of writing a HOA allowing a components
property because of this use case:
import { app } from 'hyperapp';
import { Contact } from './Contact';
app({
state: {
contact: Contact.state
},
actions: {
contact: Contact.actions
},
view: (state, actions) =>
<div>
...
<Contact.view state={state.contact} actions={actions.contact} />
...
</div>
});
And here's what Contact.jsx
basically looks like:
export const Contact = {
state: ...,
actions: ...,
view: ({ state, actions }) => ... // Passed from the root level app
};
With the HOA, I wanted to do something like:
app({
components: {
contact: Contact
},
view: (state, actions) =>
<div>
...
<Contact.view state={state.contact} actions={actions.contact} />
...
</div>
});
Notice I had to use Contact.view
instead of Contact
, as well as pass the state
and actions
properties, which forces me to write a different signature for Contact's view
function.
I understand modules
is more about enabling things like the router, but ideally, what I would love to see is the following:
import { app } from 'hyperapp';
import { Contact } from './Contact';
const App = {
init() {
// Runs when the component is first rendered
},
state: ...,
actions: ...,
view: (state, actions) =>
<div>
...
<Contact />
...
</div>
};
export const Actions = app(document.body, App);
And Contact.jsx
:
export const Contact = {
init() {
// Runs when the component is first rendered
},
state: ...,
actions: ...,
view: (state, actions) => ...
};
A few notes:
Contact
instead of Contact.view
in JSXstate
and actions
properties from App.view
to the Contact
componentContact
has the same signature as App
: (state, actions)
(instead of ({ state, actions })
)Any thoughts?
(I also explored this train of thought in my Medium article (which I should update to reflect the awesome changes in 0.14) Exploring Unidirectional Components in Mithril (part 1 — Hyperapp))
@vdsabev To me it sounds intuitive to be a top-level function instead of nested in actions, but I don't really need it yet.
It's going to be a top-level function just as you said. 🎉 😉
About modules VS HOAs, from the Slack — @zaceno's words:
This actually gets at the core of the problem I saw with mixins (and why I wanted them renamed from
plugins
as they once were called).: they basically served two purposes: either to A) augment hyperapp’s features, or B) to modularize your app. Now, current modules is only for B, and we have HOA for A.
@JorgeBucaran well if we decide that init
is allowed to set the initial state we could drop the state
prop since they would be duplicative, correct?
@vdsabev
No explicit passing of state and actions properties from App.view to the Contact component The view function of Contact has the same signature as App : (state, actions) (instead of ({ state, actions }))
Sounds exactly like "partial views" from https://github.com/zaceno/hyperapp-partial#partial-views
I've used this pattern a lot and come to like it. There are plenty of folks in the Hyperapp community who would advise against it because it is less explicit. On the other hand (imo) it makes the overall structure stand out more. It's a tradeoff.
FWIW the version of hyperapp-partial currently published is only compatible with hyperapp 0.12.1. I'll soon be publishing a new version for 0.14.0, or (when this makes it in) decomission partial and release a HOA that specifically adds the "partial views" (a k a "prewired components", a k a "view components", a k a "widgets").
@okwolf ...well if we decide that init is allowed to set the initial state we could drop the state prop since they would be duplicative, correct?
That would be correct, but it's not the case. init
can't set the state like actions, but it can by calling actions.
In case anyone wants to play with modules before they're released officially (I wanted to start on my prewired comopnents and event bus HOAs), here's a HOA that implements modules. It's tested, but not very thoroughly.
function modules (app) {
function noop () {}
function scopeInit (scope, fn) {
return function (state, actions) {
fn(state[scope], actions[scope])
}
}
function collectModules(opts) {
opts.state = opts.state || {}
opts.actions = opts.actions || {}
opts.modules = opts.modules || {}
var inits = [].concat(opts.init || [])
for (var name in opts.modules) {
var modOpts = collectModules(opts.modules[name])
opts.actions[name] = modOpts.actions || {}
opts.state[name] = modOpts.state || {}
inits.push(scopeInit(name, modOpts.init || noop))
}
opts.init = function (state, actions) {
inits.forEach(function (i) {
i(state, actions)
})
}
return opts
}
return function (opts) {
opts = collectModules(opts)
var actions = app(opts)
opts.init(opts.state, actions)
return actions
}
}
Another reason for sharing it is I want to make sure I have the right idea of how modules + init will work.
Is this available?
Can't find it on master branch. Neither a little doc alongside.
@augnustin this issue talks about features that were available several months ago, but removed before Hyperapp hit 1.0.
The docs are up to date as far as I know.
You'll find a lot of experimentation if you dig through old issues and PRs, so keep that in mind.
Hello @SkaterDad
Thanks for quick reply. I thought this was not yet released but it is already gone! :smile:
I was looking for a way to load remote data on initial load. I went for:
const view = (state, actions) => (
<div class='container' oncreate={e => actions.syncState()} >
</div>
);
would that be the correct way? It feels like it is not the right place to do so and a init
option would have felt more confortable but at least it works. :smile:
@augnustin There were a lot of API changes last year! It was hard to keep up with at times, but fun to follow along.
When you call the app()
function, it returns your actions, so you can call one on the next line.
For example:
const myActions = app(initialState, actions, document.getElementById('app'))
myActions.syncState()
Here's the relevant part of the README: https://github.com/hyperapp/hyperapp#interoperability
@augnustin I'd go with what @SkaterDad said, because it keeps state management logic out of the view.
In case you're interested about modules and initialization with current versions of hyperapp, I've written a bit about it. Nothing official - just my approach, but in case you're interested:
https://zaceno.github.io/hypercraft/post/modular-apps/ https://zaceno.github.io/hypercraft/post/initialization/ https://zaceno.github.io/hypercraft/post/cross-namespace-action-calling/
Hello team! Thanks for your work! I think that idea of framework with human readable :) code size is great! What about an official way to create modules with hyperapp? Why do you abandon this functionality? May be I've missed something. Could someone please give an example of complex app based on hyperapp?
P.S. @zaceno 's way in the "hypercraft" looks ok but why not out of the box? Thank you.
Hi @dmitrykurmanov. This issue is super outdated and will only confuse you — unless you are interested in Hyperapp history! 😄
You can totally create modules with Hyperapp, then wire the state and actions into your app main state and actions manually.
@jorgebucaran ok I understand.
We don't have an elegant way to call any actions, subscribe to global events or kickstart ourselves when our app loads. We can call actions after we return from
app()
but packages like the router, a mouse / keyboard interop interface, etc., could be exposed as modules and for their initialization needs, we can introduce a new init function. 🎉The modules
whopper
andmouse
of this example can definitely expose their owninit
function. They would be called after the top-levelinit
with a slice of the state and actions, e.g.,state["whopper"]
andactions["whopper"]
, just how it works with actions.In @Zaceno's words, what's the difference between modules and mixins and how do we explain their overlap with HOAs?
/cc @lukejacksonn @zaceno @andyrj @Swizz