-
Notifications
You must be signed in to change notification settings - Fork 222
react-graphql improvements #726
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,15 @@ | ||
/* eslint react-hooks/rules-of-hooks: off */ | ||
|
||
import {useEffect, useMemo, useState, useRef} from 'react'; | ||
import { | ||
ApolloClient, | ||
OperationVariables, | ||
ApolloError, | ||
WatchQueryOptions, | ||
ObservableQuery, | ||
} from 'apollo-client'; | ||
import {DocumentNode} from 'graphql-typed'; | ||
import {useServerEffect} from '@shopify/react-effect'; | ||
import {useAsyncAsset} from '@shopify/react-async'; | ||
|
||
import {AsyncQueryComponentType} from '../types'; | ||
import {QueryHookOptions, QueryHookResult} from './types'; | ||
|
@@ -33,12 +36,15 @@ export default function useQuery< | |
notifyOnNetworkStatusChange, | ||
context, | ||
} = options; | ||
|
||
const client = useApolloClient(overrideClient); | ||
const [query, id] = useGraphQLDocument(queryOrComponent); | ||
|
||
useAsyncAsset(id); | ||
if (typeof window === 'undefined' && skip) { | ||
return createDefaultResult(client, variables); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the early bailout |
||
|
||
const query = useGraphQLDocument(queryOrComponent); | ||
|
||
const serializedVariables = variables && JSON.stringify(variables); | ||
const watchQueryOptions = useMemo<WatchQueryOptions<Variables> | null>( | ||
() => { | ||
if (!query) { | ||
|
@@ -60,8 +66,7 @@ export default function useQuery< | |
query, | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
context && JSON.stringify(context), | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
variables && JSON.stringify(variables), | ||
serializedVariables, | ||
fetchPolicy, | ||
errorPolicy, | ||
pollInterval, | ||
|
@@ -90,33 +95,9 @@ export default function useQuery< | |
}); | ||
|
||
const defaultResult = useMemo<QueryHookResult<Data, Variables>>( | ||
() => ({ | ||
data: undefined, | ||
error: undefined, | ||
networkStatus: undefined, | ||
loading: false, | ||
variables: queryObservable ? queryObservable.variables : variables, | ||
refetch: queryObservable | ||
? queryObservable.refetch.bind(queryObservable) | ||
: (noop as any), | ||
fetchMore: queryObservable | ||
? queryObservable.fetchMore.bind(queryObservable) | ||
: (noop as any), | ||
updateQuery: queryObservable | ||
? queryObservable.updateQuery.bind(queryObservable) | ||
: (noop as any), | ||
startPolling: queryObservable | ||
? queryObservable.startPolling.bind(queryObservable) | ||
: (noop as any), | ||
stopPolling: queryObservable | ||
? queryObservable.stopPolling.bind(queryObservable) | ||
: (noop as any), | ||
subscribeToMore: queryObservable | ||
? queryObservable.subscribeToMore.bind(queryObservable) | ||
: (noop as any), | ||
client, | ||
}), | ||
[queryObservable, client, variables], | ||
() => createDefaultResult(client, variables, queryObservable), | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[queryObservable, client, serializedVariables], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the fix for the result changing when variables were deep equal |
||
); | ||
|
||
const [responseId, setResponseId] = useState(0); | ||
|
@@ -145,6 +126,7 @@ export default function useQuery< | |
const previousData = useRef< | ||
QueryHookResult<Data, Variables>['data'] | undefined | ||
>(undefined); | ||
|
||
const currentResult = useMemo<QueryHookResult<Data, Variables>>( | ||
() => { | ||
// must of the logic below are lifted from | ||
|
@@ -202,4 +184,37 @@ export default function useQuery< | |
return currentResult; | ||
} | ||
|
||
function createDefaultResult( | ||
client: ApolloClient<unknown>, | ||
variables: any, | ||
queryObservable?: ObservableQuery, | ||
) { | ||
return { | ||
data: undefined, | ||
error: undefined, | ||
networkStatus: undefined, | ||
loading: false, | ||
variables: queryObservable ? queryObservable.variables : variables, | ||
refetch: queryObservable | ||
? queryObservable.refetch.bind(queryObservable) | ||
: noop, | ||
fetchMore: queryObservable | ||
? queryObservable.fetchMore.bind(queryObservable) | ||
: noop, | ||
updateQuery: queryObservable | ||
? queryObservable.updateQuery.bind(queryObservable) | ||
: noop, | ||
startPolling: queryObservable | ||
? queryObservable.startPolling.bind(queryObservable) | ||
: noop, | ||
stopPolling: queryObservable | ||
? queryObservable.stopPolling.bind(queryObservable) | ||
: noop, | ||
subscribeToMore: queryObservable | ||
? queryObservable.subscribeToMore.bind(queryObservable) | ||
: noop, | ||
client, | ||
}; | ||
} | ||
|
||
function noop() {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,12 +7,7 @@ import {createGraphQLFactory} from '@shopify/graphql-testing'; | |
|
||
import {createAsyncQueryComponent} from '../../async'; | ||
import useQuery from '../query'; | ||
import { | ||
mountWithGraphQL, | ||
prepareAsyncReactTasks, | ||
teardownAsyncReactTasks, | ||
createResolvablePromise, | ||
} from './utilities'; | ||
import {mountWithGraphQL, createResolvablePromise} from './utilities'; | ||
|
||
const petQuery = gql` | ||
query PetQuery { | ||
|
@@ -33,14 +28,6 @@ const mockData = { | |
}; | ||
|
||
describe('useQuery', () => { | ||
beforeEach(() => { | ||
prepareAsyncReactTasks(); | ||
}); | ||
|
||
afterEach(() => { | ||
teardownAsyncReactTasks(); | ||
}); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer needed |
||
describe('document', () => { | ||
it('returns loading=true and networkStatus=loading during the loading of query', async () => { | ||
function MockQuery({children}) { | ||
|
@@ -89,6 +76,43 @@ describe('useQuery', () => { | |
); | ||
}); | ||
|
||
it('keeps the same data when the variables stay deep-equal', async () => { | ||
function MockQuery({ | ||
children, | ||
variables, | ||
}: { | ||
children: ( | ||
result: ReturnType<typeof useQuery>, | ||
) => React.ReactElement | null; | ||
variables?: object; | ||
}) { | ||
const results = useQuery(petQuery, {variables}); | ||
return children(results); | ||
} | ||
|
||
const graphQL = createGraphQL({PetQuery: mockData}); | ||
const renderPropSpy = jest.fn(() => null); | ||
const variables = {foo: 'bar'}; | ||
|
||
const mockQuery = await mountWithGraphQL( | ||
<MockQuery variables={variables}>{renderPropSpy}</MockQuery>, | ||
{ | ||
graphQL, | ||
}, | ||
); | ||
|
||
mockQuery.setProps({variables: {...variables}}); | ||
|
||
expect(graphQL.operations.all()).toHaveLength(1); | ||
|
||
// Once for initial render while loading, once for when the data loaded, and a final time | ||
// when we update the props and re-render the component. | ||
expect(renderPropSpy).toHaveBeenCalledTimes(3); | ||
|
||
const [, firstLoadedCall, secondLoadedCall] = renderPropSpy.mock.calls; | ||
expect(firstLoadedCall[0]).toBe(secondLoadedCall[0]); | ||
}); | ||
|
||
it('watchQuery is not called when skip is true', async () => { | ||
const mockClient = createMockApolloClient(); | ||
const watchQuerySpy = jest.fn(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import * as React from 'react'; | ||
import {IfAllOptionalKeys} from '@shopify/useful-types'; | ||
import {Root} from './root'; | ||
import {Root, Options as RootOptions} from './root'; | ||
import {Element} from './element'; | ||
|
||
export {Root, Element}; | ||
|
@@ -81,9 +81,9 @@ export class CustomRoot<Props, Context extends object> extends Root<Props> { | |
constructor( | ||
tree: React.ReactElement<Props>, | ||
public readonly context: Context, | ||
resolve: (element: Element<unknown>) => Element<unknown> | null, | ||
options?: RootOptions, | ||
) { | ||
super(tree, resolve); | ||
super(tree, options); | ||
} | ||
} | ||
|
||
|
@@ -105,11 +105,11 @@ export function createMount< | |
options: MountOptions = {} as any, | ||
) { | ||
const context = createContext(options); | ||
const rendered = render(element, context, options); | ||
|
||
const wrapper = new CustomRoot(rendered, context, root => | ||
root.find(element.type), | ||
); | ||
const wrapper = new CustomRoot(element, context, { | ||
render: element => render(element, context, options), | ||
resolveRoot: root => root.find(element.type), | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The basic gist of this fix is that instead of a |
||
|
||
const afterMountResult = afterMount(wrapper, options); | ||
|
||
|
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 also moved this back into this hook, where it feels more appropriate than the
useQuery
hook