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

Collapse reducers and effects into actions. Implement #126. #127

Merged
merged 6 commits into from
Mar 5, 2017

Conversation

jorgebucaran
Copy link
Owner

@jorgebucaran jorgebucaran commented Mar 4, 2017

Remove reducers/effects for a familiar concept: actions.

  • Actions can be used to update the model/render the view. How? Return the new model, just like with reducers before:
app({
    model: 0,
    actions: {
        add: model => model + 1,
        sub: model => model - 1
    }
})
  • In addition, actions can be used to cause side effects or update the view asynchronously. How? You have two options:
  1. Same as before. Run your async tasks inside the function and when you are done call further actions to update the view if necessary.
app({
  actions: {
    add: model => model + 1,
    sub: model => model - 1,
    waitThenAdd: (model, data, actions) => {
      setTimeout(actions.add, 1000)
  }
})
  1. Return a Promise. In this fashion, you can have multiple actions that return promises and chain them together using the .then or catch errors via .catch.
app({
  actions: {
    add: model => model + 1,
    sub: model => model - 1,
    waitThenAdd: (model, data, actions) =>
      new Promise(resolve => setTimeout(resolve, 1000)).then(actions.add)
  }
})

If you don't know Promises work or don't want to use them, that's okay. Promises are not necessary in order to use hyperapp, but it's nice you could use them if you wanted and hyperapp does the right thing (which is returning them to the caller instead of updating the model/view).

Caveats

  • Before, reducers and effects had different function signatures. Now all actions have the same signature: (model, data, actions, error).
  • To know whether an action updates the model/view or not, Hyperapp checks its return value. If you return a primitive/model then it's logical to assume you want to update the model.
  • If an action returns undefined, it doesn't make much sense to update the model or render the view.

Related:

If your action returns a model or part of a model, then this
value is merged with the model and the view is updated, this
is exactly how reducers work right now.

If your action returns undefined (does not return anything)
we don't update the model, and it works exactly how effects
do right now.

As a special bonus, if your function returns a Promise-like
object, we assume it's an effect too and return the promise.
This lets you use async/await with actions or create a chain
of task promises. Only remember to always keep your promises.
@codecov
Copy link

codecov bot commented Mar 4, 2017

Codecov Report

Merging #127 into master will not change coverage.
The diff coverage is 100%.

@@          Coverage Diff          @@
##           master   #127   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           4      4           
  Lines         174    171    -3     
=====================================
- Hits          174    171    -3
Impacted Files Coverage Δ
src/router.js 100% <100%> (ø)
src/app.js 100% <100%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f81a6b5...ea3675d. Read the comment docs.

@jorgebucaran jorgebucaran added the enhancement New feature or request label Mar 4, 2017
@jorgebucaran jorgebucaran self-assigned this Mar 4, 2017
@jorgebucaran
Copy link
Owner Author

@dodekeract Review please 🙏.

@lukejacksonn
Copy link
Contributor

Have you seen my proposal here. I have only just skimmed this PR but I think we are trying to achieve the same thing.

subscriptions[i](model, actions, onError)
}
})

function load(fn) {
Copy link

@liadbiz liadbiz Mar 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just looked the MDN api https://developer.mozilla.org/zh-CN/docs/Web/API/Document/readyState。

How about change this function to this way:

  function load(fn) {
    document.onreadystatechange = function () {
      if (document.readyState == "complete") {
        fn();
      }
    }
  }

or better, I want to create a start function contains these code:

  function () {
    root = app.root || document.body.appendChild(document.createElement("div"))

    render(model, view)

    for (var i = 0; i < subscriptions.length; i++) {
      subscriptions[i](model, actions, onError)
    }
  }

then you can make the load function into this:

  function load() {
    document.onreadystatechange = function () {
      if (document.readyState == "complete") {
        start();
      }
    }
  }

Does it look more clear to read?
(actually this is the first time I do the "review code" thing. thanks this warm community! )

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liadbiz Thanks! :)

See #69 thread for the original reason we introduced this code.

@jorgebucaran
Copy link
Owner Author

@lukejacksonn Thanks. I answered some of your comments there, please have a look! For the most up to date info, see the top of this thread.

Correct me if I am wrong, but your proposal requires actions to always return a Promise?

@jorgebucaran
Copy link
Owner Author

I'd like to proceed with this PR 🔥.

Please if you have any further comments, post them soon 🙏 🙏.

@lukejacksonn
Copy link
Contributor

lukejacksonn commented Mar 5, 2017

I think I am up to date with updates haha!

Correct me if I am wrong, but your proposal requires actions to always return a Promise?

I'm assuming you are saying this because of Promise.resolve(action(m,d,a)) if so then no this is not true as Promise.resolve(42) /=> 42. If it is not passed a promise then it will resolve immediately with the primitive value (a way overlooked quality imo). So any action that returns an already computed proposition model (a reduced value) will just pass through. Otherwise it is a promise of a reduced value or nothing (a side effect).

Seeing as action used to return nothing (or more recently actions which could be done here but there would be no point). The only issue I see with it is what @selfup mentioned which is..

The problem with that is that Promises are not very performant even if they resolve immediately

I have no solid evidence but I have read that resolving a primitive value is evaluated as If !IsPromise(x) return x;, would be interesting to see because this is nice..

container[key] = function (data) {
  for (i = 0; i < hooks.onAction.length; i++) {
    hooks.onAction[i](name, data)
  }
  return Promise.resolve(app.actions[key](model, data, actions))
  .then(result => {
    if(typeof result === 'object') { // Check it wasn't just an effect
      for (i = 0; i < hooks.onUpdate.length; i++) {
        hooks.onUpdate[i](model, result, data)
      }
      model = merge(model, result)
      render(model, view)
    }
  })
}

It also has the benefit of making all actions thenable. Which means chaining becomes standardized and no more passing actions along every reducer in a chain.

@jorgebucaran
Copy link
Owner Author

@lukejacksonn Ah, okay, right. In that case, I don't see what prevents you from doing this already then.

@jorgebucaran
Copy link
Owner Author

@lukejacksonn It seems you were suggesting to use Promise directly inside hyperapp?

@lukejacksonn
Copy link
Contributor

@jorgebucaran
Copy link
Owner Author

@lukejacksonn Even if promises were super fast, which they are not, the problem with this is forcing users to polyfill Promise.

@lukejacksonn
Copy link
Contributor

lukejacksonn commented Mar 5, 2017

I wasn't aware that was a limitation of the project. Most developers are very accustomed to importing babel-polyfill these days especially if using a bundler, it would mean having to serve an IE friendly version on CDN though perhaps.

Performance is a real issue but I would be intrigued to see how it compares. I don't think there is going to be much in it given the simplicity of Promise.resolve and how much browsers are trying to optimize it.

Let's get this PR merged and I can play around 👍

@jorgebucaran
Copy link
Owner Author

@liadbiz I've added a few comments to explain what's going on in certain places. If there are other places you think need further explanation let me know.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Mar 5, 2017

@lukejacksonn Thanks, for looking into perf.

See this: https://github.com/lukeed/polkadot/blob/master/benchmarks/readme.md by @lukeed.

I wasn't aware that was a limitation of the project. Most developers are very accustomed to importing babel-polyfill these days especially if using a bundler, it would mean having to serve an IE friendly version on CDN though perhaps.

The project has a determined goal of not straying too far away from the vicinity of 1kb. If we introduce Promise we break this promise!

Well, not true, if you are using an ultra modern browser, you wouldn't need a polyfill, so technically we wouldn't be breaking any promises, but right now, we can support IE10 out of the box without polyfills and still be 1kb, which is nice.

The entire code base is a mix of mostly ES3 and some ES5 (forEach, Object.keys) and I've been really trying to keep it that way.

Not everyone is using promises. Whatever their reasons are, if we use Promise out of the box that's a reason for a Promise-denier to not use hyperapp.

@jorgebucaran jorgebucaran merged commit 8bf3047 into master Mar 5, 2017
@jorgebucaran jorgebucaran deleted the actions branch March 5, 2017 05:11
jorgebucaran added a commit that referenced this pull request Jan 7, 2018
Collapse reducers and effects into actions. Implement #126.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants