-
Notifications
You must be signed in to change notification settings - Fork 226
✨ add useQuery
hook
#663
Conversation
8944d5c
to
248b773
Compare
958baf5
to
9ddaf5a
Compare
@@ -1,4 +1,4 @@ | |||
export {ApolloProvider} from './ApolloProvider'; | |||
export {useApolloClient} from './apollo-client'; | |||
export {default as useApolloClient} from './apollo-client'; |
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.
decided to export this so we can use it instead of withApollo
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.
Overall looks great 👍
packages/react-async/src/Async.tsx
Outdated
@@ -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'; |
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 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; |
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 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; |
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.
&& 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. |
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 do you think about calling it useGraphQLClient
instead? I am not sure if it's better to hide Apollo's involvement or not 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.
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?
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.
Who knows about the Apollo thing, I think this is fine though (renaming a function is a fairly easy refactor later anyways) 👍
packages/react-graphql/README.md
Outdated
### `useApolloClient` | ||
|
||
`useApolloClient` hook can be use to access apollo client that is currently in the context. | ||
Which can be use to trigger query manually. |
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.
Not really a sentence
packages/react-graphql/src/async.tsx
Outdated
Object.defineProperty(FinalComponent, 'resolved', { | ||
get: () => resolvedDocument, | ||
// accessor descriptor don't need this | ||
// writable: 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.
🔥?
async () => { | ||
if (!isDocumentNode(documentOrComponent)) { | ||
try { | ||
const resolved = await documentOrComponent.resolve(); |
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 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;
}
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.
arg...I would have assume that react have this behaviour imbedded in all the hooks now. (don't know why I thought that...)
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.
@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
?
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 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 🤔
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, 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).
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 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.
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 is literally the exact same problem solved by having a mounted
instance variable on a class component.
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.
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.
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, 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']>(); |
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.
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}) => { |
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.
Function declarations :D
6895705
to
5da5726
Compare
Still have the following to do: Todo:
|
7af9df9
to
ec53193
Compare
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.
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>({ |
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.
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. |
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.
Who knows about the Apollo thing, I think this is fine though (renaming a function is a fairly easy refactor later anyways) 👍
packages/react-graphql/README.md
Outdated
} | ||
``` | ||
|
||
### with query document |
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.
For follow up: make sure headings are sentence case
} else { | ||
return documentOrComponent.resolved; | ||
} | ||
}); |
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 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], |
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.
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; |
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.
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 👍
Re-open #642
Close #586
🎩 instructions:
web-proving-ground
locallyyarn
inweb-proving-ground
quilt
, doyarn && yarn tophat react-async ../web-proving-ground
quilt
, doyarn tophat react-hooks ../web-proving-ground
quilt
, doyarn tophat react-graphql ../web-proving-ground
web-proving-ground
and doyarn dev
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.