jorgebucaran / hyperapp

1kB-ish JavaScript framework for building hypertext applications
MIT License
19.08k stars 780 forks source link

Introduce init and modules. #406

Closed jorgebucaran closed 7 years ago

jorgebucaran commented 7 years ago

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

By the way, modules are not like mixins as they would be scoped to a state & action slice, that you can't opt out from.

import { whopper } from "./whooper"
import { mouse } from "./mouse"

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!
  },
  state,
  actions,
  view,
  modules: { whopper, mouse }
})

The modules whopper and mouse of this example can definitely expose their own init function. They would be called after the top-level init with a slice of the state and actions, e.g., state["whopper"] and actions["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?

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, modules is only for (B), and we have HOA for (A).

/cc @lukejacksonn @zaceno @andyrj @Swizz

johanalkstal commented 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. :)

andyrj commented 7 years ago

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:

jorgebucaran commented 7 years ago

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

okwolf commented 7 years ago

@JorgeBucaran this is already doable with an HOA today, are you proposing adding support for this to core?

jorgebucaran commented 7 years ago

@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 }
})
okwolf commented 7 years ago

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

zaceno commented 7 years ago

Should modules be allowed to have modules in turn?

jorgebucaran commented 7 years ago

@zaceno Yes, modules should be recursive.

jorgebucaran commented 7 years ago

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?

Mytrill commented 7 years ago

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?

zaceno commented 7 years ago

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

jorgebucaran commented 7 years ago

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

zaceno commented 7 years ago

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)

jorgebucaran commented 7 years ago

@zaceno I'm super glad you are on board with this too. It was a struggle to figure this one out!

zaceno commented 7 years ago

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

VinSpee commented 7 years ago

Digging the compriomise here! IMO this will be great for the community ecosystem.

vdsabev commented 7 years ago

Init

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.

Modules

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.

Probably a crazy idea that wouldn't work at all

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:

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

jorgebucaran commented 7 years ago

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

okwolf commented 7 years ago

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

zaceno commented 7 years ago

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

jorgebucaran commented 7 years ago

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

zaceno commented 7 years ago

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.

augnustin commented 6 years ago

Is this available?

Can't find it on master branch. Neither a little doc alongside.

SkaterDad commented 6 years ago

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

augnustin commented 6 years ago

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:

SkaterDad commented 6 years ago

@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

zaceno commented 6 years ago

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

dmitry-kurmanov commented 6 years ago

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.

jorgebucaran commented 6 years ago

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.

dmitry-kurmanov commented 6 years ago

@jorgebucaran ok I understand.