Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] What if hyperapp was just a bit less magical and a bit more transparent? #380

Closed
jorgebucaran opened this issue Sep 20, 2017 · 12 comments

Comments

@jorgebucaran
Copy link
Owner

jorgebucaran commented Sep 20, 2017

This is not a proposal to rewrite hyperapp, but just an attempt to gather some feedback from the community about our API.

If this were to happen it would be a major breaking change. This would be so major that one might as well publish it as a different "framework". 😉

I'd just like to share the API I thought up with all of you so help me decide if this could ever be for us or if I should explore this idea elsewhere. 🤔

Here it comes...

import { h, app } from "hyperapp"
const { render, call } = app({ count: 1 })

document.body.appendChild(
  render(state => (
    <main>
      <h1>{state.count}</h1>
      <button onclick={() => call(state => ({ count: state.count + 1 }))}>
        +
      </button>
      <button
        onclick={() =>
          setTimeout(() => {
            call(state => ({ count: state.count - 1 }))
          }, 1000)}
      >
        -
      </button>
    </main>
  ))
)

Please give it 5 minutes and think about all the implications. See how by decoupling render and update from the app call that means we no longer need actions or events. And mixins, well those can be implemented in a manner similar to Redux enhancers, as the second argument to the app() call.

On the other hand you are responsible for initializing the app yourself and merging the state, actions and other stuff exported by mixins or component-like mixins.

I am not in favor or replacing the current API for this (yet), but I am starting to like it a lot. Like @lukejacksonn said, food for thought. 🍝

EDIT: Corrected as pointed out by @okwolf.

@okwolf
Copy link
Contributor

okwolf commented Sep 20, 2017

<button
  onclick={() =>
    update(state => {
      setTimeout(() => {
        update(state => ({ count: state.count - 1 }))
      }, 1000)
    })}
>
  -
</button>

@jorgebucaran is the outer update() call even needed? Shouldn't calling update from inside setTimeout already do the job? 🤔

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Sep 20, 2017

@okwolf It's needed so we can get a copy of the state.

You are right, it's not needed in this case because we don't use the state to call setTimeout! 🎉

document.body.appendChild(
  render(state => (
    <main>
      <h1>{state.count}</h1>
      <button onclick={() => update(state => ({ count: state.count + 1 }))}>
        +
      </button>
      <button
        onclick={() =>
          setTimeout(() => {
            update(state => ({ count: state.count - 1 }))
          }, 1000)}
      >
        -
      </button>
    </main>
  ))
)

...but if we need to access, e.g., state.delay, you'd need it alright.

<button
  onclick={() =>
    update(({ delay }) => {
      setTimeout(() => {
        update(state => ({ count: state.count - 1 }))
      }, delay)
    })}
/>

@jorgebucaran
Copy link
Owner Author

BTW I called the functions render and update, but that's just a name. I think render works well, but update could be dispatch, action, resolve, command, call, etc.

@zaceno
Copy link
Contributor

zaceno commented Sep 20, 2017

I think it would be worth exploring, and I'd like to experiment with it. Perhaps we could create a branch for it? Publish it as hyperapp2000 or something.

It's hard to say yay or nay off hand without exploring it a bit in code. I'm thinking about some of the patterns I depend on (wether kosher or not), such as custom-events for separating concerns, scoped updates, and prewired components. They'll probably look a lot different if they're even relevant under this new paradigm.

One thing I like about the current paradigm, is that the mixins and events system gives you one central place to customize a lot of things. If we don't have a render event in the new paradigm, for instance you can't inject other stuff to the view as arguments (if that's what you want). Not via a mixin anyway. You'd have to wrap the render function.

@infinnie
Copy link
Contributor

It would feel awesome.

Wondering what impact this would have on the router and other components that currently rely on mixins.

@SkaterDad
Copy link
Contributor

SkaterDad commented Sep 20, 2017

Like @zaceno, I currently rely on the event system in an app I'm developing. Not saying similar things couldn't be accomplished with the new API, but I'd be curious to see a realistic application before/after. Something with routing and data fetching that uses the existing events system.

Even just an implentation of the router and logger would be useful to see how it could work.

I can see the benefit of decoupling actions from the app initialization. You're basically changing hyperapp from using dependency injection to requiring devs to manually import whatever actions they're going to use in the file. That probably will make type checking, editor autocomplete and dead code elimination easier.

The current version of hyperapp has made me 100x more productive than I was with other frameworks, so I reserve my right to resist change very strongly. 😁

@danigb
Copy link
Contributor

danigb commented Sep 20, 2017

Wow. Conceptually it looks awesome to me. I don't know exactly what would be the practical implications and if the developer experience would be better or not. But this seems to summarize the essence of the functional UI paradigm, and the flexibility it exhibits is amazing.

Please release it in any way so we can play with it.

And by the way, I'm following you "behind the trenches" and I'm impressed by the community work you all are doing: making all the evolution of the library transparent, and involving all the people. Congrats 👏 👏

@Swizz
Copy link
Contributor

Swizz commented Sep 20, 2017

I dont know if I am a little bit off topic, but here we go.

I personnaly think all the magical about Hyperapp is about actions. And more precisely about the actions I need to define and the actions I will use all other the framework.

app({
  actions: {
    add(state, actions, { value }) {
      return { count: state.count + value }
    }
  },

  view(state, actions) {
    return <button onclick={() => actions.add({ value: 3 })}/>
  }
})

I have to define (state, actions, data) but after I need to call (data).

Now we admitted all over HA to define all functiont that way

(state, actions, data) => result

I think this is really a great thing to have only one function signature all over the framework. This will add simplicity and cut some questions. But what about thinking all entries of the framework as High Order Functions ?

All the entries/hooks of the framework will now follow only one function signature.

(state, actions) => (data) => function

So it could be also a great standar to introduce thunk for all the async stuff instead of Promise.

(state, actions) => (data) => (thunk) => function

So I guess it will be more easy to understand what I define and what I get.

app({
  actions: {
    add(state, actions) {
      return ({ value }) => { count: state.count + value }
    }
  },

  view(state, actions) {
    return <button onclick={() => actions.add({ value: 3 })}/>
  }
})

A more complete example (with sync and async) could be, the following counter.

app({
  state: {
    count: 0
  },
  view: (state, actions) =>
    <main>
      <h1>
        {state.count}
      </h1>
      <button onclick={() => actions.subLater(1, 3000)} disabled={state.count <= 0}>ー (3 sec)</button>
      <button onclick={() => actions.sub(1)} disabled={state.count <= 0}>ー (now)</button>
      <button onclick={() => actions.add(1)}>+ (now)</button>
      <button onclick={() => actions.addLater(1, 3000)}>+ (3 sec)</button>
    </main>,
  actions: {
    sub: (state, actions) => (value) => ({ count: state.count - value }),
    add: (state, actions) => (value) => ({ count: state.count + value }),
    subLater: (state, actions) => (value, time) => update => {
      setTimeout(() => {
        update(state => state.count <= 0 ? {} : { count: state.count - value })
      }, time)
    },
    addLater: (state, actions) => (value, time) => update => {
      setTimeout(() => {
        update(state => ({ count: state.count + value }))
      }, time)
    }
  }
})

@lukejacksonn
Copy link
Contributor

lukejacksonn commented Sep 21, 2017

In your proposal you introduce a function call which looks to take a thunk function with the signature state => result which I assume always gets passed the latest state and that the result of calling the function gets passed to update which in turn triggers a render.

Implementation details aside, this means you could do this:

state: {
  count: 0,
},
actions: {
  resetCount: _ => ({ count: 0 }),
  setCountTo: data => _ => ({ count: data }),
  addToCount: data => state => ({ count: state.count + data }),
},
view: state =>
  h('button', {
    oncreate: e => setInterval(_ => call(actions.addToCount(1)), 1000),
    onclick: e => call(actions.resetCount),
  }, state.count)

There are pros and cons to this.. have a think about it. I'm happy to answer questions 😅

@Swizz
Copy link
Contributor

Swizz commented Sep 21, 2017

If you are talking about the update(state => ({ count: state.count + value })) this is already part of Hyperapp features. Since #347

@lukejacksonn
Copy link
Contributor

lukejacksonn commented Sep 22, 2017

It is like the update function.. but you can't access update from within a view currently.. can you 🤔 wouldn't it mean I would have to write the action like this too:

addToCount: (state, actions, data) => update => update(state => ({ count: state.count + data })),

@jorgebucaran
Copy link
Owner Author

Thank you everyone for your invaluable feedback. I'll be closing here and updating #385 where I'll explain the exciting upcoming changes to Hyperapp. 👋❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants