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

Hooks API and separate packages #129

Merged
merged 88 commits into from
Jun 25, 2019
Merged

Hooks API and separate packages #129

merged 88 commits into from
Jun 25, 2019

Conversation

ryanashcraft
Copy link
Contributor

@ryanashcraft ryanashcraft commented Jun 7, 2019

Hooks

Note: depends on hopefully soon-to-be released of react-redux.

  • Compatibility with react-redux v7, which uses React's new context API
  • useRequest: which is similar to connectRequest but only for a single query. It returns the isPending state and a refresh (aka forceRequest) callback. This hook will dispatch a requestAsync redux action when the hook "mounts" and whenever the query config's query key changes.
  • useMutation, which returns an isPending and mutate callback.
  • connectRequest has been rewritten to use hooks and is hopefully compatible with React's concurrent mode

Separate packages

  • redux-query, redux-query-react, and redux-query-interface-superagent. This enables users that don't use react or superagent to no longer have to depend on those libraries.
  • The redux-query/advanced entry point has been removed. The "advanced" middleware is now just the primary middleware.

Smaller things

  • No longer require update fields in query configs
  • Updating some dependencies
  • More tests for the React API

To do

  • Update README
  • Write transition docs
  • Dogfood/test with Amplitude on production
  • Publish as v3.0

To be considered

  • Returning more query metadata (i.e. response status) in addition to isPending for useRequest and useMutation.
  • Returning response metadata (like the promise return value from dispatching mutateAsync, including response body and entities) for useMutation.
  • Flow types for hooks and query configs.
  • Updated docs that go into more depth the project's principles and best practices.
  • Address Superagent tries to mutate frozen store on timeout #121 (Superagent tries to mutate frozen store on timeout)

Copy link
Contributor

@trashstack trashstack left a comment

Choose a reason for hiding this comment

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

Basically just questions

babel.config.js Outdated Show resolved Hide resolved
return { cancelKeys, requestActions };
};

const useMemoizedActions = (mapPropsToConfigs, props, callback) => {
Copy link
Contributor

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));
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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]);
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

const transformQueryConfigToAction = useConstCallback(queryConfig => {
return {
...queryConfig,
unstable_preDispatchCallback: finishedCallback,
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

@trashstack trashstack left a 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

@ryanashcraft
Copy link
Contributor Author

@ManThursday updates from today:

  • Only set babel node target for test env (with comment), per your comment
  • Simplify connectRequest diffing, as you indicated
  • Remove several lodash libs
  • Fix useMutation to actually return the mutate promise
  • Start flow types: hooks and query configs
  • Remove networkHandler from redux state and actions

New to dos:

  • Add flow types for getQueryKey, all redux actions, connectRequest, entities reducer

Copy link
Contributor

@trashstack trashstack left a 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 anys but presumably that's not possible for most of them.

@karevn
Copy link
Contributor

karevn commented Jun 12, 2019

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,
Copy link
Contributor

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 } => {
Copy link
Contributor

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).

Copy link
Contributor Author

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;
Copy link
Contributor

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

Copy link
Contributor Author

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) {
Copy link
Contributor

@karevn karevn Jun 12, 2019

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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 :)

@ryanashcraft
Copy link
Contributor Author

@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?

@karevn
Copy link
Contributor

karevn commented Jun 17, 2019

OK, I'll start writing Typescript typings to publish them via DefinitelyTyped then.

@karevn
Copy link
Contributor

karevn commented Jun 23, 2019

Btw, what about including .d.ts file into redux-query packages? I think it would be much more transparent for Typescript users.

Also, I've realised that top-level package.json contains a reference to ./bin/build.sh, which is not included into the repo.

@ryanashcraft
Copy link
Contributor Author

@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.

@ryanashcraft
Copy link
Contributor Author

@karevn Let's aim for Typescript in a later release though as 3.0 is getting really close.

@karevn
Copy link
Contributor

karevn commented Jun 24, 2019

@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 } => {
Copy link
Contributor

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?

Copy link
Contributor Author

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;
};

Copy link
Contributor

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.

Copy link
Contributor Author

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> => {
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

chnaged

@ryanashcraft
Copy link
Contributor Author

@ManThursday Updated. PTAL. Thanks.

Copy link
Contributor

@trashstack trashstack left a comment

Choose a reason for hiding this comment

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

lgtm

@ryanashcraft ryanashcraft merged commit 3a89acd into master Jun 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants