jorgebucaran / hyperapp

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

Components #238

Closed jorgebucaran closed 7 years ago

jorgebucaran commented 7 years ago

Let's discuss how components could look like in a future version of HyperApp.

Submitted Proposals


Example

counter.js

const Counter = (props, children) => ({
  state: {
    value: props.value
  },
  view: ({ value }, { inc, dec }) =>
    <div>
      <h1>{state.value}</h1>
      <button onclick={inc}>+</button>
      <button onclick={dec}>-</button>
    </div>,
  actions: {
    inc: state => state + 1,
    dec: state => state - 1
  }
})

index.js

app({
  state: {
    title: "Hello."
  },
  view: (state, actions) =>
    <main>
      <h1>{state.title}</h1>
      <Counter value={1} />
    </main>
})

Credits

All credit goes to @MatejMazur:

Related

naugtur commented 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.

nitin42 commented 7 years ago

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

naugtur commented 7 years ago

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)

Dohxis commented 7 years ago

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:

naugtur commented 7 years ago

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

Dohxis commented 7 years ago

@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 }),
    }
};
andyrj commented 7 years ago

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

MatejBransky commented 7 years ago

@andyrj You just rewrited my first idea. 😄 It begins look messy here..there are a lot of similar ideas. Problem with my approach is in third point of @jbucaran comment.

Edit: sry, I didn't include example code with component(..) but @jbucaran knows what I mean.

zaceno commented 7 years ago

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?

andyrj commented 7 years ago

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

zaceno commented 7 years ago

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

andyrj commented 7 years ago

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

zaceno commented 7 years ago

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

zaceno commented 7 years ago

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

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?

naugtur commented 7 years ago

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

zaceno commented 7 years ago

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

zaceno commented 7 years ago

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.

andyrj commented 7 years ago

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

zaceno commented 7 years ago

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

Swizz commented 7 years ago

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.

zaceno commented 7 years ago

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

jorgebucaran commented 7 years ago

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?

zaceno commented 7 years ago

@jbucaran: good plan. Just documenting API's, right? -- no lengthy example apps?

zaceno commented 7 years ago

Component API proposal: "Bound view functions" https://gist.github.com/zaceno/83286dfd2a18ebbd3b4f1d7cb5810a0c

MatejBransky commented 7 years ago

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

jorgebucaran commented 7 years ago

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

MatejBransky commented 7 years ago

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.

Swizz commented 7 years ago

@jbucaran Is there a due date ?

jorgebucaran commented 7 years ago

@zaceno Wow.

jorgebucaran commented 7 years ago

@zaceno Did you just single-handled figured it all out?

zaceno commented 7 years ago

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

zaceno commented 7 years ago

@jbucaran , ...um I not sure 😜 I'm hardly sure I know what we're trying to figure out

jorgebucaran commented 7 years ago

@MatejMazur What's wrong with @zaceno's solution?

MatejBransky commented 7 years ago

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

jorgebucaran commented 7 years ago

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.

zaceno commented 7 years ago

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"

jorgebucaran commented 7 years ago

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

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

zaceno commented 7 years ago

... uh oh -- Did I just bust in here at the last minute and push everything back to square 1? Hope not!

jorgebucaran commented 7 years ago

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

MatejBransky commented 7 years ago

@jbucaran Sry for my English. But you wrote it. Imagine 20 or more components.. that's undesirable.

jorgebucaran commented 7 years ago

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

zaceno commented 7 years ago

@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

MatejBransky commented 7 years ago

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

zaceno commented 7 years ago

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

MatejBransky commented 7 years ago

@zaceno Now me either. 😄

zaceno commented 7 years ago

@MatejMazur :)

But about the increased boilerplate: It's the exact same situation with actions. Right?

jorgebucaran commented 7 years ago

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

jorgebucaran commented 7 years ago

@MatejMazur Remember that components are going to live in their own file.

jorgebucaran commented 7 years ago

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