Closed jorgebucaran closed 7 years ago
You mean for the first paragraph or second?
The example I posted above is still relevant and does what I said in both paragraphs actually. I'm working on another example too.
@Swizz One thing !
Let's say we are switching-out the components but what about their state when they are switched ? We should preserve their state in memory so as to avoid re-rendering (performance issue). Right ? This way we could cache or store the components which are not active.
Passing a reference to app to components is still an unknown to me, but merging states and actions seems even harder. Anyway,
counter/Counter.js
import { h } from "hyperapp"
// this part could be wrapped in a function call, but I don't see a reason to
export default (props, children, actions) => (
<div>
<h1>{props.state}</h1>
<button onclick={actions.up}>+</button>
<button onclick={actions.down}>-</button>
</div>
)
counter/actions.js
export default {
up: state => state + 1,
down: state => state - 1
}
import { h, app, scopedActions } from "hyperapp"
import actions1 from "counter/actions.js"
import Counter from "counter/Counter.js"
app({
state: {
title: "Hello.",
counterItem: 5
},
view: (state, actions) =>
<main>
<h1>{state.title}</h1>
<Counter state={state.counterItem} />
</main>,
actions: Object.assign({}, scopedActions("counterItem",actions1), ...)
})
That scopedActions part I'm not proud of, but the goal is to let more than one component share an action through an obvious reference. it could work without scopedActions but each action would have to be capable of not breaking the whole state.
If we put actions in components, how do you prevent them from name collisions while keeping them available to other components? And don't tell me they shouldn't be available - all redux projects I've seen had reducers in multiple features listen to a shared action type (eg. rerendering something when you close a modal)
As my first suggestion was bad (I realized why), I was experimenting more and wrote everything as an objects
const Counter = {
view: (state, actions) => (
<div>
<button onclick={actions.add}>+</button>
{state.number}
<button onclick={actions.sub}>-</button>
</div>
),
actions: {
add: (state) => state.number + 1,
sub: (state) => state.number - 1,
}
};
I really like how simple it looks and how easy it would be to add features to it. What do others think? My suggestions are:
@Dohxis your current suggestion repeats a lot of bits already printed above, but it doesn't explain how state would be scoped or actions merged. If you ignore these two aspects, your suggestion is practically the same as my previous one.
@naugtur Yes, I just want to support the suggested component design. No wrappers like component()
simple plain objects. Talking about actions I guess it would be nice if all actions from all components would be merged into one under its own namespace. I am sure there is a better way to do this but having functionality like this would help I think.
const Counter = {
view: (state, actions, globalActions) => (
<div>
<button onclick={actions.add}>+</button>
{state.number}
<button onclick={actions.sub}>-</button>
<button onclick={globalActions.OtherComponent.doSomething}>Go wild!</button>
</div>
),
actions: {
add: state => ({ state.number + 1 }),
sub: state => ({ state.number - 1 }),
}
};
@naugtur I'm a little worried there still needs to be some key/id for the components even if we have scopedActions(), or people are going to be mucking up the state thinking that it's only in the component or did I miss a part there?
What if you made the Component() a part of the hyperapp api and had it extend the state and generate id's for each component?
NOTE: very rough not working code just trying to express my thought
function Component(name, {state, views, actions}) {
let id = generateId();
/* I'm thinking components extend the global state atom something like
{
Components: {
name: {
id: {
// state specified by component goes here...
}
}
}
}
*/
// and they can register name, to a map for components {state, view, actions}
// maybe it could wrap actions, kind of like scopedActions in naugtur's example above.
scopedActions = Object.keys(actions).map((a) => {
return (globalState) => a(globalState.Components[name].id)
// I'm thinking actions would need to be curried or partially applied
})
// then as long as the actions returned state relative to the described state given to Component(),
// we would know where to merge it...
// down side would be then it would have trouble changing state outside of it's component scope,
// or we would need to create another way to differentiate a scopedAction and non scoped action...
// return some object that will be useful on render passes to look up component by name and id?
}
Then the part of the code that checks for tagName or function in h.js, could be modified for components by name instead of just using the function as is?
I don't know that might be to far off from the elm way, been looking at the docs for scaling the elm architecture and I really don't like elm syntax lol.
I'm trying to jump into this topic, trying to follow along on the progress so far. Quite bewildering but fun :) Anyhow, one question stands out for me:
(please excuse me if this has already been talked about, and please nobody take this as any kind of criticism -- it's a well-meaning and honest question)
It is very clear we are aiming to keep all the state in a single state-tree. I'm all for that. As I understand it, the main reason for having a central store for all state (and the main motivation for libraries like Flux, Redux, Vuex...) is because apps with only locally-stateful-components end up with very messy, hard to follow "cross-talk" -- because they need to share a lot more state than initially anticipated.
But the patterns I've seen so far have state and actions that are defined very independently. Even though we're merging their state and actions to the main tree, they don't seem to have any access to the main state tree. So how are the components meant to communicate? If the answer is: "via the props", then they are effectually no different to locally stateful components. And if that's the case, then what's the point of the "one-state-to-rule-them-all"?
So I guess what I'm saying is: it looks to me like were trying to emulate locally-stateful components, and I don't see how keeping their state in the main tree will save us from the problems that locally-stateful components exhibit in frameworks that support them.
So, clearly there's something here I'm not getting ;) Anyone care to fill me in?
@zaceno I agree that it will be emulating stateful components but there could be some way of escaping the scoping, maybe extend actions to have global and local. I understand the concern, but also see where it could be implemented in a way that makes some applications easier to express.
@andyrj I agree that locally-stateful components, judiciously used (wether emulated or not) can make an application easier to express.
I just feel like if (mostly) locally scoped components are what you want then just let the scope be local, don't bother with merging it to the main state tree. Do what Vue, React, et al do and let the components communicate via props and/or event bus.
So I'm not opposed to locally scoped components (on the contrary, I use them) BUT that can be solved outside hyperapp. There's no reason for hyperapp to explicitly support local-state components.
The thing I'm questioning now is: why do we want hyperapp to support emulated locally-scoped components?
@zaceno ya I looked at your example and was impressed by the dark arts ;) (referring to @jbucaran's comment on it) I didn't fully grasp how it worked though I am gonna have to look over it again.
Something still keeps making me think, if there's a clean way to do it, having the single state atom and components would be nice...
@andyrj: Lol, yes I'm known to break a taboo or two :) Actually don't look too closely at that example, because I realized it doesn't update when you pass new props to it. So there's a little more work to do with it, but I'm certain it can be made to work. The basic notion is that a locally scoped component is just it's own app(...)
-- you just need to solve how and where to mount it.
Not to be too "down" on this thread, I must say: I see that there are a few real and important problems we're trying to solve here (just not sure if components are the answer). And those are
How we can scope / fragment state. So actions & views can be reused for different parts of the state tree without being dependent on the structure of it.
How we can work with a dynamic state tree. Instantiating new "branches", reordering them et c (in conjuction with having bound views and actions)
At least the first problem, I've seen some nice patterns presented on slack and elsewhere I think. Perhaps that's enough until the problem is more clearly understood?
@zaceno In my examples (including the first one in repo that actually works) I focus on having one state that's bound to all actions no matter where they are. Latest example is also scoping the state so that each action is not forced to be able to handle the whole state object (like combining reducers in redux). Components can only interact by:
I'm not a contributor here (not saying I won't become one if opportunity presents itself) so feel free to disregard my opinion, but I think local state of a component that is a pure function is as easy as adding a closure and can successfully be ignored in this discussion.
@naugtur Your opinion is every bit as valid as mine! :) Anyway I'm not trying to present an opinion so much as understand. Clearly you guys in here have thought a lot about this.
So if the type of components you're describing have access to the entire state and set of actions (or all the actions at least) -- that certainly is something different from what I was percieving. Definitely not "emulated local scope". I think there's probably some value in that approach!
(And yes, as I said before, true local scope can be handled outside hyperapp and we can disregard that for this discussion. I only brought it up because it looked like we were on the road to that...)
So with the risk of causing even more confusion (and possibly repeating what someone in here has already proposed), here's my thing to add to the mix:
https://codepen.io/zaceno/pen/gRvWPx?editors=0010
No magic whatsoever with the state & actions (I'd prefer to leave that to some other tool, like lenses which @lukejacksonn has suggested). In this concept, components are nothing more than view functions bound to the same state and actions as the main view function. Made available through a parameter to both the view and other components. It's basically just syntactic sugar so you don't have to pass the state and actions to every sub-component that needs them
...Not sure if this is even useful enough to warrant inclusion (as the codepen shows, it could be hacked on top of app if desired) -- but at least it does not monkey with the state/actions, which I think is important.
@zaceno the only thing I don't like about that example is how the main app has to know all the state needed for the components.
I kind of feel like the benefit of components would be allowing that to be delegated while still storing it in the single state atom.
I also think what you mentioned in the other issue about returning emit from app() could be a really good place to look at making a component() that can be external to the main app() so that it doesn't require changes to the current code.
@andyrj : right, that's kind of the point I was making before. All of it except the part about still storing it in the single state atom. If you want your components state to be independent and unknown to the main state -- why even store it there? The way I see it, if that's what you want, then go for a external/locally stateful component. (and you're right, the app-returns-emit PR could help with that too)
Hence my example explicitly avoids doing anything with the state. All state available to everyone all the time :)
BTW, if you're interested in separating state & actions into different modules, you could use mixins.
I didn't read all the comments since at least two days, but I think we are on the way to discuss the concept even if concept is clear in @jbucaran mind. https://github.com/hyperapp/hyperapp/issues/238#issuecomment-311743386
The most relevant purpose is to discuss how to achieve that, isnt it ? Everybody seem to be sticking to his guns, so we need to reach a consensus.
@Swizz You're right there seems to be little consensus on what components should be and what they do. I think what we need is a good example app that could be rewritten with the various concepts to demonstrate their benefits. It's hard to reach consensus if everyone is thinking in the abstract, and has their own hypothetical use-cases in mind.
The question is, what would such an example app be? We're all writing counter-examples, but counters are too simple to show the benefits of components.
As for @jbucarans comment, I'm still wondering about this point:
Components are not stateful. Whatever they expose: state, actions, events should be merged back into the mothership state/action/events tree
I understand, and basically agree with the sentiment, but:
a) EITHER components are given restricted / no access to the rest of the system state. In which case they are essentially locally scoped for all practical purposes, and there's no point in merging the state back.
b) OR components are given full access to the mothership state / access. In that case, why have multiple references to the same data inside components? Why not manage all the state in the mothership in the first place?
So I can't really see a scenario where components (as in "sub-apps") exposing state & actions makes sense.
I do see a point to having helpers (like lenses) to access & manipulate state in a scoped fashion. But I think that is something we solve separate from components.
Can anyone help summarize the different proposals? I don't want to get bog down in implementation details, but just touch on the surface API (which is all I wanted to discuss in this issue).
What about a gist for each proposal and create a list of them here?
@jbucaran: good plan. Just documenting API's, right? -- no lengthy example apps?
Component API proposal: "Bound view functions" https://gist.github.com/zaceno/83286dfd2a18ebbd3b4f1d7cb5810a0c
I must laugh when I see how everyone uses the word "magical", but it's only magical because of missing documentation (when it works, there is no problem explaining how later).
However the goal of the components is not to complicate the code, but to facilitate work independent of the main structure. Zaceno's example is not easy readable. That doesn't solve the issue neither generally proposed components
prop/arg in app()
because imagine that you need ten or twenty components.. This quickly becomes unreadable and full of repetitions (you must write component name multiple times). So I don't see any really useful solution yet (neither mine because of problem with multiple apps).
Single state of truth was goal because of better history maintainence and time travel debugging and easy implementation of HMR. Maybe it's not useful to everyone but if we use multiple states then we create another similar library as React, Preact etc.. and only difference will be in app structure. Maybe it's enough.. I'd like create apps without classes...
@MatejMazur No need to waste energy discussing stateful components. We're not going to implement them. Ever.
@zaceno Thanks for your proposal. I've updated the top issue to add it. 🎉
Then this discuss is mainly about how to initiliaze components, how to create instance state and connect it to app state and how to keep simple writing of code without a lot of repetitions. Example with counters is good enough because imagine how you must write example with multiple counters where you can add or remove one of them. Then you realize that counter example in Elm architecture needs to know even the structure above it but that's not its interest (this is the reason why is component based approach so popular you can separate app into multiple parts and they don't need to know parent). Connecting component actions is relatively easy. But there is huge problem with calling components because you need to know which app called component and you need id of instance and this is where all problems begin. Don't worry how it will complicate structure of HyperApp core. Firstly we need to know if it's even possible to achieve something similar to this.
@jbucaran Is there a due date ?
@zaceno Wow.
@zaceno Did you just single-handled figured it all out?
@MatejMazur
Don't worry how it will complicate structure of HyperApp core. Firstly we need to know if it's even possible to achieve something similar to this.
Yes that's already possible (or close enough anyway) with the current version of hyperapp. But not if you plan to merge the state back into the main state. You run into troubles of keeping track of multiple instances, just like you said.
Additionally I'm arguing that there's no clear value in trying to write your app that way AND ALSO keep all your state in the mothership anyway. So if that's how you want to write your code (and I wouldn't blame you) there's no reason to wait for anything in hyperapp. (Except you may need https://github.com/hyperapp/hyperapp/pull/259)
@jbucaran , ...um I not sure 😜 I'm hardly sure I know what we're trying to figure out
@MatejMazur What's wrong with @zaceno's solution?
@jbucaran components
arg and prop...you must insert mutliple times a component name...once in import, then in components
and then in view args and then (finally) in view returned value. Otherwise it's the similar to previous solution what we did before (I don't know if here or in the Slack channel). This is not usable for small components..it would be messy. So components would make sense only for large applications, which is actually a completely different purpose (components should be used as building blocks for whole app not only for huge parts..then you don't need them).
You are right. For example, AddCounterButton
shows up three times:
components: {
...
AddCounterButton: (state, actions) => (
<button onclick={actions.addCounter}>Add counter</button>
)
},
view: (state, actions, {Counter, Total, AddCounterButton}) =>
<div>
<AddCounterButton />
{state.counters.map((v, i) =>
<Counter index={i} />
)}
<Total />
</div>
But how is this a drawback, considering all the issues with the other solutions?
@zaceno's proposal, by using named props, solves the issue we were having when we were using an array. Incidentally, this is also how actions
work, named props.
the destructuring in the view is just so I can use it with jsx. could just do:
view: (state, actions, components) =>
{components.AddCounterButton()}
...
I'm presuming if you're making a bound component like this, you're either declaring it once in the apps components, or including as a mixin. So there will be no "import AddCounterButton"
@MatejMazur The following signature:
view: (state, actions, {Counter, Total, AddCounterButton}) =>
is the same one you originally proposed. I didn't like it then, because I thought this could be implemented in a different way, but other solutions lead to other problems (the real world).
I like @zaceno's components, because they are just like mixins or actions, in that we have a way to tell app()
to inject them with code.
Components look a lot like actions.
(state, { increment, decrement, send, fetchAll }, data)
@jbucaran Yes, I know but once you told me that it will begin full of components then it is messy and doesn't make sense to achieve that. I think that we must end this because it goes wrong direction (much more typing with components than without them).
... uh oh -- Did I just bust in here at the last minute and push everything back to square 1? Hope not!
@MatejMazur Can you try rephrasing that sentence? I think I lost you.
@zaceno Veni, vidi, vici. 🎉
This looks pretty good, but I still have one issue with this in that, what if you are using 20 components?
@jbucaran Sry for my English. But you wrote it. Imagine 20 or more components.. that's undesirable.
@MatejMazur Ah, is that what you meant? Okay, yeah, I agree with that. 😢
But wait a second, this would be fixed if components can have their own components. No? 🤔
@jbucaran @MatejMazur I agree the destructuring of 20 components in the view function would not be pretty. But probably also not necessary. In my proposal, every component also gets access to the components-object. So you only need to destructure out the handful you need in each function
@jbucaran maybe...but it will increase boilerplate (multiple times component name).. I don't like it. Why did you change your mind on that? 😄 You didn't want that. 😄 Now we've switched opinions.
Still gotta say though -- I'm not that big a fan of my own proposal ;) Not that I see problems with it. Mostly that I don't see it adding a lot of value
@zaceno Now me either. 😄
@MatejMazur :)
But about the increased boilerplate: It's the exact same situation with actions. Right?
@MatejMazur Now we've switched opinions.
It's not a matter of opinion. I don't think we have another choice. That's all. @zaceno's proposal is at the surface level, pretty much what you had before, almost. You should be proud you were first. 😉
@zaceno ... uh oh -- Did I just bust in here at the last minute and push everything back to square 1? Hope not!
No, not at all. We're not back to square one. 😄
For starters, your API design is a bit different to what MatejMazur originally had (not for a lot though, but your use of props instead of an array makes a huge difference).
We also have come a long way (I have and I am sure so has MatejMazur) studying many of the possibilities. 🤓
We also have a tiny implementation which you've provided via that CodePen.
@MatejMazur Remember that components are going to live in their own file.
@zaceno We need components to be able to have their own components. Do you have any idea how we could achieve that with what you have?
Also, maybe you can open a PR? 🔥
Let's discuss how components could look like in a future version of HyperApp.
Submitted Proposals
Example
counter.js
index.js
Credits
All credit goes to @MatejMazur:
Related
147
219