There is currently no way to add actions dynamically.
Since actions operate on state we probably also need to think about how to dynamically add state.
Could we not just return a reference of the state together with the actions from app and reinitialize the app when new actions and state are loaded? We could also mark returned actions as already initialised and then skip them during reinitialization.
const initState = { /* my state */ }
const initActions = { /* my actions */ }
let { state, actions } = app(initState, initActions, /* view */, /* container */)
// each action has the property action.initialized = true
// later load new actions and state
load('./mymodule.js').then(({newState, newActions}) => {
{ state, actions } = app({...state, ...newState}, {...actions, ...newActions}, /* view */, /* container */)
});
@timjacobi Running app twice?
@JorgeBucaran yeah multiple app calls - or at least multiple init calls makes sense to me. It's along the lines of "multiple stores" I was thinking about before. If you're initing new actions, it's always going to be for new initial state. So there won't be any crossover possible wether or not you splice the new state on to the original state tree or just keep it in a different one.
The only problem, of course, is how to do full state persistence/time travel.
But I think the dynamically initialized actions use cases rule out persistent/time traveling state anyway.
I think this is a very important question not just for code splitting but for importing reusable "components".
I agree that we should consider dynamic state slice injection as @timjacobi has mentioned.
Third party "component"
const view = (s, a) => <p>...</p>;
const state = {
customCompSliceNamespace: {
...
},
};
const actions = {
customCompSliceNamespace: {
...
},
};
export {
view,
model: {
state,
actions,
},
};
Then when importing it (we do some magic???)
import { model: customCompModel } from 'customComp';
app(
...,
...,
...,
document.body,
[customCompModel, ..., ...],
);
However, this is starting to feel like mixins again (sort of) and technically we can just have users export this process to userland
import { app } from 'hyperapp';
import { shareAble } from 'hyperapp-shareable';
import { model: customCompModel } from 'customComp';
import model from './model';
const { state, actions } = shareAble([
model,
customComp,
]);
app(
state,
actions,
...,
document.body,
);
To me, the best use case for code splitting would be for routing and this issue could facilitate this process if we come to a solution π
@selfup have a look at hyperapp-nestable if you haven't already. Just at the source I mean. It's basically the "magic" you're talking about, which works by calling app inside lifecycle methods.
Now wether that's good enough I don't know, but it's fully possible to make shareable components today using that method.
If multiple apps is going to be the answer to reusable components, code splitting etc c, we should start exploring what (if anything) is missing to fully use that approach.
@JorgeBucaran yes I deliberately suggested multiple app calls, in additional app call is basically a refresh based on new state and actions
@timjacobi So, how do you make multiple apps?
@JorgeBucaran An app is specific to a root element, calling app on the same root element again with new actions and state will reinitialise the app. Calling app with different state and actions on another root element will create a new app on that element. The former is kinda similar to hydration, we have a state and the DOM for it and reinitialising the app on that DOM.
@timjacobi Now, I see what you are saying. I think this is the best idea we have so far. π
@zaceno The only problem, of course, is how to do full state persistence/time travel.
What would this be a problem? π€
When we say dynamic import of actions, many way are possible, it only depends
what we are awaiting/requesting for.
I will only focus on surface API, core specifics stuffs will only be superificaly
aborded.
The Counter example will be namespaced to help me to promote my thoughts.
We have already discussed about this API
late in the past, with this one we agree to the statement to do not discriminate
actions front for state. Actions become updatable as the same ease as we did with
the state. By this way, an action is now able to add another action.
import { h, app } from "hyperapp"
const model = {
counter: {
count: 0,
down: () => model => ({ count: state.count - 1 }),
up: () => model => ({ count: state.count + 1 })
}
}
export const main = app(model, () => {}, document.body)
Actions/namespaces/state are loaded asynchronously, and
made available by the actions dispatcher once loaded. This one request, to add
promise support in core.
Only actions import is shawn, but lazy load of state is also possible using this way.
import { h, app } from "hyperapp"
const state = {
counter: {
count: 0
}
}
const actions = {
counter: import("./app/counter/actions")
}
export const main = app(state, actions, () => {}, document.body)
Each inited app provide a method allowing to add
new actions/state from the outside at runtime enhance(state, actions).
This one allow more or less routed import for codesplitting but from the outside
of the app.
import { h, app } from "hyperapp"
const state = {
counter: {
count: 0
}
}
const actions = {
counter: {}
}
export const main = app(state, actions, () => {}, document.body)
// actions dispatcher is now available through main.actions
import("./app/counter/actions").then(actions => {
main.enhance({}, { counter: actions })
})
This one works like the last one, but it directly provided
by the app inside an action. In other words the enhancement is done by the app itself
and not by the outside.
import { h, app } from "hyperapp"
const state = {
counter: {
count: 0
}
}
const actions = {
counter: {
load: () => () => enhance => {
import("./app/counter/actions").then(actions =>
enhance({}, { counter: actions })
)
}
}
}
export const main = app(state, actions, () => {}, document.body)
This is only some naive examples, but the goal is only to provide real thoughts
about how the API could looks like.
I kind of like the simple non breaking approach of just brute forcing hydration. Now yes, we loose time travel but this is known since we are creating an entirely new object reference.
_immutable hyperapps_
Does our current hydrate merge or just replace? Because if it merges we can essentially preserve time travel as well right?
However if we make an API call on init how do we avoid reloading the entire meat and bones of the current view. Looks like we would run into some state problems unless people are told to pass current state to the new app.
addSomeGiantComp: () => (state, actions) => {
/* some dynamic import here */
app(
Object.assign({}, state, newState),
Object.assign({}, actions, newActions),
view,
root,
);
},
Now I am sure that will work fine for ensuring that all current state is maintained, but now we have two "problems".
const { up } = app(...);
The only way to update these action references would be to advise to do:let { up } = app(...); on the first initialization and manually updates references
OR
Tell users to never destructure read only vars from app and to just have the entire object reference and do a (_gasp_) mutating merge on said actions from the new ones:
replaceAllExposedRefs(window.exposedActions, app(...));
So exluding time travel the recommended way (_without changing any of our own source code/API for hyperapp_):
addSomeGiantComp: () => (state, actions) => {
/* some dynamic import here */
// update global reference to exposed non destructured actions
// Object.assign might not be enough
// we might need to make a function for users to import in this case
// since actions are flat, we can safely rebind :tada:
Object.assign(window.exposedActions, app(
Object.assign({}, state, newState),
Object.assign({}, actions, newActions),
view,
root,
));
},
Will that work? Or only update references to the NEW actions? I'll try later when I have some time haha
My use case for code splitting is to be able to "stream" an app. Imagine a huge app like medium.com, I want to be able to do the first meaningful paint really quickly. I want to be able to load a tiny initial app, and load everything else piece by piece.
The user would see the skeleton of medium.com say, with the title only, then the articles would load be partially rendered, then the remaining UI bits are loaded after. I want to be able to control the full UX of what part of the site loads when.
Right now websites are pretty much big bang. Here load this giant bundle.js and wait 3 seconds, or better yet, here load this static dom, then this bundle.js to hydrate, then wait 3 seconds for interactive.
I want to be even faster than all of that. I see hyperapp being able to get me there since its so tiny. React's runtime is already too big for my use case.
Dynamic actions is one way to make this happen. With dynamic actions (and views) I can then load modules on demand.
What about route-based code splitting? I think this is what Elm plans to do for 0.19.
My only comment about code-splitting is that if it's a thing I wouldn't want it to be dependent on routes. I would want to be in control of how actions get added myself.
_without consideration to updating exposed actions if any_
https://codepen.io/selfup/pen/ZvJomR?editors=1010
I will work on how to update exposed actions either using Object.assign or just a simple function that updates all KVs to force the actions to references the new hyperapp
_inspired by @timjacobi π_
// cannot use const as we have to change the refs here eventually
let { up, down } = app(...);
load('./myModule.js')
.then(({ newState, newActions }) => {
{ up, down } = app(
Object.assign({}, state, newState),
Object.assign({}, actions, newActions),
view,
root,
);
});
First we need a simple function:
function updateExposedActionRefs(oldExposed, newExposed) {
Object.keys(newExposed)
.forEach((key) => {
oldExposed[key] = newExposed[key];
});
};
Then we need the first exposed actions to be globally bound somewhere:
window.exposedHyperappActions = app(...);
Now we can update the references in the old object to the newly exposed hyperapp actions:
addComp: () => (state, actions) => {
// PRETEND THIS IS DYNAMICALLY IMPORTED
const newState = {
count: 42,
};
const newActions = {
up: () => state => ({ count: state.count + 1 }),
};
// BRUTE FORCE HYDRATION
const newlyExposedAppActions = app(
Object.assign({}, state, newState),
Object.assign({}, actions, newActions),
view,
document.body,
);
// UPDATE OLD REFS TO NEW REFS
updateExposedActionRefs(window.exposedHyperappActions, newlyExposedAppActions);
},
This enables people incrementing Hyperapp into other framework/vanilla code, or to the devs that prefer having multiple Hyperapps, to be able to brute force hydrate and keep communication between apps/js untouched! :tada:
@timjacobi @selfup whoa this is a really clever approach I haven't considered before. Have a feeling there are plenty of neat ways to exploit this idea of "rehydrating actions"
@JorgeBucaran never mind I was talking about a different thing I realized now. I was thinking it was like literally having separate state-management being rendered together in the same tree. Which also is an idea that could be explored further. Just for fun ;)
@zaceno Yea as soon as I read his idea of hydrating the entire app it clicked and made complete sense to me haha.
Thank you @timjacobi for bringing this up. I really like this idea π
The one thing I currently enjoy about using Object.assign As Seen Here for merging old actions/sate with new ones is that _we do not have to change what's currently in core at all_ π
@selfup your approach will cause multiple re-renders for each action because you actually start the app twice, for example modify this line in your pen:
const view = (state, actions) => (console.log('render'),
Any idea how to solve it?
Oh interesting. I knew something was too good to be true. I'll mess around some more tomorrow. Thank you so much for bringing this up :pray:
Couldn't we just insert some prop in the container and use that during setup to properly "hydrate" the app?
container.cantTouchThis = true
That is not a bad idea at all. Do you have an example? I am curious π
@selfup I have nothing, nothing, nothing. π΅ Nothing yet.
I think we are ok @JorgeBucaran @frenzzy https://codepen.io/selfup/pen/ZvJomR?editors=1011
I added two extra actions, and it only console logs "render" once π per hydration cycle π
I thought you meant that it will render the new amount of actions. Unfortunately calling app(...) again will force a re-render. Which is why I tried calling it a _brute force hydration_
Since I changed the new injected state to 42 (for the count) and it updates it to 42, that is the expected behavior. Not everyone will want this, but this is a scenario that can cover 90% of the use case without changing the API.
Please check the comment below
Looks like we need to wipe old references somehow. Thanks for bringing it up @frenzzy
We are getting unexpected behavior when calling - since it resets the state to the previous apps state π€
One more comment below :pray:
Ok so it's not quite pretty but it works:
addComp: () => (oldState, oldActions) => {
// PRETEND THIS IS DYNAMICALLY IMPORTED
const newState = {
count: 42,
};
const newActions = {
up: () => state => ({ count: state.count + 1 }),
hello: () => () => {},
wow: () => state => ({ state }),
};
// BRUTE FORCE HYDRATION
app(
Object.assign({}, Object.assign({}, oldState, state), newState),
Object.assign({}, Object.assign({}, oldActions, actions), newActions),
view,
document.body,
);
},
We have to force a de-ref of the old state π
At this point it would be best to provide this as a (third party/not in core) function that can be provided to those that run into this issue?
Something like: import hafh from 'hyperapp-frontend-hydration';
and then: hafh(app, view, root, oldState, oldActions, state, actions, newState, newActions);
It's not _the best API_ but it works without us changing anything and it solves a rare case
@selfup Thanks for researching this, could you summarize everything you found out in a single post? I can't really look into this at the moment, but I will be able to do it in a few weeks. And I know I'll have forgotten everything I read today by then.
Yes I will make a write up of some sort to keep a good record of what has been discovered for sure. I will also keep messing around to see if I can reduce the footprint of any 3rd party function if needed! π
Thanks! It's totally fine if core needs to change for this to work by the way. As long as we don't run out of positive natural numbers, we'll be fine.
So, we are moving into the solution to recreate an app each time we are able to enhance the actions ?
How about this for a hack: use an HOA that holds the actions in a closure and lets you add to them after creating?
Terrible (but working) example:
const main = withAddActions(app)(state, actions, view, document.body);
main.addActions({
upBy: by => state => ({ count: state.count + by }),
downBy: by => state => ({ count: state.count - by })
});
main.up();
main.upBy(5);
main.downBy(2);
I think the better approach here is lazy action enhancement (initialization), it will increase startup time of large apps significantly and will add support of dynamically added actions.
We can mark enhanced functions with a flag to later do not enhance again.
if (!action.h) {
action = (() => ...)()
action.h = true
}
action(...)
It also could be a unique identifier flag to solve the problem if user will try to use the same actions in multiple app instances.
function app(state, actions, view, container) {
const id = Math.random()
...
if (action.h !== id) {
action = (() => ...)()
action.h = id
}
...
}
Third party enhancers must care about copy this flag if they enhance actions too.
I think it is doable without public API changes.
@frenzzy will this preserve the current API of making hyperapps? We are at 1.0 finally and it would be best to not introduce any kind of breaking change while we observe what happens in the ecosystem as it grows.
@okwolf This could work as well! It's like the dark side version π but it works. Maybe we could have a section like:
π
Breaking the API is fine if you release 2.0
Of course! But we should still see what happens to the ecosystem with 1.0. I am pretty sure @JorgeBucaran wants to help with the growing of the community as a big priority so that we can fix things when things break as people explore creating apps with the 1.0 API.
_sorry for the run on sentence lol_
This solution actually works without any modifications to the library however I'm not a fan of capturing a reference to the state in the view function and am also not sure how performant this would be on a large app.
import { h, app } from './src/index.js'
let state
const initialState = {
count: 0,
}
const actions = {
down: () => state => ({ count: state.count - 1 }),
up: () => state => ({ count: state.count + 1 }),
}
const newActions = { up2: () => state => ({ count: state.count + 2 }) }
const view = (currentState, actions) => {
state = currentState
return h("div", {}, [
h("h1", {}, currentState.count),
h("button", { onclick: () => actions.down() }, "-"),
h("button", { onclick: () => actions.up() }, "+"),
h("button", { onclick: () => actions.up2() }, "+2"),
])
}
app(initialState, actions, view, document.body)
setTimeout(() => app(state, {...actions, ...newActions}, view, document.body), 1000)
The problem with the reference to state could be solved by returning the state from the call to app as proposed above. let { actions, state } = app(/* arguments */)
I like this as well!
Pretty sure I was able to remove all old references here: https://github.com/hyperapp/hyperapp/issues/533#issuecomment-355206787
Yea we have no idea to be honest. Hyperapp initializes quite quickly compared to other libs, so taking advantage of that in this case could be the perfect use of that attribute.
The whole ref/mutation aspects of the solutions are a tough pill to swallow, but at the same time it enables us to not change the API.
Can you think of a way to _add_ to the API _without breaking_ anything that would enable us to update actions/state?
I am going to mess around on a branch and see what I can come with as well.
Thanks again for indulging in this weirdness with us π
But changing the API how? I am still counting on https://github.com/hyperapp/hyperapp/issues/533#issuecomment-355211415.
Yea I am still moving and stuff so this might take me a week or so haha.
To have another action to check possibly.
We check for 'init', we _could_ check 'bruteForce' (or whatever you want to call it), and in there we can add actions/state, then re-render and have patch not have to update everything since it will check to see.
However because we initialize faster than we patch (no checks need to be made really), it could just be worth calling app again at that point and not changing anything π€
@selfup is mine the dark side because it mutates the actions returned by app?
If that's the case, a slightly more elaborate version could use a deep copy of the actions and would still work. The only limitation there would be the existing actions couldn't "see" the dynamically added ones, but then if they needed to you probably should have loaded them together with the app anyway.
One redeeming quality of the HOA approach is you can drop it in an existing app without breaking other HOAs or needing to duplicate existing state, actions, and container parameters.
And it's not just code splitting that benefits from this. You could actually use this feature for true HMR also, which I haven't seen a solution for in Hyperapp yet.
@selfup I was thinking about exporting an additional function in the last days but haven't had the time to play around with the code yet.
My idea was to use the actions object returned from app as a reference to the application. We could then store those references internally together with actions and state and expose an additional updateApp (name tbc) function which takes the reference and new actions/state and updates those internally. This would remove the need for a full re-render.
const main = app(/* arguments */)
import('./mymodule.js').then(({state, actions}) => updateApp(main, state, actions))
Followed up the discussion.
@timjacobi yup, helper function sounds good to be and the best way to go.
@timjacobi @olstenlarck yep I agree as well _but_
People like to have multiple hyperapps or use HOAs, so this exposed function will be problematic (if exposed from the main hyperapp import.
We could just expose a function that is referenced to a certain app like this:
const { actionsA, updateApp: updateAppOne } = app(...);
const { actionsT, updateApp: updateAppTwo } = app(...);
// ...
import('./foo.js').then(({ state, actions }) => updateAppOne(actionsA, state, actions));
import('./bar.js').then(({ state, actions }) => updateAppOne(actionsT, state, actions));
This way we can easily call it in a Promise like you suggested
This is fun π
Let me know what you all think
@okwolf I think you are onto something with HMR. Maybe the solution above can help with that as well. HOAs are not as common, and exposing something to userland that is as simple as: return mergeHelper(oldState, oldActions, newState, newActions); in a appropriately named action that we would check for can really solidify (as well as simplify) the overall process.
Ok I got it to work, but we have to ask the user to update the exposed actions themselves.
const {
actions: exposed,
updateApp,
} = app(
state,
actions,
view,
document.body,
);
// ...
setTimeout(() => {
const newState = {
hello: 'hello world',
num: 42,
};
const newActions = {
sayHello: () => s => ({ num: s.num * 2 }),
};
Object.assign(
exposed,
updateApp(
newState,
newActions,
),
);
}, 3000);
I will make a PR with the code. It's very rudimentary and will def need a review (or a different approach) but please take a look! (I will update this once the PR is made)
Unfortunately this is a _minor_ breaking change π
Ok I got it to not break the API: https://github.com/hyperapp/hyperapp/pull/542#issuecomment-355713808
Let me know what you all think!
I like @okwolf's HOA approach or a way to reuse app (call it twice), but definitely not an updateApp function.
@selfup wouldn't calling app multiple times also call oncreate again for all nodes in the new VDOM tree? I'm not sure if onremove and ondestroy would be called for doing cleanup on the previous app, but I'm pretty sure there would be a memory leak of everything in scope of the closure of the previous app call.
Hi all!!! I'm super late to this party, but I'm Sean one of the maintainers of webpack <3!
If you were looking in terms of API's that support code splitting well, you simply need to surface the capability for your API to accept a Promise
Therefore webpack will take import('./path/to/module') and transform it to a Promise that resolves that module. I didn't take time to look through all of the thread here, but figured I'd provide my 2 cents if you want it :-D
Oh wow!! Sorry I saw that I actually commented on this back in 1.0!!!! https://github.com/hyperapp/hyperapp/issues/244
Super sorry for flaking out (switching emails lost all my GitHub notifies). Anyways, so this is done in vuex right now using
store.registerModule()
So I believe you can do:
import SomeVuexStoreModule from "./modules/users"
store.registerModule(SomeVuexStoreModule );
or async
if (someRuntimeConditionalForcesMeToNeedsomeUserStuff) {
const SomeVuexStoreModule = () => import("./modules/users");
store.registerModule(SomeVuexStoreModule
}
Inspired by this discussion (and Hyperapp in general), today I added lazy loading / code splitting to Derpy, my silly little state manager π
Example from the docs:
const store = createStore();
import('./counter-model').then((CounterModel) => {
store.set({ counter: CounterModel });
// `store.model.counter` is now available
});
Or you could do something like:
const store = createStore({
async import(key, moduleName) {
return { [key]: await import(moduleName) };
}
});
store.model.import('counter', './counter-model');
store.model.import('anotherModel', './another-model');
The key is calling the createProxyFunction on the functions in the imported module. createProxyFunction resolves promises, merges the state, and calls the subscriptions.
Perhaps others in the community can get some ideas from this. Cheers!
@vdsabev oh neato!
Hey @TheLarkInn! Ok so here is the dilemma:
thisIE10/11 is a deal breaker π
Can you think of a way to support Promises in core....without a polyfill/supporting Promises?
We might just expose the solution to userland (some npm module) and go with a HOA (Higher Order App) or a new app instance binding to the same root div.
We can already dynamically import views (stateless components essentially) into the VDOM, and state can be updated via actions (as well as exposed app actions), but updating actions/exposed actions themselves is the hard part for us.
So yea, this is why there is so much experimenting!
@okwolf You bring up an excellent point about the lifecycle hooks/mem leaks...
Maybe the HOA is the Jedi after all π
https://codepen.io/selfup/pen/vpWWGx?editors=0011
So now we would import the function that wraps each app:
const main = injectUpdate(app)(state, actions, view, document.body);
main.updateApp({
newActions: {
mult: () => ({ count }) => ({ count: count * 2 }),
},
newState: {
count: 90,
},
});
// OR
const main = injectUpdate(app)(state, actions, view, document.body);
import('./thing.js')
.then(({ state: newState, actions: newActions }) => {
main.updateApp({ newState, newActions });
});
Thanks to @okwolf this is very very simple from a userland perspective!
### Biggest benefits
@selfup I'm liking the direction this is going. You could even make updateApp take a new view for complete dynamic functionality! β
That would be a cool way to route! Woohoo
One side effect of updateApp via an HOA is action obfuscation for devtools like @hyperapp/logger and hyperapp-redux-devtools:

If this becomes a common pattern for code splitting/HMR/etc then we could have the dev tools look for typeof payload.action === "function" && payload.data and then expand the payload so dynamic actions look normal.
@selfup here's a slightly refactored HOA for updateApp that supports changing the view: https://codepen.io/okwolf/pen/ppdpgj?editors=0010
I think we found a winner!!!
Can we find a solution using @timjacobi's original proposal? It was the most elegant of all the current suggestions. I have to admit @okwolf is a magic wolf πΊ for creating that HOA, but maybe we have a simpler solution. π€
@JorgeBucaran the main issue with calling app multiple times is the coordination of unwiring and cleaning up the resources of the previous app. Unless app returns an action you can use for shutdown/cleanup there's going to be weird behaviors and possible memory leaks.
An HOA seems to be the best userland solution, and I'm not sure what we would add to core that would be acceptable for this.
Yea calling app twice unfortunately will fire a bunch of events but never fire clean ups first, and even then things get tricky. It would be a huge performance hit. Kind of something we naively didn't envision π
I really liked the idea, so much so I wrote a whole book pretty much in this issue LOL but I think HOA is the safe, least negative approach, that also provides other juicy benefits!
@selfup What about my cantTouchThis suggestion here? That no good?
@JorgeBucaran what would this do? Internally how would this stop oncreate being fired, but ondestory being called. Would those calls be synchronous?
If you can solve that, we should be good, but it feels dirty haha
@okwolf had trouble doing stuff like that when we were talking about it on Slack
If we pick the HOA we have two options:
@hyperapp/update)const { h, app, updateApp } = hyperapp but that would add a lot of bytes to core for a not so common featureThe issue of unwanted bytes makes me wonder if we should have multiple builds? If somebody had an even smaller h implementation, right now they canβt use it without also loading the one that is baked in.
@selfup if we want the solution to be an updateApp function I think it should be a modular HOA addon and not built into core.
I think the only change we might make to core would be supporting calling app multiple times in a way that cleans up the old app somehow.
@Pyrolistical If somebody had an even smaller h implementation, right now they canβt use it without also loading the one that is baked in.
Actually, you can totally use your own h implementation without loading the one that is baked in thanks to rollup/fusebox/webpack's tree-shaking.
import { h } from "./my-h"
import { app } from "hyperapp"
// ...
if we want the solution to be an updateApp function I think it should be a modular HOA addon and not built into core.
I think the only change we might make to core would be supporting calling app multiple times in a way that cleans up the old app somehow.
I completely agree with @okwolf. π―
For h function interop I agree about tree shaking π
And yes, @okwolf I think we can all agree that the HOA is the most flexible, safe, headache free solution at this point π
I would like to get a view of the heap while running the application in order to understand which cleanups we need to perform when running app multiple times. Does anyone know a good tool to do this?
That alone could be an interesting project as well! I'll look around
I'd like to clarify that I am still open to changing core to support this out of the box if we can find a viable implementation to @timjacobi's proposal of calling app() twice.
I think the only change we might make to core would be supporting calling app multiple times in a way that cleans up the old app somehow.
This sounds like a good idea in general.
@whaaaley This sounds like a good idea in general.
Agreed. Dynamically updating apps just happens to be one use case that will exacerbate this issue.
What is awaited when "cleaning up the old app" ?
I think the only change we might make to core would be supporting calling app multiple times in a way that cleans up the old app somehow.
Definitely! But it requires some reference to the app instance, somehow.
I mean: we don't want to indiscriminately clean up anything from previous calls to app. Because we want to keep the ability to have multiple apps running side by side. (right?)
Because we want to keep the ability to have multiple apps running side by side. (right?)
Not really (at least that's not how I hope people will use Hyperapp), but on the other hand, if we make it impossible to run multiple apps side by side, then we close that door forever, which is not good either.
if we make it impossible to run multiple apps side by side, then we close that door forever, which is not good either
right that's what I meant. I know it's not a primary use-case, but it'd be a shame to have to say "no that's impossible". Even something as vanilla as a "demo-app-gallery" would not work any more.
The cleanest way to integrate Hyperapp into legacy MVCs is to have multiple Hyperapps. So it would be nice to keep _instances_ in mind π
@selfup The cleanest way to integrate Hyperapp into legacy MVCs is to have multiple Hyperapps. So it would be nice to keep instances in mind π
Some might prefer not to think of such approaches as "clean" and choose other dirty approaches for converting a legacy app π
Yes that as well, but with template partials, seperate instances that can communicate via exposed actions IMHO is clean. But then again that's just me π. GitLab does the same with Vue (EventHub)
What about a combination of an injecting action and dynamically loaded ordinary functions?
Maybe something like
app(
/* state */ {
pageData: { /* page specific data */ },
currentPage: function (state, actions) {
/* a function that returns the current view */
},
global: { /* the global state */ }
}, /* actions */ {
inject: function ([f, ... remaining]) {
return function (state, actions) {
return {
pageData: $.extend({}, state.pageData,
f.apply(null, [state.pageData, actions].concat(remaining)))
};
}
}
// Actions omitted for navigation
}, /* view */ function (state, actions) {
return state.currentPage(state, actions);
}
);
Cf. #558
Then just inject functions in components:
// Function body
return (<input class={ getClass({
input: true,
"input--valid": props.valid
}) } oninput={ function (e) {
// validate() is an ordinary function
props.inject([validate, $(this).val(), props.condition]);
}} />);
can we treat init of a new app in the same way as generating the next view? if so wouldn't there be no need for clean up?
Basically Hydration works well for this scenario, isn't it ?
I would suggest that one add no actions at run time, but, as I previously discussed, that an injecting action be added, which calls non-action functions that could be dynamically imported and could return state slices.
@infinnie I would suggest that one add no actions at run time, but, as I previously discussed, that an injecting action be added, which calls non-action functions that could be dynamically imported and could return state slices.
If you have a function that you can call and it returns a state slice that gets merged isn't that the definition of a wired action? π€
But that is an ordinary function that, except for being passed in as an argument by an action, it has nothing to do with the HyperApp mechanism. See my previous comment for an example.
@infinnie what's the use case exactly for this non-action action?
I think the killer features that actual dynamic actions enable are code splitting and hot dev tools like HMR πΆ β»οΈ
@okwolf I guess that enhancing non-action functions in real actions could even more elegantly do that.
@okwolf Could you create a gist with instructions describing how to use your HOA? I can't never find
the link. π
@jorgebucaran how's this for starters? https://gist.github.com/okwolf/d38773ae51c0837ba73dad1e3e348b83
OK, to demonstrate βenhancing non-action functions in real actionsβ, Iβve created a repository:
https://github.com/infinnie/countertest
Have fun with that!
So Iβve come up with a more elegant solution (than my previous one):
// counterfunctions.js
export var counterFunctions = {
stateGetter: function (state) { // should return an object
return {
values: state.pageData
};
},
mutators: { // analogous to synchronous actions, and would not be exposed to the render function
up: function (name, by) {
return function (boundState) {
var obj = {};
obj[name] = boundState.values[name] + by;
return obj;
};
}, down: function (name, by) {
return function (boundState) {
var obj = {};
obj[name] = boundState.values[name] - by;
return obj;
};
}
}, operations: { // could be either synchronous or asynchronous, call multiple mutators, and would be exposed to the render function
up: function (boundState, mutators, name, by) {
return mutators.up(name, by);
}, delayedDown: function (boundState, mutators, name, by) {
setTimeout(function () {
mutators.down(name, by);
}, 2333);
}
}
};
Counter.jsx:
import { connect } from "../utils/connect";
import { counterFunctions } from "./counterfunctions";
var {
stateGetter,
mutators,
operations
} = counterFunctions, Connector = connect(stateGetter, mutators, operations);
export var Counter = function ({ name, by }) {
return (<Connector render={function ({ values, delayedDown, up }) {
// do something
var value = values[name], down = delayedDown;
return (<p>
<button class="btn counterBtn" type="button" onclick={function () {
down(name, by);
return false;
} }>
−
</button>
<span class="counterValue">{value}</span>
<button class="btn counterBtn" type="button" onclick={function () {
up(name, by);
return false;
}}>
+
</button>
</p>);
}
}/>);
};
And counters could be called like
<Counter name="CounterA" by={2} />
JSFiddle: https://jsfiddle.net/x527ch03/4/
@infinnie Does this make use of Lazy Components?
@jorgebucaran Yes. Lazy Components are utilized in the connect() function, which is shown in the Fiddle but omitted here.
@infinnie This is okay for 1.0, good job! Now, for 2.0, lazy components are "probably" going to be removed (the functionality may still remain in order to create dynamically loaded components). On the other hand, implementing dynamic actions will be much easier in 2.0 so it doesn't matter. π
However, what lazy components are (especially) good at is avoiding your having to pass the state and actions all the way down. Any solution for that?
Implementing dynamically loaded components is not the major obstacle to removing lazy components. Having to pass the state and actions all the way down is.
@infinnie As you may (or not?) be aware 2.0 actions are not passed down anymore, the state is, so the problem was solved in half.
I am, but where could I see the in-development Hyperapp 2 library?
Was their too much work in resolving the lazy components?
@infinnie Soon! π
@TheLarkInn Yeah, lazy components are computed during patching so it's difficult to eliminate nulls. The current implementation translates null to "" for lazy components, but that causes keys to malfunction in some cases https://github.com/hyperapp/router/issues/66.
In 2.0 I am going to use the functionality of the lazy components to implement dynamic components, kind of like react-loadable, so they are not completely going away though! :)
Totally understandable!! No worries keep up the great work!!!!
FYI: Polymer apparently caught the Redux bug and provides an enhancer for dynamically adding reducers.
Once actions are unwired in 2.0 this is no longer an issue, correct?
@okwolf Almost. We still need to figure out exactly how dynamic components will be implemented.
What do mean by dynamic components?
Something like this? https://codesandbox.io/s/qzoyznopnj
Its harder to think about dynamic components in 2.0 since action and view are separate things. I avoided this issue in mainapp by defining a component as an plain old javascript object with data+action+view. This way I can load the whole component in one shot: https://github.com/concept-not-found/mainapp-samples/blob/master/code-splitting-component/src/index.js#L5
What do mean by dynamic components?
Components that can be imported using the dynamic import() proposal.
Its harder to think about dynamic components in 2.0 since action and view are separate things.
_No_, on the contrary, it's much easier because you no longer need to pass an actions implementation object to the app() function. The module that exports YourComponent may import its own actions and not need to worry about wiring them to Hyperapp in order to use them.
@Pyrolistical One more thing, don't spread misinformation like that.
Not trying to spread misinformation. Just didn't understand how 2.0 is suppose to work. Just need to see an example
See https://github.com/hyperapp/hyperapp/issues/672 β βDynamic Imports"
I don't paste it here because the API may actually turn out different. I haven't looked too much into this part of 2.0 yet. The possibility is there, I'm just not exactly sure what the API should look like.
Yeah, I've seen that. But what I don't understand how do you load both actions+data+view with one import?
@Pyrolistical You'll dynamically import a module that it itself imports a view component and actions.
@jorgebucaran Will there be an built-in mechanism for attaching the initial state of dynamically imported actions to the app's state tree? Or will we need to run our own "init" action after import('blah.js')?
@SkaterDad Good question. I am thinking there will be no built-in mechanism, but rather we will pass the dispatch function into to components so we can create something like ReactLoadable and within it, initialize any dynamically loaded state.
Does that make sense to you?
I think we need a way to ensure that the dynamic import and init only happen one time per application session.
Having the components do their own initialization seems like it could lead to problems, particularly if the VDOM patching triggers the component to be recreated and you wanted existing state to be maintained.
I haven't looked into how React Loadable handles this, but I would guess they don't care about re-initialization since the state is local to the component you're loading.
@SkaterDad The import occurs only once when the component is created. Just make sure it's not destroyed or recycled.
I think my library can help: https://github.com/luisvinicius167/hyperapp-lazy-modules
You can lazyload you modules with their own view, actions and state.
Have anyone been able to dynamically load actions in V1? cc @okwolf @SkaterDad
I'd like to close this as outdated and (eventually) create a new issue to discuss dynamically loading components, actions, modules, etc., in V2.
My apps are at a point where I don't need to do code splitting or dynamic importing (full blown SPA with mutliple sections is still < 45 KB gzipped :grinning:), so I don't have anything else to add here.
It seems like V2 will make this a lot easier, though.
@jorgebucaran I really like all of these "Summary" comments you're adding to old issues. :+1:
@jorgebucaran I've been able to get HMR to work using hyperapp-moisturize by @selfup. This isn't really relevant anymore for V2 so I'm not sure if there's any more discussion to be had in this issue.
Here is a higher-order-(app)-function that has the ability to be called multiple times to load actions dynamically by @okwolf.
import { h, app } from "hyperapp"
// higher-order app function for update powers
const withUpdateApp = nextApp => {
return (initialState, actionsTemplate, view, container) => {
let currentView = view
const enhancedActions = nextApp(
initialState,
{
...actionsTemplate,
runAction: ({ action, data }) => state => {
const result = action(data)
return typeof result === "function"
? result(state, enhancedActions)
: result
},
updateApp: ({ state = {}, actions = {}, view = currentView }) => {
Object.keys(actions).forEach(name => {
Object.assign(enhancedActions, {
[name]: data =>
enhancedActions.runAction({ action: actions[name], data })
})
})
currentView = view
return state
}
},
(state, actions) => currentView(state, actions),
container
)
return enhancedActions
}
}
// Compose with app
const main = withUpdateApp(app)(state, actions, view, document.body)
// Use with updated state, actions, and/or view
main.updateApp({
state: {
// new state values from here will be merged in
},
actions: {
// these actions will be added/overridden
},
view: {
// optional new view function to use
}
})
Most helpful comment
@jorgebucaran how's this for starters? https://gist.github.com/okwolf/d38773ae51c0837ba73dad1e3e348b83