- Notifications
You must be signed in to change notification settings - Fork90
Improvement/add context to async options#247
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:next
Are you sure you want to change the base?
Improvement/add context to async options#247
Conversation
codesandbox-cibot commentedJan 21, 2020 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
This pull request is automatically built and testable inCodeSandbox. To see build info of the built libraries, clickhere or the icon next to each commit SHA. Latest deployment of this branch, based on commit2a45ae2:
|
| package-lock.json | ||
| yarn.lock | ||
| .vscode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This should probably be in yourglobal gitignore too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I've added it because the project explicitly instructs you to create settings for.vscode in theCONTRIBUTING.md.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Yeah I agree we should add it. Just wanted to let you know you can use a global gitignore file for it too.
Impressive work! I'm going to need to find some time to take a closer look at this. Very nice to see you updated all the docs too ❤️ |
packages/react-async/src/context.ts Outdated
| KEYS_OF_FETCHOPTIONS.forEach(k=>{ | ||
| deletecontext[k] | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
It seems this means it is impossible to have keys in context that conflict withAsyncOptions ...
Shouldn't it delete entries fromoptions but not fromoptions.context ?
Given that this PR is breaking change maybe it should only support passing arguments throughoptions.context to bypass this processing ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
+1 for only passing arguments throughoptions.context. It'll be a pretty big change but for good reason. Still having the option to also use rest props onAsyncProps only complicates things.
I would also like to see thecontext arg onrun to be merged on top ofoptions.context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
@ghengeveld so just to be sure you want the api to change from:
<AsyncpromiseFn={loadPlayer}playerId={1}suspense={true}>useAsync(loadPlayer,{playerId:1,suspense:true});// AltuseAsync({promiseFn:loadPlayer,playerId:1,suspense:true});
To:
<AsyncpromiseFn={loadPlayer}context={{playerId:1}}suspense={true}>useAsync(loadPlayer,{context:{playerId:1},suspense:true});// AltuseAsync({promiseFn:loadPlayer,context:{playerId:1},suspense:true});
I think this would be a great change.
I'm also for making thecontext pure, as in nothing gets magically added to it or deleted from it. This will make the code more readable.
But I'd also recommend overloadinguseAsync in the future to allow:
useAsync(loadPlayer,{playerId:1},{suspense:true});
As I think this would result in the most readable api.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
If we are changing APIs here anyways I'm all for moving from positional parameters to named parameters (one parameter with a keyed object).
So I'd prefer to be only left with
<AsyncpromiseFn={loadPlayer}context={{playerId:1}}suspense={true}>useAsync({promiseFn:loadPlayer,context:{playerId:1},suspense:true});
as that will make it more readable at a glance, allow for easier future expansion and will keep the typings more readable/less error-prone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I'm in favor of dropping the overloaded signatures and only support the single object argument. The shorthand where you pass in the async function as first argument was added to support some very basic use cases where it'd be theonly argument, but it's not worth the extra complexity in code and API consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
By the way we're going to have to write a codemod for this, because it would otherwise require quite a bit of manual work to upgrade.
ghengeveldJan 27, 2020 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I'm also for making the context pure, as in nothing gets magically added to it or deleted from it. This will make the code more readable.
Agreed. People can always spread it themselves. This meansdeferFn will always only receive thecontext fromrun, and thecontext fromoptions is only passed topromiseFn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I've updated the code so:
- There is no more overload of useAsync.
- There is no more magical context spreading / building.
ef374bf to57d2143Comparecodecovbot commentedJan 27, 2020 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Codecov Report
@@ Coverage Diff @@## next #247 +/- ##==========================================+ Coverage 98.58% 98.59% +<.01%========================================== Files 8 8 Lines 424 427 +3 Branches 148 148 ==========================================+ Hits 418 421 +3 Misses 6 6
Continue to review full report at Codecov.
|
c3dd34f to131646eCompareThe `promiseFn` and the `deferFn` have been unified. They now share thefollowing signature:```tsexport type AsyncFn<T, C> = ( context: C | undefined, props: AsyncProps<T, C>, controller: AbortController) => Promise<T>```Before the `deferFn` and `promiseFn` had this signature:```tsexport type PromiseFn<T> = (props: AsyncProps<T>, controller: AbortController) => Promise<T>export type DeferFn<T> = ( args: any[], props: AsyncProps<T>, controller: AbortController) => Promise<T>```The big change is the introduction of the `context` parameter. The ideabehind this parameter is that it will contain the parameters which arenot known to `AsyncOptions` for use in the `promiseFn` and `asyncFn`.Another goal of this commit is to make TypeScript more understandingof the `context` which `AsyncProps` implicitly carries around. Beforethis commit the `AsyncProps` accepted extra prop via `[prop: string]: any`.This breaks TypeScript's understanding of the divisions somewhat. Thisalso led to missing types for `onCancel` and `suspense`, which have beenadded in this commit.To solve this we no longer allow random extra properties that are unknownto `AsyncProps`. Instead only the new `context` of `AsyncProps` is passed.This means that the `[prop: string]: any` of `AsyncProps` is removed thismakes TypeScript understand the props better.The other big change of this commit that `useAsync` no longer supportsan overload. This means that the user can no longer do:```tsconst state = useAsync(loadPlayer, { context: { playerId: 1 } })```But have to be more explicit:```tconst state = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } })```These changes are of course a breaking change.Also now compiling TypeScript on `yarn test` this should prevent typeerrors from slipping in.Closes:async-library#246WIP: Trying to fix buildasdf131646e to2a45ae2Comparemoroine commentedJul 21, 2020
What's the status of this PR? |
In need of a close review. It's been a while since I looked at it. |
LRNZ09 commentedAug 28, 2020
Just my 2c here: I like the fact that in v10 you can use the short version of the |
LRNZ09 commentedNov 12, 2020
Any update on this? |
ZebulanStanphill left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I have some experience with TypeScript, so I decided to take a look at this PR.
I found some type weirdness in packages/react-async/src/useAsync.tsx, as well as some potential simplifications and wording improvements that could be made in other files. Also, this PR needs a rebase.
| -[`promise`](#promise) An already started Promise instance. | ||
| -[`promiseFn`](#promisefn) Function that returns a Promise, automatically invoked. | ||
| -[`deferFn`](#deferfn) Function that returns a Promise, manually invoked with`run`. | ||
| -[`context`](#context) The first argument for the`promise` and`promiseFn` function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
| -[`context`](#context) The first argument for the`promise` and`promiseFn` function. | |
| -[`context`](#context) The first argument for the`promiseFn` or`deferFn` function. |
| A function that returns a promise. It is automatically invoked in`componentDidMount` and`componentDidUpdate`. The function receives all component props\(or options\) and an AbortController instance as arguments. | ||
| >Be aware that updating`promiseFn` will trigger it to cancel any pending promise and load the new promise. Passing an inline (arrow) function will cause it to change and reload on every render of the parent component. You can avoid this by defining the`promiseFn` value**outside** of the render method. If you need to pass variables to the`promiseFn`, pass themas additionalpropsto`<Async>`, as`promiseFn` will be invoked with these props. Alternatively you can use`useCallback` or[memoize-one](https://github.com/alexreardon/memoize-one) to avoid unnecessary updates. | ||
| >Be aware that updating`promiseFn` will trigger it to cancel any pending promise and load the new promise. Passing an inline (arrow) function will cause it to change and reload on every render of the parent component. You can avoid this by defining the`promiseFn` value**outside** of the render method. If you need to pass variables to the`promiseFn`, pass themvia the`context`propsof`<Async>`, as`promiseFn` will be invoked with these props. Alternatively you can use`useCallback` or[memoize-one](https://github.com/alexreardon/memoize-one) to avoid unnecessary updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
| >Be aware that updating`promiseFn` will trigger it to cancel any pending promise and load the new promise. Passing an inline (arrow) function will cause it to change and reload on every render of the parent component. You can avoid this by defining the`promiseFn` value**outside** of the render method. If you need to pass variables to the`promiseFn`, pass them via the`context`props of`<Async>`, as`promiseFn` will be invoked with these props. Alternatively you can use`useCallback` or[memoize-one](https://github.com/alexreardon/memoize-one) to avoid unnecessary updates. | |
| >Be aware that updating`promiseFn` will trigger it to cancel any pending promise and load the new promise. Passing an inline (arrow) function will cause it to change and reload on every render of the parent component. You can avoid this by defining the`promiseFn` value**outside** of the render method. If you need to pass variables to the`promiseFn`, pass them via the`context`prop of`<Async>`, as`promiseFn` will be invoked with these props. Alternatively you can use`useCallback` or[memoize-one](https://github.com/alexreardon/memoize-one) to avoid unnecessary updates. |
| The difference is the idea of having a`context`, the context will contain all parameters | ||
| to`AsyncProps` which are not native to the`AsyncProps`. Before you could pass any parameter | ||
| to`AsyncProps` and it would pass them to the`deferFn` and`promiseFn`, now you need to use | ||
| the`context` instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
| The difference is the idea of having a`context`, the context will contain all parameters | |
| to`AsyncProps` which are not native to the`AsyncProps`. Before you could pass any parameter | |
| to`AsyncProps` and it would pass them to the`deferFn` and`promiseFn`, now you need to use | |
| the`context` instead. | |
| The difference is the introduction of a`context` parameter. The context will contain all parameters | |
| to`AsyncProps` which are not native to`AsyncProps`. Before you could pass any parameter | |
| to`AsyncProps` and it would pass them to the`deferFn` and`promiseFn`; now you need to put them in the`context` instead. |
| ```jsx | ||
| constMyComponent= ()=> { | ||
| const {data,error,isPending }=useAsync(loadPlayer,options) | ||
| const {data,error,isPending }=useAsync({ promiseFn:loadPlayer,context: { playerId:1 } }) | ||
| // ... | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This example no longer makes any sense if you're going to just remove the shorthand version, so this entire section should be removed.
| ``` | ||
| As you can see, the`deferFn` is invoked with 3 arguments:`args`,`props` and the AbortController.`args` is anarray | ||
| As you can see, the`deferFn` is invoked with 3 arguments:`context`,`props` and the AbortController.`context` is anobject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Oxford comma.
| As you can see, the`deferFn` is invoked with 3 arguments:`context`,`props` and the AbortController.`context` is an object | |
| As you can see, the`deferFn` is invoked with 3 arguments:`context`,`props`, and the AbortController.`context` is an object |
| count, | ||
| watch:count, | ||
| context:{ | ||
| a:1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
What's the purpose thea property in this test?
| constisMounted=useRef(true) | ||
| constlastArgs=useRef<any[]|undefined>(undefined) | ||
| constlastOptions=useRef<AsyncOptions<T>>(options) | ||
| constlastArgs=useRef<C|undefined>(undefined) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Shouldn't this just be:
| constlastArgs=useRef<C|undefined>(undefined) | |
| constlastArgs=useRef<C>(undefined) |
I haven't tested, but I think this may remove the need to explicitly cast toC in a few places.
| *@returns {AsyncState<T, FetchRun<T>>} | ||
| */ | ||
| functionuseAsyncFetch<T>( | ||
| functionuseAsyncFetch<T,C>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
The typing for this function is kinda wack.
First of all, theinit parameter is using aRequestInit type that doesn't seem to actually exist. I think the type parameterC here should replace every instance ofRequestInit... or rather, perhapsC should just be renamed toRequestInit here?
Second, the 3rd argument's type probably shouldn't beFetchOptions, but rather a subset ofFetchOptions (that may not need the 2nd type parameterC at all), because passing in an object containingpromiseFn ordeferFn props doesn't make any sense in this context, yet it's technically allowed by the types here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I foundRequestInit defined innode_modules/@types/react-native/globals.d.ts. Appears to be a global type which should explain why it isn't declared in this file.
And I think C works just fine here as it's the type for theContext rather than the inits. So, it probably shouldn't replace any instance ofRequestInit.
| constpromiseFn=useCallback( | ||
| (_:AsyncOptions<T>,{ signal}:AbortController)=>{ | ||
| (context:C,_:AsyncOptions<T,C>,{ signal}:AbortController)=>{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Since the 2nd parameter's name starts with an underscore to indicate it isn't used, the 1st one should do the same for consistency/clarity.
| (context:C,_:AsyncOptions<T,C>,{ signal}:AbortController)=>{ | |
| (_context:C,_options:AsyncOptions<T,C>,{ signal}:AbortController)=>{ |
Also, my other comment aboutC andRequestInit being the same thing applies here as well.
Dysron commentedJan 6, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Any activity on this still? Looks like a great update to the package! |
Description
The
promiseFnand thedeferFnhave been unified. They now share the following signature:Before the
deferFnandpromiseFnhad this signature:The difference is the idea of having a
context, the context will contain all parametersto
AsyncPropswhich are not native to theAsyncProps. For example:In the above example the context would be
{playerId: 1}.This means that you know need to expect three parameter for the
promiseFninstead of two.So before in
< 10.0.0you would do this:In
11.0.0you need to account for the three parameters:For the
deferFnthis means no longer expecting an array of arguments but instead a singular argument.The
runnow accepts only one argument which is a singular value. All other arguments torunbutthe first will be ignored.
So before in
< 10.0.0you would do this:In
11.0.0you need to account for for the parameters not being an array:Breaking changes
Yes
Checklist
Make sure you check all the boxes. You can omit items that are not applicable.
<Async>anduseAsync()