-
Notifications
You must be signed in to change notification settings - Fork 65
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
Hooks API and separate packages #129
Conversation
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.
Basically just questions
return { cancelKeys, requestActions }; | ||
}; | ||
|
||
const useMemoizedActions = (mapPropsToConfigs, props, callback) => { |
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 custom hook feels like an implementation detail of the useEffect
hook in useMultiRequest
.
I'd like to spend a bit of time looking at this case.
const promise = reduxDispatch(requestAsync(action)); | ||
|
||
if (promise) { | ||
pendingRequests.current.add(getQueryKey(action)); |
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.
So this is the awkward thing to have in the dependency list if you were using useCallback
instead 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.
Yeah no idea how to do this without using refs. Don't think it's a problem do you?
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.
No it's not. I was just trying to understand the need for the custom hook.
return () => { | ||
[...pendingRequests.current].forEach(dispatchCancelToRedux); | ||
}; | ||
}, [dispatchCancelToRedux, dispatchRequestToRedux, requestReduxActions]); |
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.
As discussed I'm not sure it's worth it just to take advantage of the built in dependency comparison.
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 did do some research and this is similar to how urql and react-apollo-hooks implement their hooks. They memoize the request object or configuration based on the graphql key and then use in a useEffect dependency.
urql: https://github.com/FormidableLabs/urql/blob/master/src/hooks/useRequest.ts
react-apollo-hooks: https://github.com/trojanowski/react-apollo-hooks/blob/e590e8f21f1ae4871d641d9681f7932433c8daca/src/useQuery.ts#L126
Also I've been thinking about this more I'm finding a hard time thinking how inlining the caching would make the code easier to read. Do you mind elaborating on your feelings 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 did do some research and this is similar to how urql and react-apollo-hooks implement their hooks. They memoize the request object or configuration based on the graphql key and then use in a useEffect dependency.
It makes me a lot happier that other people are doing it. I'll take a look at those examples.
Also I've been thinking about this more I'm finding a hard time thinking how inlining the caching would make the code easier to read. Do you mind elaborating on your feelings here?
I think that understanding referential changes (and therefore when hooks are running) is going to be one of the core difficulties devs have with hooks. When you have a custom hook like useConstCallback
that does something different from standard examples it makes that problem more complicated.
I guess what I was trying to do was come up with something that was less "hooky" and maybe more familiar.
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 see the memoization in those examples but they're using all the standard patterns - mostly just useMemo
.
They don't seem to have needed to come up with anything like useConstCallback
or used refs in that way.
const mutate = React.useCallback(() => { | ||
dispatchMutationToRedux({ | ||
...requestReduxAction, | ||
force: true, |
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.
Could you pull this up into transformQueryConfigToAction
and drop this useCallback
?
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.
There still needs to be a useCallback though regardless, right?
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.
Yes, but it should only need to be the one on line 29.
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.
You still need a callback that is bound to the current query config though.
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 is what I mean: https://github.com/amplitude/redux-query/compare/v3.0...v3.0-will?expand=1
const transformQueryConfigToAction = useConstCallback(queryConfig => { | ||
return { | ||
...queryConfig, | ||
unstable_preDispatchCallback: finishedCallback, |
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 don't remember the significance of this. Something to do with trying to keep the props of the component sane with updates coming from different sources?
Does it still work and make sense in a hooks/concurrent world?
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's to avoid sending cancelation requests in cases when the query was just finished.
If you don't use a sync callback and instead use promises, the then
callback will trigger as a micro-task after the redux store updated. If React re-renders the component/hook between those two events and the query keys change, then the component/hook's internal state will still indicate that the query is still in-flight and therefore dispatch out a cancelation action.
I'm not confident how hooks or concurrent could impact this. My guess is it may change the race to make this not as much of an issue as React might be more likely to delay re-rendering. Given that, I think it's best to keep the callback as-is.
Tangential question – I wonder if we should make the "Trying to cancel a request that is not in flight" warning dev-only (or completely drop it?). It's not such a bad thing to dispatch a cancelation action, right?
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.
Given that, I think it's best to keep the callback as-is.
👍
Tangential question – I wonder if we should make the "Trying to cancel a request that is not in flight" warning dev-only (or completely drop it?). It's not such a bad thing to dispatch a cancelation action, right?
Yea, that sounds fine.
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.
Based on those answers lgtm
@ManThursday updates from today:
New to dos:
|
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.
New changes lgtm
Would be nice to refine some of the any
s but presumably that's not possible for most of them.
What about Typescript types? TS is seemingly taking over these days, so even Facebook has rewritten some of their code to TS. |
}; | ||
|
||
export const optimisticUpdateEntities = ( | ||
optimisticUpdate: OptimisticUpdate, |
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 think it's better to have these ones written the same way as the ones here: https://github.com/amplitude/redux-query/pull/129/files#diff-fe52475398e3e004625a25aaf3263de2R6 (with defaults)
return pendingQueries; | ||
}; | ||
|
||
const pick = (source: { [key: string]: any }, keys: Array<string>): { [key: string]: any } => { |
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 general strategy in this project is to use lodash functions (citing Ryan). Why is this custom implementation here? I believe there should be a consistency - if Lodash is a preferrable lib - then it should be used, and if custom implementations are required - why not to get rid of Lodash alltogether (which I believe is a good idea, as it would save about 30% of lib size).
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.
As I've migrated everything to flow I have reversed my decision on that. Getting flow to work well with other libraries is a pain (unless it's something like idx that just works automatically).
My thinking on this has also evolved quite a bit. I think your original point about bundle size is valid, and generally shedding dependencies, if not really necessary, is a good thing.
}; | ||
|
||
const isStatusOK = status => status >= 200 && status < 300; | ||
const isStatusOk = (status: ?Status): boolean => | ||
status !== null && status !== undefined && status >= 200 && status < 300; |
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.
undefined
check is not necessary here, as undefined < 200 === false && undefined > 300 === false
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.
Flow is picky here and I prefer the explicitness. Although reading it now I think I'd prefer to write it like typeof status === 'number' &&
let returnValue; | ||
const { getQueryKey, ...config } = { ...defaultConfig, ...customConfig }; | ||
const config = { ...defaultConfig, ...customConfig }; | ||
|
||
switch (action.type) { |
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 code would be much more readable if every switch
branch had been extracted to a separate 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.
Agreed! I think it's ok for now but after this is merged and you'd like to take a stab at refactoring it, I think that'd be a good change.
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.
Sure thing - I'd like :)
@karevn We use flow at Amplitude and although it's not the popular choice today, we have no plans to move. That's why my main focus was flow, but I don't see any reason why we can't support both. Would you be up for contributing typescript types? |
OK, I'll start writing Typescript typings to publish them via DefinitelyTyped then. |
ec7f0ee
to
3df6884
Compare
Btw, what about including Also, I've realised that top-level package.json contains a reference to |
@karevn Sounds good as long as it's relatively straightforward to maintain – key thing is to keep the flow types and typescript types in sync as much as possible. Also good catch with the build script. |
@karevn Let's aim for Typescript in a later release though as 3.0 is getting really close. |
@ryanashcraft yes, that's exactly the way I see it - 3.0, then TS typings. Not to make a PR to PR :) |
return pendingQueries; | ||
}; | ||
|
||
const pick = (source: { [key: string]: any }, keys: Array<string>): { [key: string]: any } => { |
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.
Probably doesn't mean much with the current usage but could this be generic'd?
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.
Do you like this better? Unfortunately $Shape doesn't do what I want here (not sure why) – you can still reference any fields as if they are not optional.
const pick = <T: { [key: string]: mixed }>(source: T, keysToPick: Array<string>): $Shape<T> => {
const picked = { ...source };
const keysToPickSet = new Set(...keysToPick);
const keysToDelete = Object.keys(source).filter(key => !keysToPickSet.has(key));
for (const key of keysToDelete) {
if (picked.hasOwnProperty(key)) {
delete picked[key];
}
}
return picked;
};
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.
Take a look at this facebook/flow#7566
I also thought $Shape
worked like you did.
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.
Oh cool. Nice find. But still strange and a little disappointing that's how you do it and not $Shape.
return (Array.isArray(maybe) ? maybe : [maybe]).filter(Boolean); | ||
}; | ||
|
||
const difference = (a, b) => { | ||
const difference = <T>(a: Array<T>, b: Array<T>): Array<T> => { |
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.
$ReadOnlyArray<T>
const { result, rerender } = renderHook(() => useMemoizedQueryConfig(queryConfig, transform)); | ||
const returnValue1 = result.current; | ||
|
||
// Rerending but no props chnaged and the query config is the exact same, so the return value |
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.
chnaged
@ManThursday Updated. PTAL. Thanks. |
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.
lgtm
Hooks
Note: depends on hopefully soon-to-be released of react-redux.isPending
state and arefresh
(akaforceRequest
) callback. This hook will dispatch a requestAsync redux action when the hook "mounts" and whenever the query config's query key changes.mutate
callback.Separate packages
Smaller things
update
fields in query configsTo do
To be considered