jorgebucaran / hyperapp

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

The Elm Architecture 🍵 #65

Closed itrelease closed 7 years ago

itrelease commented 7 years ago

Maybe it would be better if effects shouldn't be called directly by user but go along with result of updating model as a second element in an array or some other structure and that second element should be handled by runtime so it will be look more like TEA:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    MorePlease ->
      (model, getRandomGif model.topic)

    NewGif (Ok newUrl) ->
      ( { model | gifUrl = newUrl }, Cmd.none)

    NewGif (Err _) ->
      (model, Cmd.none)
SkaterDad commented 7 years ago

Do you have a more complex example where you're actually updating the model before firing off the effect? It would be helpful to illustrate some benefits.

I'm not a big fan of that aspect of Elm, to be honest. It creates a lot of that Cmd.none boilerplate, like in your example (or the shorthand version with !). I'm sure elm had a very good reason to do it that way (whether it be a type system requirement or effect management reason). elm is a fantastic language, but understanding how to chain updates & effects isn't very simple.

With javascript's flexility I'm liking how hyperapp just lets you call the reducers and effects the same way.

itrelease commented 7 years ago

@SkaterDad often when I need to request something I need to set loading flag or reset some field to initial state

SkaterDad commented 7 years ago

The example in the readme implies that's already possible, since you can call the update functions from within the effect?

const effects = {
    waitThenAdd: (model, msg) => {
        msg.toggle()
        wait(1000).then(msg.add).then(msg.toggle)
    }
}

EDIT: I hope to start migrating an app sometime this week, and definitely plan on doing things like that also. Hopefully it goes smoothly.

itrelease commented 7 years ago

@SkaterDad I didn't said that you can't do this in current implementation I just pointed that you have to call this side-effect things directly and call update methods inside these effects which I think looks not that good in contrast with elm

SkaterDad commented 7 years ago

Fair enough :smile:

This is the fun part of open source to me, learning the different ways people prefer to do things.

jorgebucaran commented 7 years ago

@itrelease 🤔 What would be the proposed syntax using JavaScript?

itrelease commented 7 years ago

@jbucaran

update: {
  loadMore: (model, data, effects) {
    return {
      model: { ...model, loading: true },
      effect: effects.fetchMore(data.url) // but this shouldn't start fetching immediately this should be more like Task thing that will be forked and run by runtime
  };
}
jorgebucaran commented 7 years ago

@itrelease Hmm, this is getting interesting!

I'll need a more concrete example, if possible, based in one of the existing ones of how both models would contrast.

There's an example in the documentation for effects.

How would that entire example look in your model?

If it isn't reproducible, then you can come up with a new example yourself, but please make sure to show both an implementation using HyperApp and one in pseudo-code in your model/idea.

itrelease commented 7 years ago

@jbucaran

const model = {
  counter: 0,
  waiting: false,
  error: null
}

const view = (model, msg) => {
  return html`
    <button
      onclick=${msg.waitThenAdd}
      disabled=${model.waiting}
    >
      ${model.counter}
    </button>
  `;
};

const update = {
  waitThenAdd: (model, data, effects) => {
    return {
      model: ({ ...model, waiting: true }),
      effect: effects.wait(
        1000,
        (_) => ({ ...model, waiting: false, error: null, counter: model.counter + 1 }),
        (err) => ({ ...model, waiting: false, error: err })
      )
    };
  }
};

const effects = {
  wait: (time, onReject, onResolve) =>
    (model) => new Promise(resolve => setTimeout(_ => resolve(), time));
}

app({ model, view, update, effects });
jorgebucaran commented 7 years ago

@itrelease Thank you for your time and writing this for me. I'm going through it now, trying to understand what's going on.

At first glance I can say, however, it looks more complex than the original example.

tzellman commented 7 years ago

I agree that the example now seems a bit more complex. Maybe all we need to do is state in the docs that the effects do not cause a re-render to occur. Essentially, that is the only difference currently between effects and reducers.

jorgebucaran commented 7 years ago

See also What's the difference between effects and reducers?

tunnckoCore commented 7 years ago

What is TEA? The drink? :laughing: Sry, i'm not Elm.

As seeing the examples and discussion, what's the problem to do such thing currently?

What about the following?

const model = {
  counter: 0,
  waiting: false,
  error: null
}

const view = (model, msg) => {
  return html`
    <button
      onclick=${msg.waitThenAdd}
      disabled=${model.waiting}
    >
      ${model.counter}
    </button>
  `;
};

const update = {
  add: (model, data) => (data)
};

const wait = (time) => new Promise(resolve => setTimeout(_ => resolve(), time))

const effects = {
  wait: (model, msg, data) => {
    msg.add({ waiting: true })
    wait(1000)
      .then(() => {
        msg.add({ waiting: false, counter: model.counter + 1 })
      })
      .catch((err) => {
        msg.add({ waiting: false, error: err })
      })
  }
}

app({ model, view, update, effects });

I believe it won't work, but.. believe it is possible to be done too :D Just a matter of thinking.

tunnckoCore commented 7 years ago

Oooh, right TEA means ... okey :D

jorgebucaran commented 7 years ago

Going to close here as I don't think I'll be changing the effects API.