Hyperapp: Need to dynamic actions in order to support code splitting

Created on 27 Oct 2017  Â·  35Comments  Â·  Source: jorgebucaran/hyperapp

435 showed its possible to write an HoA to encapsulate state+action+view into module and https://github.com/hyperapp/hyperapp/issues/435#issuecomment-340067619 showed its possible to load lazy said module, except for the module actions.

Motivation

Code splitting.

Problem

There is currently no way to add actions dynamically.

Possible Solutions

1. Include addAction action in app

  • solves the problem in the most direct way

    2. Include loadModule action in app

  • solves the bigger issue at the same time

    3. Merge the concepts of state and actions

  • state is expected to change, so what if we just put state and actions in the same place?

  • replace state and actions with root, keep view
  • allow root to hold both data and functions. functions would take in root and slice the tree as it does now
  • functions return and object which will merge into the subtree of where it lives, it can return both data and more functions (aka actions!)
  • since actions don't need to be there at init time, then modules don't need to exist either! modules can be put directly on the root or added during init or add lazily later

Option 3 is most interesting to me since it removes even more concepts from Hyperapp.

Discussion Feature

Most helpful comment

@Pyrolistical I have a contrived example with dynamic actions for the models branch with a few updates.

Here is the code:

const model = {
  count: 0,
  new: () => store => ({
    count: store.count - 1,
    mult: () => store => {
      return {
        count: store.count * 10
      }
    }
  }),
  up: () => store => ({ count: store.count + 1 })
}

const view = store => (
  <main>
    <h1>{store.count}</h1>
    <button onclick={store.up}>Increment</button>
    <button onclick={store.mult}>Multiply ({store.mult ? "Ready" : "Not Ready"})</button>
    <button onclick={store.new}>Add Dynamic Action</button>
  </main>
)

app(model, view)

gif

All 35 comments

@Pyrolistical This is currently impossible given we don't have stateful components. It could work in userland if you have different apps talking to each other, which is not unheard of, but not what you want probably.

I would prefer not having code splitting than implementing it in any of the proposed ways, sorry. But there's still hope, we just need to think more about it.

Dynamically loading views, when using e.g., a router, is feasible and I can see how this could be useful, given that views and components can make up a very large chunk of your app bundle too.

I don't understand your stateful components comment. What does stateful components have to do with this?

AFIK, in React you can dynamically load components and its child components with all its moving parts, and setup codesplitting for that. In Hyperapp you can't because there is only one app and a single entry point.

Honorable mention: Have you looked into HOAs?

@Pyrolistical How about this?

app(load => ({
  actions: {
    myAction() {
      import("/remote-module").then(load)
    }
  }
}))

???? isn't app(function) the old HoA style that has been removed?

we don't need stateful components for code splitting, we already have modules, state slice, its just dynamic actions that is the blocker.

and its an artificial blocker, there's no technical reason why actions need to be static. you could argue that apps are easier to reason about with static actions, but that's always true for static vs dynamic. simple vs power, which balance point do we want?

@Pyrolistical Hey, so what about this? It's just pseudo-code.

app(load => ({
  actions: {
    myAction() {
      import("/remote-module").then(load)
    }
  }
}))

yep, that would work

@Pyrolistical If this ever lands here, I think I would prefer something along those lines. It doesn't have to be an exact match, but close.

@Pyrolistical isn’t this the same issue as #431?
Would my suggestion from over there solve this too?

I think #431 and this issue have different motivation.

Yes, actually, #431 is about dynamically wiring actions to a sub-state of the slice, but not actually modifying the action tree. This is about adding a new module to the app dynamically. My bad.

Hmm. I may not be fully grasping the use case here, but if we're talking about code-splitting (so the async loading of modules/parts), then each dynamically loaded module would have to be quite independent (since it can't know for sure if any of the other modules are loaded or not).

My approach in such a scenario would probably be to have each module as it's own app({..}). One master app to manage the fetch/load/initializing of sub apps.

@zaceno my use case is to be able to defer loading of whole subsections of site, just not split up a single app into multiple loads.

@Pyrolistical, I see well that even more emphasizes that the different subsections/modules are quite independent, no?

Is there a reason you would want to have the subsections together in a single app({...})?

Interesting, that would require out of band communication to pass stuff between apps? That's pretty annoying.

Also, code splitting subsections of whole sites is just the MVP, where I really want to get to is being able to load tiny individual modules

It makes sense to me to be able to add dynamic actions especially for lazily instantiated modules, where you want to have actions scoped to each module. As opposed to a 'many' #435 or 'instance' #434 module with substates

Plus it seems like overkill to force nested apps to implement widgets..?

However, if that's the best way to go about it, how would you go about joining the apps together?

@snalld @Pyrolistical @zaceno We should work on a way to code split based on routes, instead of creating an intricate solution to a non-problem.

Plus it seems like overkill to force nested apps to implement widgets..?

@snalld @Pyrolistical -- perhaps you're right! It's just how I would solve it, given that hyperapp doesn't support dynamically adding actions (which seems to me a bit at odds with the declarative philosophy of hyperapp anyway)

However, if that's the best way to go about it, how would you go about joining the apps together?

Well... You could render each sub-app in a components oncreate lifecycle event.

Each app({...}) call returns the action-callers of the app, so you could wire up a parent app to control child apps through that.

You may use an external event bus to allow child->parent communication.

It's quite dated now (I wrote it for an older version of hyperapp) but perhaps https://github.com/zaceno/hyperapp-nestable could provide some ideas as well.

I realize this all feels like a "hack" on top of hyperapp. But maybe that's not so wrong. I don't know tbh.

@zaceno I suppose what feels wrong to me about that is that the child apps could load their own data which means the parent app doesn't have the only source of truth. On the other hand maybe that's ok (twitter feed widget?)

Each app({...}) call returns the action-callers of the app, so you could wire up a parent app to control child apps through that.

How do you suggest storing the action-callers in a parent app? I want to give this a try but I'm not sure how you'd go about it...

@snalld I suppose what feels wrong to me about that is that the child apps could load their own data which means the parent app doesn't have the only source of truth.

I don't recommend or encourage writing apps this way. You are of course, free to try, but it's out of the scope of what I set out to do with hyperapp and definitely in the upside-down. You've been warned. Welcome to the fringe zone.

@JorgeBucaran haha I'll tread carefully...

So just to clarify, what's your favourite way to tackle the widgets use case?

@snalld We return actions from the app() because otherwise there is no way to communicate with your app from the outside. The discussed scenario is usually a legacy system sending messages (calling actions) to hyperapp.

What's the widget use case again?

Being able to lazily instantiate a module that has its own actions (register actions dynamically) rather than have a module with substates and a general set of actions that

@JorgeBucaran it's very possible I'm not across the inner workings of HA to be getting involved in this particular discussion haha

@snalld Being able to lazily instantiate a module that has its own actions (register actions dynamically) rather than have a module with substates and a general set of actions that

In that case, using @zaceno's recommendation is one way to go about it right now. In the future something like this might be possible though.

Ah ok missed that bit

Tbh I think maybe I'm over complicating things with the whole dynamic modules thing, probably just need to rethink how to achieve what I want more declaratively

@snalld I thought we were just ideating. This is all future business. In reality, you can't possibly need any of this right now. Also, my comment here. 😄

https://github.com/hyperapp/hyperapp/issues/439#issuecomment-341001122 :

We should work on a way to code split based on routes, instead of creating an intricate solution to a non-problem.

We shouldn't do code-split on routes. It's not flexible. What if I want to make an Electron app with Hyperapp, it would limit the view tremendously. Or I have to make routes inside the electron stuff just to keep it working.

I'm really a fan of the Polymer way of routing. They just have a component where it switches components based on properties. And those properties can potentially come from a route, but also from state or an event or something.

What I'd like to see is an atomic component which has access to the full application state, but has its own actions and views, which can be dynamically loaded, based on the users' actions.

Higher order apps with access to the 'parents' state could have potential.

@Alber70g Thanks for the writeup. Could you show us some code or pseudo code of how you think the API would look like?

@Alber70g @Pyrolistical If #477 is implemented, actions can return new actions. Wouldn't that fix this?

yes it would! let's talk more in #477

@Pyrolistical I have a contrived example with dynamic actions for the models branch with a few updates.

Here is the code:

const model = {
  count: 0,
  new: () => store => ({
    count: store.count - 1,
    mult: () => store => {
      return {
        count: store.count * 10
      }
    }
  }),
  up: () => store => ({ count: store.count + 1 })
}

const view = store => (
  <main>
    <h1>{store.count}</h1>
    <button onclick={store.up}>Increment</button>
    <button onclick={store.mult}>Multiply ({store.mult ? "Ready" : "Not Ready"})</button>
    <button onclick={store.new}>Add Dynamic Action</button>
  </main>
)

app(model, view)

gif

Alas, we didn't get this since #490 was merged over #484.

The new combine concept is great for modularization of a project, but is still statically combined before the app is running.

@Pyrolistical Yes, this is a non-trivial problem to solve elegantly with the current architecture so I don't know if it will be possible. I'd be happy to help you out if you want to work on this, but I don't think I'll have much time to work on this exclusively myself since it does not affect me directly and there's so much other stuff to do.

I'll track this now over here → https://github.com/hyperapp/hyperapp/issues/533

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jorgebucaran picture jorgebucaran  Â·  3Comments

dmitrykurmanov picture dmitrykurmanov  Â·  3Comments

jamen picture jamen  Â·  4Comments

dwknippers picture dwknippers  Â·  3Comments

ghost picture ghost  Â·  3Comments