Skip to content
This repository was archived by the owner on Jan 10, 2025. It is now read-only.

✨ add useQuery hook #663

Merged
merged 1 commit into from
Apr 25, 2019
Merged

✨ add useQuery hook #663

merged 1 commit into from
Apr 25, 2019

Conversation

michenly
Copy link
Contributor

@michenly michenly commented Apr 18, 2019

Re-open #642
Close #586

🎩 instructions:

  1. get this branch on web-proving-ground locally
  2. run yarn in web-proving-ground
  3. in quilt, do yarn && yarn tophat react-async ../web-proving-ground
  4. again in quilt, do yarn tophat react-hooks ../web-proving-ground
  5. again in quilt, do yarn tophat react-graphql ../web-proving-ground
  6. once it complied, go to web-proving-ground and do yarn dev
  7. goto localhost:8081 to see the query & mutation working properly

^ kinda cool I can do tophat with two different lib. As long as I do the tophat in the dependency order.

@michenly michenly force-pushed the use-query branch 3 times, most recently from 8944d5c to 248b773 Compare April 22, 2019 17:53
@michenly michenly marked this pull request as ready for review April 22, 2019 17:54
@michenly michenly self-assigned this Apr 22, 2019
@michenly michenly force-pushed the use-query branch 2 times, most recently from 958baf5 to 9ddaf5a Compare April 22, 2019 21:22
@@ -1,4 +1,4 @@
export {ApolloProvider} from './ApolloProvider';
export {useApolloClient} from './apollo-client';
export {default as useApolloClient} from './apollo-client';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

decided to export this so we can use it instead of withApollo

Copy link
Member

@lemonmade lemonmade left a comment

Choose a reason for hiding this comment

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

Overall looks great 👍

@@ -13,7 +13,7 @@ import {
} from '@shopify/react-intersection-observer';

import {AsyncAssetContext, AsyncAssetManager} from './context/assets';
import {normalize, resolve} from './utilities';
import {initialResolved, resolve} from './utilities';
Copy link
Member

Choose a reason for hiding this comment

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

I think maybe trySynchronousResolve provides the explicitness that we kind of want here, given that it is a bit of an odd process

}

export function initialResolved<T>({id, defer}: Options<T>): T | null {
const canResolve = defer == null && id;
Copy link
Member

Choose a reason for hiding this comment

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

I think defer can also be DeferTiming.Mount and we'd still want to do this


export function initialResolved<T>({id, defer}: Options<T>): T | null {
const canResolve = defer == null && id;
const resolved = canResolve && id ? tryRequire(id()) : null;
Copy link
Member

Choose a reason for hiding this comment

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

&& id is not needed given the check above, so remove it from one

@@ -173,6 +173,97 @@ function App() {
render(<App />, document.getElementById('root'));
```

### `useApolloClient`

`useApolloClient` hook can be use to access apollo client that is currently in the context.
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about calling it useGraphQLClient instead? I am not sure if it's better to hide Apollo's involvement or not 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.

we do call this lib react-graphql ...
I can go both way of this, useApolloClient make it so people know exactly they are getting the client object from apollo and should look up apollo API on how to use it.

useGraphQLClient hides that, but is future proof if we ever not use apollo?

Copy link
Member

Choose a reason for hiding this comment

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

Who knows about the Apollo thing, I think this is fine though (renaming a function is a fairly easy refactor later anyways) 👍

### `useApolloClient`

`useApolloClient` hook can be use to access apollo client that is currently in the context.
Which can be use to trigger query manually.
Copy link
Member

Choose a reason for hiding this comment

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

Not really a sentence

Object.defineProperty(FinalComponent, 'resolved', {
get: () => resolvedDocument,
// accessor descriptor don't need this
// writable: false,
Copy link
Member

Choose a reason for hiding this comment

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

🔥?

async () => {
if (!isDocumentNode(documentOrComponent)) {
try {
const resolved = await documentOrComponent.resolve();
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to check whether the component is mounted before doing this? I wrote a nice hook for this we can throw in the shared package:

export function useMounted() {
  const mounted = useRef(true);

  useEffect(() => {
    return () => {
      mounted.current = false;
    };
  }, []);

  return mounted;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

arg...I would have assume that react have this behaviour imbedded in all the hooks now. (don't know why I thought that...)

Choose a reason for hiding this comment

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

@lemonmade curious as to why you are using a ref in the hook above instead of a variable? I can see how useRef can be useful if the value being assigned was an object since we can re-use the same reference without creating new object references, however, if it is a boolean, it should be fine to use let mounted ?

Choose a reason for hiding this comment

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

I think we need to check whether the component is mounted before doing this?

I likely don't have context, but why do we need to check for mounted ? Won't this function useGraphQLDocument not called at all if the component using it is not mounted? Feels like I am missing context 🤔

Copy link
Member

Choose a reason for hiding this comment

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

Yes, the other option here is just to let mounted = true;, then set it to false in the callback, then check that variable in scope after the async operation. The hook just wraps that logic up (the only way to wrap it up in a hook is to do it with a ref, you can't really store a persisted value any other way).

Yes, useEffect only gets called when the component is mounted, but that doesn't mean it will end while the component is still mounted. If you kick off an async operation (as is done here), there is no requirement for React to wait for it to be done while the component is still mounting (it would be catastrophic if the UI didn't update until the promise resolved, and it would make no sense to keep the component mounted beyond when a parent unmounts it).

Copy link
Contributor Author

@michenly michenly Apr 25, 2019

Choose a reason for hiding this comment

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

so what to know is this setState is after an await
the component will be mount when the async method start, but may not be mounted by the time it finished.

Ref is normally uses for accessing the dom, but in hooks land, it is also the react recommended way to keeping value you will normally keep around using property variable in class component.

Since hook turn the function itself into a component that has lifecycle, just using a variables that can be access through out the different hook can have unexpected result.

Copy link
Member

Choose a reason for hiding this comment

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

The is literally the exact same problem solved by having a mounted instance variable on a class component.

Choose a reason for hiding this comment

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

Makes sense. Thanks to you both!

As for the mounted ref, an ideal solution would have been to have some kind of cancel/abort for loadDocument which we return from the useEffect so that it cancels the document load if the component unmounts. Having said that, I can't think of a reliable way to do so for this use case.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it would definitely be preferable, we can potentially wrap in a cancellable promise as an alternative (I suggested a mounted ref because I had already written it, a cancellable promise is a little trickier)

[skip, queryObservable],
);

const previousData = useRef<QueryHookResult<Data, Variables>['data']>();
Copy link
Member

Choose a reason for hiding this comment

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

Don't need an initial value here?


describe('document', () => {
it('returns loading=true and networkStatus=loading during the loading of query', async () => {
const MockQuery = ({children}) => {
Copy link
Member

Choose a reason for hiding this comment

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

Function declarations :D

@michenly michenly force-pushed the use-query branch 6 times, most recently from 6895705 to 5da5726 Compare April 24, 2019 22:16
@michenly
Copy link
Contributor Author

michenly commented Apr 24, 2019

Still have the following to do:
but useQuery itself can be re-review

Todo:

  • useMountedRef test
  • useMountedRef README
  • react-hooks Changelog
  • react-async Changelog
  • react-graphql Changelog

@michenly michenly requested a review from lemonmade April 24, 2019 22:19
@michenly michenly force-pushed the use-query branch 2 times, most recently from 7af9df9 to ec53193 Compare April 25, 2019 13:43
Copy link
Member

@lemonmade lemonmade left a comment

Choose a reason for hiding this comment

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

Very happy with this 👍

@@ -134,8 +129,10 @@ export function Async<Value>(props: Omit<Props<Value>, 'manager'>) {
}

function initialState<Value>(props: Props<Value>): State<Value> {
const canResolve = props.defer == null && props.id;
const resolved = canResolve && props.id ? tryRequire(props.id()) : null;
const resolved = trySynchronousResolve<Value>({
Copy link
Member

Choose a reason for hiding this comment

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

FWIW, you can usually just pass props to this function directly, it has more properties but that's fine because it matches the type signature

@@ -173,6 +173,97 @@ function App() {
render(<App />, document.getElementById('root'));
```

### `useApolloClient`

`useApolloClient` hook can be use to access apollo client that is currently in the context.
Copy link
Member

Choose a reason for hiding this comment

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

Who knows about the Apollo thing, I think this is fine though (renaming a function is a fairly easy refactor later anyways) 👍

}
```

### with query document
Copy link
Member

Choose a reason for hiding this comment

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

For follow up: make sure headings are sentence case

} else {
return documentOrComponent.resolved;
}
});
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if this is a case where we can break the rule of hooks by short circuiting here. Maybe better as an optimization later, but I think it will at least be an option.

loadDocument();
}
},
[document, documentOrComponent, loadDocument],
Copy link
Member

Choose a reason for hiding this comment

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

Don't think you need documentOrComponent here?

export interface QueryHookResult<Data, Variables>
extends Omit<QueryResult<Data, Variables>, 'networkStatus' | 'variables'> {
networkStatus: QueryResult<Data, Variables>['networkStatus'] | undefined;
variables: QueryResult<Data, Variables>['variables'] | undefined;
Copy link
Member

Choose a reason for hiding this comment

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

We can technically offer up better typings for some of these because we can check if the variables have any required keys or not, but for now this is fine 👍

@michenly michenly merged commit 3eff97a into master Apr 25, 2019
@michenly michenly deleted the use-query branch April 25, 2019 15:25
@michenly michenly temporarily deployed to production April 25, 2019 16:16 Inactive
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add "blessed" hooks for GraphQL queries and mutations
3 participants