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

Allow the use of Promise that resolve to a state #305

Merged
merged 4 commits into from
Jul 24, 2017

Conversation

Swizz
Copy link
Contributor

@Swizz Swizz commented Jul 17, 2017

The following change follow the slack discussion regarding the feature allowing an action to return a Promise that resolve to a state.

Usage :

app({
  state: {
    issues: []
  },
  view: (state, actions) => (
    <main>
      { state.issues.map(issue => <h1>{issue.title}</h1>) }
      <button onclick={actions.issues}>fetch</button>
    </main>
  ),
  actions: {
    issues() {
      return fetch("https://api.github.com/repos/hyperapp/hyperapp/issues")
        .then(response => response.json())
        .then(issues => ({ issues }) )
    }
  }
})

or

app({
  state: {
    issues: []
  },
  view: (state, actions) => (
    <main>
      { state.issues.map(issue => <h1>{issue.title}</h1>) }
      <button onclick={actions.issues}>fetch</button>
    </main>
  ),
  actions: {
    async issues() {
      const response = await fetch("https://api.github.com/repos/hyperapp/hyperapp/issues")
      const issues = await response.json()
      return { issues }
    }
  }
})

When an action return a Promise. The main argument of the last .then clause will be used exactly like a sync action return.

@codecov
Copy link

codecov bot commented Jul 17, 2017

Codecov Report

Merging #305 into master will increase coverage by 0.02%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #305      +/-   ##
==========================================
+ Coverage   99.35%   99.37%   +0.02%     
==========================================
  Files           2        2              
  Lines         154      160       +6     
  Branches       48       51       +3     
==========================================
+ Hits          153      159       +6     
  Misses          1        1
Impacted Files Coverage Δ
src/app.js 99.31% <100%> (+0.02%) ⬆️

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 ef038cc...af68dd2. Read the comment docs.

Copy link
Owner

@jorgebucaran jorgebucaran left a comment

Choose a reason for hiding this comment

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

Please add a couple of tests. I'll take care of the documentation. 👍

src/app.js Outdated
if (result != null && typeof result.then == "function") {
result.then(update)
} else {
update(result)
}
Copy link
Owner

@jorgebucaran jorgebucaran Jul 17, 2017

Choose a reason for hiding this comment

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

Let's change this block to:

    if (result != null && typeof result.then === "function") {
      result.then(update)
    } else {
      update(result)
    }

and move the update implementation before the implementation of repaint right after return emit, around like 35.

Rewrite it like this please.

  function update(withState) {
    if (withState != null) {
      repaint((state = merge(state, emit("update", withState))))
    }
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to the slack discussion, we move from result.then != null to typeof result.then == "function because :

At least test if then is a function ?
If the function return nothing, the result != null will silent ignore it. But if then is not a function, that will break the thing

Copy link
Owner

Choose a reason for hiding this comment

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

@Swizz That's probably a misunderstanding. If then is not a function then the user is doing something very weird.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we forbid the use of then in state ?

app({
  state: {
    foo: 0,
    bar: 1,
    then: 2
  },
  actions: {
    change: state => ({ then: state.then + 1 }),
  }
})

Copy link
Owner

Choose a reason for hiding this comment

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

I don't think we can forbid it. I am not too worried about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, we need to check if resul.then is a function in order to call it.

If the end user return a function let execute it, and the result not null check will do the rest. If this is not a function lets do a regular result patch.

Without that, using then in result will break the app with an hyperapp internal error.

Copy link
Owner

@jorgebucaran jorgebucaran Jul 17, 2017

Choose a reason for hiding this comment

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

I don't understand what you are saying, but yes, use:

if (result != null && typeof result.then === "function") {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I use the following app

app({
  state: {
    foo: 0,
    bar: 1,
    then: 2
  },
  actions: {
    change: state => ({ then: state.then + 1 }),
  }
})

This will raise an error when using action

Uncaught TypeError: result.then is not a function

Copy link
Owner

@jorgebucaran jorgebucaran Jul 17, 2017

Choose a reason for hiding this comment

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

@Swizz

if (result != null && typeof result.then === "function") {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes yes 👍

@jorgebucaran jorgebucaran added the enhancement New feature or request label Jul 17, 2017
@jorgebucaran jorgebucaran requested review from zaceno and jamen July 17, 2017 18:15
@zaceno
Copy link
Contributor

zaceno commented Jul 17, 2017

So this means you can no longer chain async actions, without the state being changed in each step of the chain?

I have a vague feeling (can't substantiate it though...) that while this will make some things that are already possible simpler, it will also make other things (that we can do today) impossible, or more complicated. Maybe I'm just an old man afraid of new things... ;)

@jorgebucaran
Copy link
Owner

@zaceno So this means you can no longer chain async actions, without the state being changed in each step of the chain?

What do you mean?

@jorgebucaran
Copy link
Owner

See this example:

const delay = seconds => new Promise(done => setTimeout(done, seconds * 1000))

app({
  state: 0,
  view: (state, actions) =>
    <main>
      <h1>
        {state}
      </h1>
      <button onclick={actions.upLater}>+</button>
    </main>,
  actions: {
    up: state => state + 1,
    upLater: async (state, actions) => {
      await delay(1)
      actions.up()
    }
  }
})

With or without this patch, if you click ten times the + button, the counter will increase x 10 with a delay.

Now, see the following example:

app({
  state: 0,
  view: (state, actions) =>
    <main>
      <h1>
        {state}
      </h1>
      <button onclick={actions.upLater}>+1</button>
    </main>,
  actions: {
    upLater: async (state, actions) => {
      await delay(1)
      return state + 1
    }
  }
})

Without this patch the above is an error, but with this patch, actions.upLater are debounced, so clicking the + button 10 times quickly only updates the counter by +1.

@zaceno
Copy link
Contributor

zaceno commented Jul 17, 2017

@jbucaran Well I guess what becomes impossible with this change, is if you wanted to get inbetween an xhr call returning, and the state being update, but you didn't want to put the logic for that in a .then inside the action that makes the async request.

I often find myself using three distinct "types" of actions:

  • those for doing async backend requests (basically, the old "effects")
  • those for setting state (basically old reducers)
  • actions that compose async-request-actions and state-setting-actions in various ways (also like old "effects")

Mainly I think my worry stems from this new behavior breaking this pattern (since now, async-backend-request-actions will also update state. At some later time...). I don't yet see any real practical problems, though, so I'm probably just stuck in my ways, uncomfortable with change.

I do see how this makes actions more elegant. It reduces my "three-type" pattern down to two types:

  • Actions that update state (wether now or in the future)
  • Actions that reuse other actions.

I can feel my opinion gradually becoming more positive to this change during the writing of this post ;)

@selfup
Copy link
Contributor

selfup commented Jul 17, 2017

How would .catch and .done be handled?

@Swizz
Copy link
Contributor Author

Swizz commented Jul 17, 2017

Unfortunaly using .catch will break the chain, because the following .then will resolve

const p = new Promise(function(resolve, reject) {
  reject('fail');
});

p.catch(function(e) {
  console.log('error', e);
}).then(function(value) {
  console.log('success', value); 
})

'error' 'fail'
'success'

It would be possible to catch earlier and return the promise

const emit = app({
 actions: {
    issues() {
      const request = fetch("https://api.github.com/repos/hyperapp/hyperapp/issues")
        .then(response => response.json())
        .then(issues => ({ issues }) )

      request.catch(error => emit('error', error))

      return request
    }
  }
})

with async/await

const emit = app({
 actions: {
   async issues() {
      try {
        const response = await fetch("https://api.github.com/repos/hyperapp/hyperapp/issues")
        const issues = await response.json()
        return { issues }
      }
      catch {
        emit('error', error)
      }
    }
  }
})

@lukejacksonn
Copy link
Contributor

lukejacksonn commented Jul 17, 2017

I am wayyyy late to the conversation here (since just this afternoon even!) but I remember suggesting this a long while back.. declined due to having to support Promise in core (which I appreciate might still be a limitation). I only mention this because I see this as reimplementing Promise.resolve().

This would be how I would Allow the use of Promise that resolve to a state.

function initialize(namespace, children, lastName) {
    Object.keys(children || []).map(function(key) {
      var action = children[key]
      var name = lastName ? lastName + "." + key : key
      var update = (action, name) => data =>
        action(
          state,
          actions,
          emit("action", {
            name: name,
            data: data
          }).data
        )
      if (typeof action === "function") {
        namespace[key] = data =>
          Promise.resolve(update(action, name)(data)).then(result =>
            result != null && repaint((state = merge(state, emit("update", result))))
          )
      } else {
        initialize(namespace[key] || (namespace[key] = {}), action, name)
      }
    })
  }

This means that every action is the promise of an update (or noop if the return value is null). If the action is sync (returns a primitive value or null) then it will resolve immediately and trigger an update. If the action is async then it waits for the promise chain to resolve.. if the resolved value is not null then an update is triggered.

I actually managed to dig out our old conversations on such matters:

Not sure if this is exactly what you guys are trying to solve but it sure looks/sounds similar so might be worth some referencing!

@jorgebucaran
Copy link
Owner

@zaceno Can you share exactly what kind of code would become impossible after this change? I still can't picture what you are saying.

actions that compose async-request-actions and state-setting-actions in various ways (also like old "effects")

This patch only modifies the behavior when your return the promise. If you don't return anything, nothing happens.

@jorgebucaran
Copy link
Owner

@lukejacksonn The approach you suggest means we need to put Promise inside core, which is another issue on its own, but what's the difference between your suggestion and @Swizz's patch?

@jorgebucaran
Copy link
Owner

jorgebucaran commented Jul 18, 2017

Maybe it is too premature to merge this in. I can see myself using async actions this way, but I still don't know what the cons / cost of this new feature would be? 🤔

/cc @zaceno @lukejacksonn @selfup @Swizz @dodekeract

@lukejacksonn
Copy link
Contributor

The approach you suggest means we need to put Promise inside core

Yes it does 😌 bye bye IE11.. indeed another issue on it's own. My only argument for this is that there is nothing cool about supporting IE11 out of the box. Support could be achieved through a curated polyfill.io script tag, highlighted in the docs.

What's the difference between your suggestion and @Swizz's patch?

It takes advantage of a standardised platform feature rather than a proprietary implementation. Less upkeep and no weird limitations/workarounds like this. What @zaceno wrote in #305 (comment) describes exactly how I have found myself using actions and highlights the paradigmatic repercussions perfectly 💯👏

I still don't know what the cons / cost of this new feature would be?

TLDR; I am tempted but undecided

@jorgebucaran
Copy link
Owner

@lukejacksonn Thanks! I am still unclear about the cons myself.

Is #305 (comment) a desirable behavior?
And more importantly, what kind of code would be impossible to implement if this patch is merged?

@Swizz If you can answer some of our questions, you'll get a better chance to have this merged! 😄

@Swizz
Copy link
Contributor Author

Swizz commented Jul 19, 2017

I will try to do my best, but keep in mind this is my implementation regarding to our decision to avoid Promise in core.

  • Having to support Promise in core
    It takes advantage of a standardised platform feature rather than a proprietary implementation

Lot of people is still using Bluebird and similar, Using then that way, would it be a better choice regarding exotic Promise implementation ? IDK both are great for me

I dont understand very well the main concern here. I understand the fact we are moving from "An action that will do the request and the action that will update the state" to "An action that will make the request and update the state", but the first one is still possible. This is still possible to call an another sync action and resolve the async one to void

  • It feels like pipe/stream/coroutine territory and I am no expert.

Yes, the Promise API, in general, is designed to be used like this. At each async step, you are waiting the new step to resolve then you are moving to the next one. This is more Promise/Async related. But we are still doing that without this patch in async actions !

IDK. Maybe yes, Maybe not. Here, we are just assuming the resolved result of an async action is nothing more than the result of a sync action. The later update the state now and the first one update the state when resolved. This is the main purpose of async/await be like a sync routine but with async stuff.

  • It seems to simplify Promise syntax more than async/await

Async/await is 95% Promise and return a Promise. So I assume : Yes.

Now, Promise and Callback are very similar in term of perf. And here we are already supporting Promise/Async actions. I am just wondering : Why not resolving to an update of the state directly like a sync action ?

@jorgebucaran
Copy link
Owner

jorgebucaran commented Jul 19, 2017

@swiss @Swizz Thanks for the detailed reply, that will help figuring this out. One more thing I'd like to know, is how this change can affect existing apps as well what would be one thing we can't no longer do.

@zaceno mentioned something about "inbetween an xhr call returning", but without a code example I can't picture it.

@Swizz
Copy link
Contributor Author

Swizz commented Jul 19, 2017

I am not Swiss, but..

I have no many experiences with hyperapp to figure out.

Firstly, the one who use async actions that return something ? But this is totally a nonsense regarding actual API.
The debouce side effect will affect some one, "maybe" ?

Async which return nothing will be ignored as well and this is the current pattern, so ?

@jorgebucaran
Copy link
Owner

I am not Swiss, but..

Sorry, lol 😅

Well, let's wait for @zaceno to chime in.

@Swizz
Copy link
Contributor Author

Swizz commented Jul 19, 2017

Do I need to work on your requested changes or may I wait ?

@SkaterDad
Copy link
Contributor

I like the idea behind this PR. 👍 Half the time I forget that I can't do this already, so to me this type of change would make hyperapp more intuitive. Without needing the setData() actions after each async action, actual application bundles may end up smaller, too?

Regarding just using Promises in core... I'm not a fan of that idea. One of my main uses for hyperapp is at work, where the end users have to use IE11. Most of those projects don't use Promises or async actions at all, so requiring a Promise polyfill would be wasteful.

@zaceno
Copy link
Contributor

zaceno commented Jul 19, 2017

@Swizz @jbucaran Sorry for the delay. Don't hold anything up on account of me. I've changed my mind (or rather, realized my concerns were unfounded) and give this new behavior 👍

(@jbucaran I'd give you a code example of what I was talking about, but there's no point because I can't see any way it would be a problem)

@Swizz
Copy link
Contributor Author

Swizz commented Jul 19, 2017

@SkaterDad Larger with some bytes. I dont know why bundlesize didnt work on this PR, so I cant tell.
Do not use Promise in core will also allows the use of custom implementation of Promise not aligned with the current spec. (Only .then() is requested)

@zaceno I am glad to read this 👍 🎉

Alright, I will work on request changes later ❤️

@SkaterDad
Copy link
Contributor

SkaterDad commented Jul 19, 2017

@Swizz I know hyperapp itself will grow a tiny amount, but I was referring to our application bundles. We'll have to write less actions, and therefore ship less bytes to users.

It does seem strange that bundlesize isn't showing up in this PR. If you dig into the Travis build outputs, it shows this currently:

PASS  ./dist/hyperapp.js: 1.46kB < threshold 1.5kB gzip

@jorgebucaran
Copy link
Owner

Do I need to work on your requested changes or may I wait ?

Please do 🙏

@FlorianWendelborn
Copy link

I'll review how this affects @attach-live's codebase today. CC @fvj

@Swizz
Copy link
Contributor Author

Swizz commented Jul 20, 2017

@jbucaran @zaceno @lukejacksonn @SkaterDad

To be Promise compliant without the need of Promise in core, the only thing we do is to be Promise/A+ compliant.

And one of the big thing that define a Promise is :

A promise must provide a then method [...]
[...]
A promise’s then method accepts two arguments: promise.then(onFulfilled, onRejected)
[...]

  • 2.2.2 If onFulfilled is a function:
    • 2.2.2.1 it must be called after promise is fulfilled, with promise’s value as its first argument.
    • 2.2.2.2 it must not be called before promise is fulfilled.
    • 2.2.2.3 it must not be called more than once.

Here is our case. We are relying on the .then() existance to trust if action return a Promise or not.
And then, you are using update(data) as the onFulfilled function. Taking the fulfilled value as data.

So this patch seem compliant to Promise without the need of the Promise keyword in source.


If you want to go further, we can also be compliant with :

.all(promises: Array<Promise>).then(onFulfilled: (values: Array<any>) => any)

To be aware of actions which return an array of Promise.

And maybe we can go to be ES Observable compliant too

.subscribe(obs: Observer)
Thinking too loud. But I am an FPR Lover after all.
const { h, app } = hyperapp
const { periodic } = most

/** @jsx h */

app({
  state: { ho: 0, mi: 0, se: 0},
  view: ({ho,mi,se}, actions) => (
    <main>
      <h1>{ho>=10?ho:'0'+ho}:{mi>=10?mi:'0'+mi}:{se>=10?se:'0'+se}</h1>
    </main>
  ),
  actions: {
    clock: (state, actions) => {
      return periodic(1000).
        scan(count => count + 1, 0).
        map(count => ({
          ho: ~~(count / 3600),
          mi: ~~(count % 3600 / 60),
          se: ~~(count % 3600 % 60)
        })
    }
  },
  events: {
    loaded: (state, actions) => {
      actions.clock()
    }
  }
})

Each seconds, a new value is emitted in the stream sequence, state subscribe to the returned stream, and will be updated by the value emitted in the stream.

(I am joking, or not IDK 🤔)

@jorgebucaran
Copy link
Owner

@ngryman When you have time, please share your feedback! 🙏

Copy link
Owner

@jorgebucaran jorgebucaran left a comment

Choose a reason for hiding this comment

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

@Swizz Please rebase and update with the changes we talked about here:

Copy link

@FlorianWendelborn FlorianWendelborn left a comment

Choose a reason for hiding this comment

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

@jbucaran Looks like @attach-live will be fine with these changes. Some minor changes are needed, but overall I think this might even lead to simplified code. Would be nice if @fvj could confirm this for his projects.

That being said, I'd still like to have a way to return from actions without updating the state. Perhaps something similar to return {$: false} (some magic key that is unlikely to be a coincidence) would be good to solve this? That way we'd have sane defaults but also allow advanced functionality.

Also, what will happen with throw 'something'?

@ngryman
Copy link
Contributor

ngryman commented Jul 20, 2017

I'm very late on this one. Just dropping an idea for async stuff, why not supporting thunks instead of promises?

This has the advantage to be super easy to implement and gives much more freedom to the user: Promise, event, setTimeout, Rx, ...

For example:

app({
  state: {
    error: '',
    issues: []
  },
  actions: {
    fetch: () => (update) => {
      const request = fetch('domain.tld')
        .then(response => update({ issues: response.json() }))
        .catch(error => update({ error })
    }
  }
})

We just need to expose an update function from HyperApp that would basically merge the state and emit the update event.

@FlorianWendelborn
Copy link

FlorianWendelborn commented Jul 20, 2017

@ngryman Wouldn't this behave the same as actions.set({error}) with set: (_, __, data) => data? That's something I suggested should be the default instead of abusing return some weeks ago.

@jorgebucaran
Copy link
Owner

@ngryman Not a bad idea, but the current approach is cleaner and feels more natural. Also, I am saving returning functions for a bit later, since this is essentially how I plan to support getters using a Getter mixin.

/cc @dodekeract

@Swizz
Copy link
Contributor Author

Swizz commented Jul 21, 2017

@dodekeract In all likelihood, if an exception is thrown, the Promise attempt to call the onRejected functions. If there are not. Exception will bubble to the "main process" (the console).
You are responsible to catch exceptions : #305 (comment)


@jbucaran I tried to work on all my todolist yesterday but tiredness wins. May be during saturday night.


@ngryman I admit, thunks are good. In my mind, I only longed about a way to give the user nothing to reason about. Nothing to be responsible of.

This action is sync :

updateState(state, actions) {
  /* ... */
  return data
}

This ones are async :

async updateState(state, actions) {
  /* ... */
  return await data
}
updateState(state, actions) {
  /* ... */
  return Promise.resolve(data)
}

Nothing more. Your actions return a partial state or a promise that resolve to a partial state. Pretty easy.

@ngryman
Copy link
Contributor

ngryman commented Jul 21, 2017

@dodekeract Yes.

@jbucaran It does not feel more natural for me, but I understand your point.

@Swizz I get it, totally. What I'm discussing here is the constraint of having to use the Promise API, and only this API, for any async stuff. If I use an observer for example, or a good'ol callback, I'm forced to create a companion action and call it explicitly. Things get messy as soon as I'm not using a Promise. Unfortunately, promises are not the only async paradigm out there. Hence thunks.

They offer the ability to do whatever you want, and if you want some syntax sugar, it's quite easy to implement in userland. For example we could have some sort of thunkify function that would wrap actions into thunks and make sure returned promises are treated correctly:

// This would iterate over all actions, for the sake of the example
// and because I'm lazy, I just show what it's doing per action
const thunkify = (action) => (...args) => async (update) => {
  const res = await Promise.resolve(action(...args))
  update(res)
}

const actions = {
  updateState: async (state, actions) => Promise.resolve(data)
}

app({
  actions: thunkify(actions)
})

@jorgebucaran
Copy link
Owner

jorgebucaran commented Jul 21, 2017

@ngryman I just want to address a part of your reply.

What I'm discussing here is the constraint of having to use the Promise API, and only this API, for any async stuff.

Nope. We are only making it easier for people already using promises (which I bet are more than those using any other paradigm, but that's besides the point).

Now, we are not making it worse for people using good ol' callbacks.

see codepen here

app({
  state: 0,
  view: (state, actions) =>
    <main>
      <h1>{state}</h1>
      <button onclick={actions.upLater}>UP 1s later</button>
    </main>,
  actions: {
    upLater: (state, actions) => {
      setTimeout(actions.up, 1000)
    },
    up: state => state + 1
  }
})

@jorgebucaran
Copy link
Owner

jorgebucaran commented Jul 21, 2017

@ngryman I think we can still consider thunks, though. That is, if we can find a way to make the concept more approachable. For example, not using that horrible word anywhere in the docs to begin with.

Can you also show an example implementation of app.js? You said:

This has the advantage to be super easy to implement

Please show me.

@Swizz
Copy link
Contributor Author

Swizz commented Jul 21, 2017

As said by @jbucaran here, we are not changing the old way to do.

We are making possible to move from the following pattern

actions: {
  setUser(state, actions, user) {
    return { user }
  },
  async handleUser(state, actions, id) {
    const user = await fetch(id)
    actions.setUser(user)
  }
}

To this one

actions: {
  async handleUser(state, actions, id) {
    const user = await fetch(id)
    return { user }
  }
}

But the first one, is still possible. You can easily keep a sync action that will be responsible of updating the state and the async that call the sync one after its job will be done.

The main goal is only to sanitize the async action api to look the same as sync one. We can easily drop out the need to call an action that only be a setter.

EDIT: Run prettier over the code by @jbucaran.

@ngryman
Copy link
Contributor

ngryman commented Jul 21, 2017

@jbucaran

Now, we are not making it worse for people using good ol' callbacks.

I didn't say that :) I said that with this patch I will still have to:

I'm forced to create a companion action and call it explicitly.

Here companion action being up. I just mean that if for any reason I can't use a Promise, I'm stuck with the current behavior, so this patch does not help me :)


Please show me.

function update(data) {
  if (data != null) {
    repaint((state = merge(state, emit("update", data))))
  }
}

if (result != null && typeof result == "function") {
  result(update)
}
else {
  update(result)
}

@Swizz Yup, again I totally 100% get the vision you have. I'm just confronting it with other use cases and the fact that this is an opinionated vision.

@ngryman
Copy link
Contributor

ngryman commented Jul 21, 2017

@Swizz gave me an idea: why not implementing both? That's so easy, and we would:

  1. Have a first-class promise support
  2. Support any other paradigms
if (result != null && typeof result.then == "function") {
  result.then(update)
}
else if (result != null && typeof result == "function") {
  result(update)
}
else {
  update(result)
}

@jorgebucaran
Copy link
Owner

@ngryman @Swizz This is the smallest I was able to make it.

if (
  result != null &&
  typeof (data = result.then || result) === "function"
) {
  data.call(result, update)
} else {
  update(result)
}

@jorgebucaran
Copy link
Owner

The .call is needed because of promises 😞. This will also end up binding the "thunk" to itself, probably harmless.

@jorgebucaran
Copy link
Owner

In order to implement getters, the action must return result(update) when result is a "thunk".

@Swizz
Copy link
Contributor Author

Swizz commented Jul 21, 2017

@jbucaran I didnt understand your last comment, can you provide an example ?

@jorgebucaran
Copy link
Owner

@Swizz

if (result == null) {
} else if (typeof result == "function") {
  result = result(update)
} else if (typeof result.then == "function") {
  result.then(update)
} else {
  update(result)
}

return result

@Swizz
Copy link
Contributor Author

Swizz commented Jul 21, 2017

This will not be compatible with your bytes safety example ?

if (
  result != null &&
  typeof (data = result.then || result) === "function"
) {
  result = data.call(result, update)
} else {
  update(result)
}

return result

@jorgebucaran
Copy link
Owner

jorgebucaran commented Jul 21, 2017

Yep, unfortunately. I see, what you mean, you are right! 👍

But, that would be equivalent to:

result = result.then(update)
...
return result

I thought we wanted to return the promise not the promise.then(result) promise. Very confusing.

@Swizz Does it even make any difference here?

EDIT: Unfortunately, it does make a difference.

result = result(update)
} else if (typeof result.then == "function") {
result.then(update)
} else {
Copy link

@jamen jamen Jul 21, 2017

Choose a reason for hiding this comment

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

Could we make this else if (result != null) and then remove the if (result == null) above?

Copy link

Choose a reason for hiding this comment

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

Oh I see, I think you were discussing a smaller impl above. Disregard.

@jorgebucaran jorgebucaran merged commit 54e7dc3 into jorgebucaran:master Jul 24, 2017
@jorgebucaran
Copy link
Owner

@Swizz 🎉

@Swizz Swizz deleted the actions-promise-result branch December 26, 2017 15:06
jorgebucaran pushed a commit that referenced this pull request Jan 7, 2018
Thunks allow users to return a function from an action,
then we will call such function passing the same update
function we use enqueue repaints/renders. 

When using a thunk, it's now up to the user to initiate an 
update or not.
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.

10 participants