From 2e833b2cacb71fc2050cb3976d0bbe710baeedff Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Fri, 9 Jun 2023 13:26:09 -0400 Subject: [PATCH] Improve `useBackgroundQuery` type interface and add type tests (#10951) --- .changeset/friendly-mugs-repeat.md | 5 + .../__tests__/useBackgroundQuery.test.tsx | 225 ++++++++++++++---- .../hooks/__tests__/useSuspenseQuery.test.tsx | 60 ++--- src/react/hooks/useBackgroundQuery.ts | 94 +++++++- src/react/hooks/useSuspenseQuery.ts | 2 - src/react/types/types.ts | 4 + 6 files changed, 306 insertions(+), 84 deletions(-) create mode 100644 .changeset/friendly-mugs-repeat.md diff --git a/.changeset/friendly-mugs-repeat.md b/.changeset/friendly-mugs-repeat.md new file mode 100644 index 00000000000..a4050061576 --- /dev/null +++ b/.changeset/friendly-mugs-repeat.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': patch +--- + +Improve `useBackgroundQuery` type interface diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 8f841823cf9..a2a29a9db69 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -9,6 +9,7 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ErrorBoundary, ErrorBoundaryProps } from 'react-error-boundary'; +import { expectTypeOf } from 'expect-type'; import { GraphQLError } from 'graphql'; import { gql, @@ -38,8 +39,11 @@ import { useBackgroundQuery, useReadQuery } from '../useBackgroundQuery'; import { ApolloProvider } from '../../context'; import { SuspenseCache } from '../../cache'; import { InMemoryCache } from '../../../cache'; -import { FetchMoreFunction } from '../../../react'; -import { QueryReference } from '../../cache/QueryReference'; +import { + FetchMoreFunction, + RefetchFunction, + QueryReference, +} from '../../../react'; function renderIntegrationTest({ client, @@ -131,6 +135,37 @@ function renderIntegrationTest({ return { ...rest, query, client: _client, renders }; } +interface VariablesCaseData { + character: { + id: string; + name: string; + }; +} + +interface VariablesCaseVariables { + id: string; +} + +function useVariablesIntegrationTestCase() { + const query: TypedDocumentNode< + VariablesCaseData, + VariablesCaseVariables + > = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const CHARACTERS = ['Spider-Man', 'Black Widow', 'Iron Man', 'Hulk']; + let mocks = [...CHARACTERS].map((name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { data: { character: { id: String(index + 1), name } } }, + })); + return { mocks, query }; +} + function renderVariablesIntegrationTest({ variables, mocks, @@ -150,32 +185,8 @@ function renderVariablesIntegrationTest({ variables: { id: string }; errorPolicy?: ErrorPolicy; }) { - const CHARACTERS = ['Spider-Man', 'Black Widow', 'Iron Man', 'Hulk']; + let { mocks: _mocks, query } = useVariablesIntegrationTestCase(); - interface QueryData { - character: { - id: string; - name: string; - }; - } - - interface QueryVariables { - id: string; - } - - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - - let _mocks = [...CHARACTERS].map((name, index) => ({ - request: { query, variables: { id: String(index + 1) } }, - result: { data: { character: { id: String(index + 1), name } } }, - })); // duplicate mocks with (updated) in the name for refetches _mocks = [..._mocks, ..._mocks, ..._mocks].map( ({ request, result }, index) => { @@ -208,7 +219,7 @@ function renderVariablesIntegrationTest({ suspenseCount: number; count: number; frames: { - data: QueryData; + data: VariablesCaseData; networkStatus: NetworkStatus; error: ApolloError | undefined; }[]; @@ -239,11 +250,11 @@ function renderVariablesIntegrationTest({ variables: _variables, queryRef, }: { - variables: QueryVariables; + variables: VariablesCaseVariables; refetch: ( variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; + ) => Promise>; + queryRef: QueryReference; }) { const { data, error, networkStatus } = useReadQuery(queryRef); const [variables, setVariables] = React.useState(_variables); @@ -276,7 +287,7 @@ function renderVariablesIntegrationTest({ variables, errorPolicy = 'none', }: { - variables: QueryVariables; + variables: VariablesCaseVariables; errorPolicy?: ErrorPolicy; }) { const [queryRef, { refetch }] = useBackgroundQuery(query, { @@ -294,7 +305,7 @@ function renderVariablesIntegrationTest({ variables, errorPolicy, }: { - variables: QueryVariables; + variables: VariablesCaseVariables; errorPolicy?: ErrorPolicy; }) { return ( @@ -314,7 +325,7 @@ function renderVariablesIntegrationTest({ const { ...rest } = render( ); - const rerender = ({ variables }: { variables: QueryVariables }) => { + const rerender = ({ variables }: { variables: VariablesCaseVariables }) => { return rest.rerender(); }; return { ...rest, query, rerender, client, renders }; @@ -1243,7 +1254,15 @@ describe('useBackgroundQuery', () => { }); it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - const query = gql` + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode = gql` query { greeting { message @@ -1256,13 +1275,6 @@ describe('useBackgroundQuery', () => { } `; - interface Data { - greeting: { - message: string; - recipient: { name: string }; - }; - } - const link = new MockSubscriptionLink(); const cache = new InMemoryCache(); cache.writeQuery({ @@ -1875,9 +1887,7 @@ describe('useBackgroundQuery', () => { queryRef, refetch, }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; + refetch: RefetchFunction; queryRef: QueryReference; onChange: (id: string) => void; }) { @@ -2199,11 +2209,136 @@ describe('useBackgroundQuery', () => { // @ts-expect-error should not allow returnPartialData in options useBackgroundQuery(query, { returnPartialData: true }); }); + it('disallows refetchWritePolicy in BackgroundQueryHookOptions', () => { const { query } = renderIntegrationTest(); // @ts-expect-error should not allow refetchWritePolicy in options useBackgroundQuery(query, { refetchWritePolicy: 'overwrite' }); }); + + it('returns unknown when TData cannot be inferred', () => { + const query = gql` + query { + hello + } + `; + + const [queryRef] = useBackgroundQuery(query); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + }); + + it('disallows wider variables type than specified', () => { + const { query } = useVariablesIntegrationTestCase(); + + // @ts-expect-error should not allow wider TVariables type + useBackgroundQuery(query, { variables: { id: '1', foo: 'bar' } }); + }); + + it('returns TData in default case', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'ignore', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: 'ignore', + }); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'all', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'all', + }); + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData with errorPolicy: "none"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'none', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'none', + }); + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + // TODO: https://github.com/apollographql/apollo-client/issues/10893 + // it('returns DeepPartial with returnPartialData: true', () => { + // }); + + // TODO: https://github.com/apollographql/apollo-client/issues/10893 + // it('returns TData with returnPartialData: false', () => { + // }); + + // TODO: https://github.com/apollographql/apollo-client/issues/10893 + // it('returns TData when passing an option that does not affect TData', () => { + // }); + + // TODO: https://github.com/apollographql/apollo-client/issues/10893 + // it('handles combinations of options', () => { + // }); + + // TODO: https://github.com/apollographql/apollo-client/issues/10893 + // it('returns correct TData type when combined options that do not affect TData', () => { + // }); }); }); diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 404f313b0c6..e278cfeaf47 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -6928,17 +6928,17 @@ describe('useSuspenseQuery', () => { it('returns TData | undefined with errorPolicy: "all"', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { errorPolicy: 'all', }); expectTypeOf(inferred).toEqualTypeOf(); expectTypeOf(inferred).not.toEqualTypeOf(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: 'all', }); @@ -6949,17 +6949,17 @@ describe('useSuspenseQuery', () => { it('returns TData with errorPolicy: "none"', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { errorPolicy: 'none', }); expectTypeOf(inferred).toEqualTypeOf(); expectTypeOf(inferred).not.toEqualTypeOf(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: 'none', }); @@ -6970,17 +6970,17 @@ describe('useSuspenseQuery', () => { it('returns DeepPartial with returnPartialData: true', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { returnPartialData: true, }); expectTypeOf(inferred).toEqualTypeOf>(); expectTypeOf(inferred).not.toEqualTypeOf(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, }); @@ -7016,10 +7016,7 @@ describe('useSuspenseQuery', () => { it('returns TData when passing an option that does not affect TData', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { fetchPolicy: 'no-cache', }); @@ -7028,7 +7025,10 @@ describe('useSuspenseQuery', () => { DeepPartial >(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { fetchPolicy: 'no-cache', }); @@ -7041,10 +7041,7 @@ describe('useSuspenseQuery', () => { it('handles combinations of options', () => { const { query } = useVariablesQueryCase(); - const { data: inferredPartialDataIgnore } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferredPartialDataIgnore } = useSuspenseQuery(query, { returnPartialData: true, errorPolicy: 'ignore', }); @@ -7056,7 +7053,10 @@ describe('useSuspenseQuery', () => { inferredPartialDataIgnore ).not.toEqualTypeOf(); - const { data: explicitPartialDataIgnore } = useSuspenseQuery(query, { + const { data: explicitPartialDataIgnore } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: 'ignore', }); @@ -7068,10 +7068,7 @@ describe('useSuspenseQuery', () => { explicitPartialDataIgnore ).not.toEqualTypeOf(); - const { data: inferredPartialDataNone } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferredPartialDataNone } = useSuspenseQuery(query, { returnPartialData: true, errorPolicy: 'none', }); @@ -7083,7 +7080,10 @@ describe('useSuspenseQuery', () => { inferredPartialDataNone ).not.toEqualTypeOf(); - const { data: explicitPartialDataNone } = useSuspenseQuery(query, { + const { data: explicitPartialDataNone } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: 'none', }); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index db43c4a1c13..79e91cf93be 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -6,10 +6,7 @@ import type { } from '../../core'; import { useApolloClient } from './useApolloClient'; import type { QueryReference } from '../cache/QueryReference'; -import type { - SuspenseQueryHookOptions, - ObservableQueryFields, -} from '../types/types'; +import type { SuspenseQueryHookOptions, NoInfer } from '../types/types'; import { __use } from './internal'; import { useSuspenseCache } from './useSuspenseCache'; import { @@ -19,21 +16,104 @@ import { } from './useSuspenseQuery'; import type { FetchMoreFunction, RefetchFunction } from './useSuspenseQuery'; import { canonicalStringify } from '../../cache'; +import type { DeepPartial } from '../../utilities'; import { invariant } from '../../utilities/globals'; export type UseBackgroundQueryResult< - TData = any, + TData = unknown, TVariables extends OperationVariables = OperationVariables > = [ QueryReference, { fetchMore: FetchMoreFunction; - refetch: ObservableQueryFields['refetch']; + refetch: RefetchFunction; } ]; export function useBackgroundQuery< - TData = any, + TData, + TVariables extends OperationVariables, + TOptions extends Omit< + SuspenseQueryHookOptions, + 'variables' | 'returnPartialData' | 'refetchWritePolicy' + > +>( + query: DocumentNode | TypedDocumentNode, + options?: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > & + TOptions +): UseBackgroundQueryResult< + TOptions['errorPolicy'] extends 'ignore' | 'all' + ? // TODO: support `returnPartialData` | `refetchWritePolicy` + // see https://github.com/apollographql/apollo-client/issues/10893 + // TOptions['returnPartialData'] extends true + // ? DeepPartial | undefined + // : TData | undefined + TData | undefined + : // : TOptions['returnPartialData'] extends true + // ? DeepPartial + TData, + TVariables +>; + +export function useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables +>( + query: DocumentNode | TypedDocumentNode, + options: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > & { + returnPartialData: true; + errorPolicy: 'ignore' | 'all'; + } +): UseBackgroundQueryResult | undefined, TVariables>; + +export function useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables +>( + query: DocumentNode | TypedDocumentNode, + options: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > & { + errorPolicy: 'ignore' | 'all'; + } +): UseBackgroundQueryResult; + +// TODO: support `returnPartialData` | `refetchWritePolicy` +// see https://github.com/apollographql/apollo-client/issues/10893 + +// export function useBackgroundQuery< +// TData = unknown, +// TVariables extends OperationVariables = OperationVariables +// >( +// query: DocumentNode | TypedDocumentNode, +// options: Omit< +// SuspenseQueryHookOptions, NoInfer>, +// 'returnPartialData' | 'refetchWritePolicy' +// > & { +// returnPartialData: true; +// } +// ): UseBackgroundQueryResult, TVariables>; + +export function useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables +>( + query: DocumentNode | TypedDocumentNode, + options?: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > +): UseBackgroundQueryResult; + +export function useBackgroundQuery< + TData = unknown, TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index e381e590102..58144811c83 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -61,8 +61,6 @@ export type SubscribeToMoreFunction< TVariables extends OperationVariables > = ObservableQueryFields['subscribeToMore']; -export type Version = 'main' | 'network'; - export function useSuspenseQuery< TData, TVariables extends OperationVariables, diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 64d0a6b9b7d..863cf27552b 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -20,6 +20,10 @@ import type { } from '../../core'; import type { SuspenseCache } from '../cache'; +/* QueryReference type */ + +export type { QueryReference } from '../cache/QueryReference'; + /* Common types */ export type { DefaultContext as Context } from "../../core";