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

Async/Await Support #176

Merged
merged 10 commits into from
Nov 16, 2017
Merged

Async/Await Support #176

merged 10 commits into from
Nov 16, 2017

Conversation

mikew
Copy link
Contributor

@mikew mikew commented Oct 2, 2017

This PR adds support for native async / await functions.

  • There is a special handler when the promise is a function with a constructor named AsyncFunction (as they are in Node and modern browsers)
  • In order to support compiled async/await code, it also calls the function and checks if the result is a promise.
  • When calling the function, it passes dispatch and getState from redux, just like redux-thunk.

So a (maybe silly) example from the guide goes from this:

// fulfilled promise
const foo = () => {
  return dispatch => {

    return dispatch({
      type: 'TYPE',
      payload: new Promise()
    }).then(() => dispatch(bar()));
  };
}

to this:

const foo = () => ({
  type: 'TYPE',
  async payload(dispatch) {
    dispatch(bar());
  }
});

@mikew
Copy link
Contributor Author

mikew commented Oct 2, 2017

Also while working on this I noticed your instanbul config is off, and these builds are silently failing: https://travis-ci.org/pburtchaell/redux-promise-middleware/builds/282257458#L608-L651

Also, this needs yet another babel config so that it stops compiling async/await to its own thing. I've tested that locally but haven't included it in the PR.

@pburtchaell
Copy link
Owner

@mikew Thanks! I'll take a look into this later this week. 👍

@pburtchaell pburtchaell added enhancement in progress Issues in progress by the author(s) labels Oct 8, 2017
@pburtchaell pburtchaell self-requested a review October 8, 2017 13:55
@pburtchaell
Copy link
Owner

pburtchaell commented Oct 8, 2017

I noticed your instanbul config is off, and these builds are silently failing

This is now fixed on master. Would you mind rebasing your branch to master, @mikew?

@mikew
Copy link
Contributor Author

mikew commented Oct 8, 2017

Can do!

* origin/master:
  Remove known vulnerabilities badge
  Fix Istanbul tests
  Update dev dependencies
@mikew
Copy link
Contributor Author

mikew commented Oct 8, 2017

I merged instead of rebasing, since a rebase would require push --force.

@pburtchaell
Copy link
Owner

That's fine! Next step here is to add some documentation. Could you add a guide to the docs directory? After that, I'll start on a code review. 👍

@pburtchaell
Copy link
Owner

Just a heads up that you might want to rebase/merge again. I recently pushed some updates to the docs.

…-middleware into async-await-support

* 'master' of https://github.com/pburtchaell/redux-promise-middleware:
  Rename file
  Update documentation
  Add comments
  Update examples
  Reorder gitignore and npmignore
@mikew
Copy link
Contributor Author

mikew commented Oct 9, 2017

Should I rewrite the chaining actions docs since it's now handled internally?

@mikew
Copy link
Contributor Author

mikew commented Oct 9, 2017

Actually the semantics are just different enough in a couple of examples that, so I think I'll leave them be.

@pburtchaell
Copy link
Owner

Should I rewrite the chaining actions docs since it's now handled internally?

No, I don't think this change warrants removing any content/guides from the docs. I think there should just be a new section for use with async functions. This, and examples where ever else it would make sense. Would you agree?

@mikew
Copy link
Contributor Author

mikew commented Oct 12, 2017

Agreed, and sorry it's taking me so long.

@robhowell
Copy link

Hi @mikew - this looks like a really useful change, can't wait until it's merged! Would you like any help writing the documentation?

@mikew
Copy link
Contributor Author

mikew commented Oct 30, 2017

Oh gosh that would be lovely. I've added what I have to the PR: https://github.com/pburtchaell/redux-promise-middleware/pull/176/files#diff-596e1efe9b79c33a5a05e9b0fe30f33f

What's there may even suffice.

@mikew
Copy link
Contributor Author

mikew commented Oct 30, 2017

Something that I didn't mention in there, is that when you don't use an async function we will still use that value for the payload.

@tomatau
Copy link
Collaborator

tomatau commented Oct 30, 2017

I'd like to see some clarity about the semantics between the return value (resolve of the async func) and the thunk like dispatch. What's the order that these will dispatch in according to the middleware.

const foo = () => ({
  type: 'TYPE',
  async payload(dispatch) {
    const filteredResult = await getResultAsync()
      .then(result => {
        dispatch(foo(result))
        return filter(result)
      })
    dispatch(bar())
    return filteredResult
  }
});

Here I'd expect 4 actions to be dispatched

  • pending of TYPE
  • fulfilled of TYPE
  • FOO
  • BAR

What would the order be?

@mikew
Copy link
Contributor Author

mikew commented Oct 30, 2017

Not sure why you'd expect FOO and BAR to be dispatched after TYPE_FULFILLED, if that were the case you'd never be able to use the resolved value from the function in the _FULFILLED action.

@tomatau
Copy link
Collaborator

tomatau commented Oct 30, 2017

@mikew I don't expect that order, I'm asking what the order would be.

But to play devil's advocate, how would I dispatch an action after fulfilled though? If I did want to?

@mikew
Copy link
Contributor Author

mikew commented Oct 30, 2017

The phrase "Here I'd expect 4 actions to be dispatched" is a little confusing then :)

It would be:

  • TYPE_PENDING
  • FOO
  • BAR
  • TYPE_FULFILLED, with a payload of filteredResult

@tomatau
Copy link
Collaborator

tomatau commented Oct 30, 2017

I came with absolutely no expectations about the order and used an unordered list!... I'm just a wide eyed curious cat asking! I couldn't put them in an order when listing them!

@tomatau
Copy link
Collaborator

tomatau commented Oct 30, 2017

It would be:
TYPE_PENDING
FOO
BAR
TYPE_FULFILLED, with a payload of filteredResult

I feel like the PENDING would happen after the FOO and the BAR because the payload would be invoked by the middleware before the pending was dispatched?...

https://github.com/mikew/redux-promise-middleware/blob/986f650cea8e01b7ff6f0e7905b8fe04822aeac7/src/index.js#L45

@mikew
Copy link
Contributor Author

mikew commented Oct 30, 2017

But to play devil's advocate, how would I dispatch an action after fulfilled though? If I did want to?

You can't, you'd have to use redux-thunk, which gives you 100% control of the flow of actions. I don't think that's any different than this middleware currently works.

@tomatau
Copy link
Collaborator

tomatau commented Oct 30, 2017

You can't, you'd have to use redux-thunk, which gives you 100% control of the flow of actions. I don't think that's any different than this middleware currently works.

Yes, these latest versions work that way...

grumbles to self and continues to support version 2

@mikew
Copy link
Contributor Author

mikew commented Oct 30, 2017

I feel like the PENDING would happen after the FOO and before the BAR because the payload would be invoked by the middleware before the pending was dispatched?...

That's actually a good catch, and I didn't write any specs to verify the behaviour in that case.

@mikew
Copy link
Contributor Author

mikew commented Oct 30, 2017

Yes, these latest versions work that way...

Are you stores dependant on data existing in other stores? That seems like a recipe for disaster. I'd move that to the component layer, and if the data that it requires from two stores isn't available yet, well, you're still loading.

@pburtchaell
Copy link
Owner

pburtchaell commented Nov 5, 2017

const foo = () => ({
  type: 'TYPE',
  async payload(dispatch) {
    const filteredResult = await getResultAsync()
      .then(result => {
        dispatch(foo(result))
        return filter(result)
      })
    dispatch(bar())
    return filteredResult;
  }
});

I feel like the PENDING would happen after the FOO and before the BAR because the payload would be invoked by the middleware before the pending was dispatched?

@tomatau I think it makes sense to dispatch the PENDING action at the time the promise is instantiated (or the async function called). In this case, (unless I'm mistaken) the promise is instantiated when the async payload(dispatch) function is called.

Given the design principle async action objects describe the promise state, this order makes sense to me.

  • TYPE_PENDING
  • FOO
  • BAR
  • TYPE_FULFILLED (or TYPE_REJECTED)

Let's just make sure this is well documented!

@tomatau
Copy link
Collaborator

tomatau commented Nov 5, 2017

@tomatau I think it makes sense to dispatch the PENDING action at the time the promise is instantiated (or the async function called). In this case, (unless I'm mistaken) the promise is instantiated when the async payload(dispatch) function is called.

The PR would need to be changed for this to be the case.

I feel like this PR/feature is a bit confusing even in a basic example and also a bit restrictive in what you can do with it all in order to support something that kinda looks cool? Unless I'm missing something?

@pburtchaell
Copy link
Owner

pburtchaell commented Nov 5, 2017

The PR would need to be changed for this to be the case.

Yep, lines 44-45 need to be moved to call the async function after the pending action is dispatched on lines 106-110.

I feel like this PR/feature is a bit confusing even in a basic example

In my opinion, at a bare minimum, it does make sense to support async functions, which is where this PR started.

I do agree there is some complexity/confusion, though. I think that comes from (1) providing dispatch and getState as arguments to the async function and (2) understanding the order in which actions are dispatched.

@mikew
Copy link
Contributor Author

mikew commented Nov 5, 2017

The problem is with transpiled async functions. If they're not transpiled, we can tell ahead of time if it's an async function.

But if it's transpiled, we need to call the function to tell if it's async or not. And if we call the function, any actions it dispatches will be dispatched before we can do _PENDING.

@mikew
Copy link
Contributor Author

mikew commented Nov 5, 2017

So I can see a few ways of handling this:

  • Keep it as is, this way async functions would be treated the same as functions that return a promise, just like Node does
  • Have special handling of async functions, this would mean async functions are treated differently than functions that return a promise
  • Ditch the dispatch/getState bits and rely on redux-thunk

@pburtchaell
Copy link
Owner

The problem is with transpiled async functions. If they're not transpiled, we can tell ahead of time if it's an async function.

What's the purpose of this comparison, in that case?

(promise.constructor.name === 'AsyncFunction')

Full disclaimer, I don't use async functions in practice and don't know the ins and outs of browser support and how it's transpiled (assuming this is with Babel?).

I can see a few ways of handling this [...] Ditch the dispatch/getState bits and rely on redux-thunk

What do you think of removing dispatch and getState from this PR and only implementing support for async functions?

const foo = () => ({
  type: 'TYPE',
  async payload() {
    const result = await doSomething();
    return result;
  }
});

This will help move this PR along IMO.

In another PR and/or issue, we can talk about how to introduce dispatch and getState. I should note this is somewhat related to issue #67. It would be great if we could marry this discussion with the discussion there.

@mikew
Copy link
Contributor Author

mikew commented Nov 7, 2017

Yes, Babel or TypeScript will transpile asynchronous functions to regular old functions that return a promise. If they aren't transpiled, you can actually determine if they are asynchronous or not ahead of time, which is what that line is doing.

In retrospect, that line is probably useless, because the other promise check will work. But if you ever wanted to treat them differently, that would be the place.

I'm cool with ditching the dispatch/getState bits, but I'm also one of the people who was never affected by #67, and would just solve it another way.

@pburtchaell
Copy link
Owner

pburtchaell commented Nov 13, 2017

if (typeof promise === 'function') {
  const functionResult = promise();

  if (isPromise(functionResult)) { 
    ..
  }
}

This change makes sense: check if the payload is a function. If true, call the function and check if the return is a promise. I have two concerns though.

First, the typeof promise === 'function' comparison works, but—to play devils advocate here—it works only under the assumption a function will always be an async function. Yes, it is an antipattern to have a synchronous function as the payload, but I think our implementation should be robust enough to handle this case.

// Yes, this is an antipattern...
// ...but, if someone does implement it, it would be called and have unexpected results.
const foo = () => ({
  type: 'TYPE',
  payload() {
    ...
  }
});

Second, we must call the async function to see if a promise is returned.

const functionResult = promise()

This means the function is called before the pending action is dispatched. This this an issue @tomatau brought up earlier when he asked about the order in which actions are dispatched. I don't think this makes sense.

Given this silly example (using Redux Thunk):

const fooAction = () => {
  return dispatch => {

    return dispatch({
      type: 'FOO',
      async payload() {
        dispatch({
          type: 'BAR'
        });

        return true;
      }
    });
  };
};

The dispatched action order is:

  • BAR
  • FOO_PENDING
  • FOO_FULFILLED

EDIT: I should mention, yes, this is the current behavior, but I think if we implement async functions—where someone is more likely to dispatch multiple async actions in a chain—we should change the order in which the actions are dispatched.

So, in conclusion, is there another way to can detect an async function? The (promise.constructor.name === 'AsyncFunction') comparison would be perfect, but as you mentioned, it isn't supported by browsers.

If there isn't another way, I worry we might not be able to implement this until async functions are supported by browsers.

@pburtchaell
Copy link
Owner

Also, I'm sorry for the delay! I was away this past week and just getting back.

@mikew
Copy link
Contributor Author

mikew commented Nov 13, 2017

Yes, it is an antipattern to have a synchronous function as the payload, but I think our implementation should be robust enough to handle this case.

That's what this covers:

https://github.com/mikew/redux-promise-middleware/blob/6d2d0ec5f7d915d3cbeb1e4964fa8567a8825b6f/src/index.js#L51

So, in conclusion, is there another way to can detect an async function?

Nope. In fact it looks like checking for AsyncFunction is a faux pas: tc39/proposal-async-await#78

@pburtchaell
Copy link
Owner

Okay. This is good to merge into the master branch, but I want to do some cleanup work and review all of the tests before it's released to npm. I'll keep you posted on the status @mikew and also leave #175 open until it's released!

Thanks for the work on this and for keeping up with the comments. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in progress Issues in progress by the author(s)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants