diff --git a/docs/api.md b/docs/api.md index cf4bd3c480..e86a16c51d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -11,6 +11,7 @@ interface UseQueryArgs { query: string; variables?: any; requestPolicy?: RequestPolicy; + skip?: boolean; } ``` @@ -100,6 +101,7 @@ interface UseSubscriptionState { | query | `string` | The GraphQL request's query | | variables | `object` | The GraphQL request's variables | | requestPolicy | `?RequestPolicy` | An optional request policy that should be used | +| skip | `?boolean` | A boolean flag instructing `Query` to skip executing the subsequent query operation | | children | `RenderProps => ReactNode` | A function that follows the typical render props pattern. The shape of the render props is as follows | #### Render Props diff --git a/src/components/Query.test.tsx b/src/components/Query.test.tsx index 1cf6bf4104..05c85afe3a 100644 --- a/src/components/Query.test.tsx +++ b/src/components/Query.test.tsx @@ -110,3 +110,23 @@ describe('on error', () => { expect(childProps).toHaveProperty('error', error); }); }); + +describe('skip', () => { + beforeEach(() => { + client.executeQuery.mockReturnValue(fromValue({ data: 1234 })); + }); + + it('should skip executing the query if skip is true', () => { + mountWrapper({ ...props, skip: true }); + expect(client.executeQuery).not.toHaveBeenCalled(); + }); + + it('should not call executeQuery if skip changes to true', () => { + const wrapper = mountWrapper(props); + expect(client.executeQuery).toHaveBeenCalledTimes(1); + + // @ts-ignore + wrapper.setProps({ ...props, query: '{ newQuery }', skip: true }); + expect(client.executeQuery).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/Query.tsx b/src/components/Query.tsx index 0f5e0a857c..66c3dc75c8 100644 --- a/src/components/Query.tsx +++ b/src/components/Query.tsx @@ -11,6 +11,7 @@ interface QueryHandlerProps { variables?: object; client: Client; requestPolicy?: RequestPolicy; + skip?: boolean; children: (arg: QueryHandlerState) => ReactNode; } @@ -30,19 +31,27 @@ class QueryHandler extends Component { this.setState({ fetching: true }); - const [teardown] = pipe( - this.props.client.executeQuery(this.request, { - requestPolicy: this.props.requestPolicy, - ...opts, - }), - subscribe(({ data, error }) => { - this.setState({ - fetching: false, - data, - error, - }); - }) - ); + let teardown = noop; + + if (!this.props.skip) { + [teardown] = pipe( + this.props.client.executeQuery(this.request, { + requestPolicy: this.props.requestPolicy, + ...opts, + }), + subscribe(({ data, error }) => { + this.setState({ + fetching: false, + data, + error, + }); + }) + ); + } else { + this.setState({ + fetching: false, + }); + } this.unsubscribe = teardown; }; diff --git a/src/hooks/useQuery.test.tsx b/src/hooks/useQuery.test.tsx index e78f39fc09..e2bedc5a0e 100644 --- a/src/hooks/useQuery.test.tsx +++ b/src/hooks/useQuery.test.tsx @@ -19,30 +19,36 @@ jest.mock('../client', () => { import React, { FC } from 'react'; import renderer, { act } from 'react-test-renderer'; -// @ts-ignore - data is imported from mock only import { createClient } from '../client'; -import { useQuery } from './useQuery'; +import { OperationContext } from '../types'; +import { useQuery, UseQueryArgs, UseQueryState } from './useQuery'; // @ts-ignore const client = createClient() as { executeQuery: jest.Mock }; -const props = { +const props: UseQueryArgs<{ myVar: number }> = { query: '{ example }', variables: { myVar: 1234, }, + skip: false, }; -let state: any; -let execute: any; -const QueryUser: FC = ({ query, variables }) => { - const [s, e] = useQuery({ query, variables }); +let state: UseQueryState | undefined; +let execute: ((opts?: Partial) => void) | undefined; + +const QueryUser: FC> = ({ + query, + variables, + skip, +}) => { + const [s, e] = useQuery({ query, variables, skip }); state = s; execute = e; return

{s.data}

; }; beforeAll(() => { - // tslint:disable-next-line + // tslint:disable-next-line no-console console.log( 'supressing console.error output due to react-test-renderer spam (hooks related)' ); @@ -158,7 +164,25 @@ describe('on change', () => { describe('execute query', () => { it('triggers query execution', () => { renderer.create(); - act(() => execute()); + act(() => execute!()); expect(client.executeQuery).toBeCalledTimes(2); }); }); + +describe('skip', () => { + it('skips executing the query if skip is true', () => { + renderer.create(); + expect(client.executeQuery).not.toBeCalled(); + }); + + it('skips executing queries if skip updates to true', () => { + const wrapper = renderer.create(); + + /** + * Call update twice for the change to be detected. + */ + wrapper.update(); + wrapper.update(); + expect(client.executeQuery).toBeCalledTimes(1); + }); +}); diff --git a/src/hooks/useQuery.ts b/src/hooks/useQuery.ts index 51b9d71789..17936d407a 100644 --- a/src/hooks/useQuery.ts +++ b/src/hooks/useQuery.ts @@ -9,9 +9,10 @@ export interface UseQueryArgs { query: string | DocumentNode; variables?: V; requestPolicy?: RequestPolicy; + skip?: boolean; } -interface UseQueryState { +export interface UseQueryState { fetching: boolean; data?: T; error?: CombinedError; @@ -39,27 +40,34 @@ export const useQuery = ( const executeQuery = useCallback( (opts?: Partial) => { unsubscribe(); + setState(s => ({ ...s, fetching: true })); - const [teardown] = pipe( - client.executeQuery(request, { - requestPolicy: args.requestPolicy, - ...opts, - }), - subscribe(({ data, error }) => - setState({ fetching: false, data, error }) - ) - ); + let teardown = noop; + + if (!args.skip) { + [teardown] = pipe( + client.executeQuery(request, { + requestPolicy: args.requestPolicy, + ...opts, + }), + subscribe(({ data, error }) => + setState({ fetching: false, data, error }) + ) + ); + } else { + setState(s => ({ ...s, fetching: false })); + } unsubscribe = teardown; }, - [request.key] + [request.key, args.skip, args.requestPolicy] ); useEffect(() => { executeQuery(); return unsubscribe; - }, [request.key]); + }, [request.key, args.skip]); return [state, executeQuery]; };