Skip to content

Commit

Permalink
make "useLazyQuery" execution function error like "useMutation"’s
Browse files Browse the repository at this point in the history
  • Loading branch information
brainkim committed Jan 19, 2022
1 parent 55307f2 commit 78c136a
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 50 deletions.
82 changes: 75 additions & 7 deletions src/react/hooks/__tests__/useLazyQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { GraphQLError } from 'graphql';
import gql from 'graphql-tag';
import { renderHook } from '@testing-library/react-hooks';

Expand Down Expand Up @@ -450,11 +451,11 @@ describe('useLazyQuery Hook', () => {
expect(result.current[1].previousData).toBe(undefined);

setTimeout(() => execute({ variables: { id: 2 }}));
// Why is there no loading state here?

await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toEqual(data1);
expect(result.current[1].previousData).toBe(undefined);
expect(result.current[1].loading).toBe(true);
expect(result.current[1].data).toBe(undefined);
expect(result.current[1].previousData).toEqual(data1);

await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
Expand Down Expand Up @@ -542,9 +543,7 @@ describe('useLazyQuery Hook', () => {
await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toEqual({ hello: 'world' });

expect(executeResult).toBeInstanceOf(Promise);
expect(await executeResult).toEqual(result.current[1]);
await expect(executeResult).resolves.toEqual(result.current[1]);
});

it('should have matching results from execution function and hook', async () => {
Expand Down Expand Up @@ -657,4 +656,73 @@ describe('useLazyQuery Hook', () => {
},
});
});

it('the promise should reject with errors the “way useMutation does”', async () => {
const query = gql`{ hello }`;
const mocks = [
{
request: { query },
result: {
errors: [new GraphQLError('error 1')],
},
delay: 20,
},
{
request: { query },
result: {
errors: [new GraphQLError('error 2')],
},
delay: 20,
},
];

const { result, waitForNextUpdate } = renderHook(
() => useLazyQuery(query),
{
wrapper: ({ children }) => (
<MockedProvider mocks={mocks}>
{children}
</MockedProvider>
),
},
);

const execute = result.current[0];
let executeResult: any;
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toBe(undefined);
setTimeout(() => {
executeResult = execute();
executeResult.catch(() => {});
});

await waitForNextUpdate();
expect(result.current[1].loading).toBe(true);
expect(result.current[1].data).toBe(undefined);
expect(result.current[1].error).toBe(undefined);

await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toBe(undefined);
expect(result.current[1].error).toEqual(new Error('error 1'));

await expect(executeResult).rejects.toEqual(new Error('error 1'));

setTimeout(() => {
executeResult = execute();
executeResult.catch(() => {});
});

await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toBe(undefined);
expect(result.current[1].error).toEqual(new Error('error 1'));

await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toBe(undefined);
expect(result.current[1].error).toEqual(new Error('error 2'));

await expect(executeResult).rejects.toEqual(new Error('error 2'));
});
});
79 changes: 36 additions & 43 deletions src/react/hooks/useLazyQuery.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { DocumentNode } from 'graphql';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';

import {
LazyQueryHookOptions,
LazyQueryResult,
QueryLazyOptions,
QueryTuple,
} from '../types/types';
Expand All @@ -28,56 +27,19 @@ export function useLazyQuery<TData = any, TVariables = OperationVariables>(
const [execution, setExecution] = useState<{
called: boolean,
options?: QueryLazyOptions<TVariables>,
resolve?: (result: LazyQueryResult<TData, TVariables>) => void,
}>({
called: false,
});

let result = useQuery<TData, TVariables>(query, {
...options,
...execution.options,
// We don’t set skip to execution.called, because we need useQuery to call
// addQueryPromise, so that ssr calls waits for execute to be called.
// We don’t set skip to execution.called, because some useQuery SSR code
// checks skip for some reason.
fetchPolicy: execution.called ? options?.fetchPolicy : 'standby',
skip: undefined,
});

const execute = useCallback<
QueryTuple<TData, TVariables>[0]
>((executeOptions?: QueryLazyOptions<TVariables>) => {
let resolve!: (result: LazyQueryResult<TData, TVariables>) => void;
const promise = new Promise<LazyQueryResult<TData, TVariables>>(
(resolve1) => (resolve = resolve1),
);
setExecution((execution) => {
if (execution.called) {
resolve(result.refetch(executeOptions?.variables) as any);
return execution;
}

return {
called: true,
resolve,
options: executeOptions,
};
});

return promise;
}, []);

// NOTE(brian): I tried to call refetch() for the first call, but some truly
// strange circular object errors started appearing in jest tests.
useEffect(() => {
const { resolve } = execution;
if (!result.loading && resolve) {
setExecution((execution) => {
return { ...execution, resolve: undefined };
});

resolve(result);
}
}, [result, execution]);

if (!execution.called) {
result = {
...result,
Expand All @@ -86,16 +48,47 @@ export function useLazyQuery<TData = any, TVariables = OperationVariables>(
error: void 0,
called: false,
};
}


// We use useMemo here to make sure the eager methods have a stable identity.
const eagerMethods = useMemo(() => {
const eagerMethods: Record<string, any> = {};
for (const key of EAGER_METHODS) {
const method = result[key];
result[key] = (...args: any) => {
eagerMethods[key] = (...args: any) => {
setExecution((execution) => ({ ...execution, called: true }));
return (method as any)(...args);
};
}

return eagerMethods;
}, []);

result.error = result.error || void 0;
for (const key of EAGER_METHODS) {
result[key] = eagerMethods[key];
}

const execute = useCallback<
QueryTuple<TData, TVariables>[0]
>((executeOptions?: QueryLazyOptions<TVariables>) => {
setExecution({ called: true, options: executeOptions });
return result.refetch(executeOptions?.variables).then((result1) => {
const result2 = {
...result,
data: result1.data,
error: result1.error,
called: true,
loading: false,
};

for (const key of EAGER_METHODS) {
result2[key] = eagerMethods[key];
}

return result2;
});
}, []);

return [execute, result];
}

0 comments on commit 78c136a

Please sign in to comment.