diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 8a6f21665bd..267adc1a330 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2101,11 +2101,17 @@ export function useQuery(rv: ReactiveVar): T; // @public (undocumented) -export function useReadQuery(queryRef: QueryReference): { +export function useReadQuery(queryRef: QueryReference): UseReadQueryResult; + +// @public (undocumented) +export interface UseReadQueryResult { + // (undocumented) data: TData; - networkStatus: NetworkStatus; + // (undocumented) error: ApolloError | undefined; -}; + // (undocumented) + networkStatus: NetworkStatus; +} // @public (undocumented) export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index cf0f649ed18..110739c8df9 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -1988,11 +1988,17 @@ export function useQuery(rv: ReactiveVar): T; // @public (undocumented) -export function useReadQuery(queryRef: QueryReference): { +export function useReadQuery(queryRef: QueryReference): UseReadQueryResult; + +// @public (undocumented) +export interface UseReadQueryResult { + // (undocumented) data: TData; - networkStatus: NetworkStatus; + // (undocumented) error: ApolloError | undefined; -}; + // (undocumented) + networkStatus: NetworkStatus; +} // Warning: (ae-forgotten-export) The symbol "SubscriptionHookOptions" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index e3f49b900c0..3004669da51 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2742,11 +2742,17 @@ export function useQuery(rv: ReactiveVar): T; // @public (undocumented) -export function useReadQuery(queryRef: QueryReference): { +export function useReadQuery(queryRef: QueryReference): UseReadQueryResult; + +// @public (undocumented) +export interface UseReadQueryResult { + // (undocumented) data: TData; - networkStatus: NetworkStatus; + // (undocumented) error: ApolloError | undefined; -}; + // (undocumented) + networkStatus: NetworkStatus; +} // @public (undocumented) export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; diff --git a/.changeset/good-experts-repair.md b/.changeset/good-experts-repair.md new file mode 100644 index 00000000000..37aef92f934 --- /dev/null +++ b/.changeset/good-experts-repair.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add an explicit return type for the `useReadQuery` hook called `UseReadQueryResult`. Previously the return type of this hook was inferred from the return value. diff --git a/.circleci/config.yml b/.circleci/config.yml index 84e1015d438..e6f85eb6a75 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - secops: apollo/circleci-secops-orb@2.0.0 + secops: apollo/circleci-secops-orb@2.0.1 jobs: # Filesize: diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx index 06a69d5db25..9821626749c 100644 --- a/docs/source/development-testing/testing.mdx +++ b/docs/source/development-testing/testing.mdx @@ -169,6 +169,9 @@ We can use the asynchronous `screen.findByText` method to query the DOM elements ```jsx it("should render dog", async () => { const dogMock = { + delay: 30 // to prevent React from batching the loading state away + // delay: Infinity // if you only want to test the loading state + request: { query: GET_DOG_QUERY, variables: { name: "Buck" } diff --git a/renovate.json b/renovate.json index 9264bc08977..ac7f81ee4b8 100644 --- a/renovate.json +++ b/renovate.json @@ -33,6 +33,7 @@ "react-dom-17", "@testing-library/react-12", "@rollup/plugin-node-resolve", - "rollup" + "rollup", + "glob" ] } diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 3aace0d3e9d..61d50665cac 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,6 +11,7 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; +export type { UseReadQueryResult } from "./useReadQuery.js"; export { useReadQuery } from "./useReadQuery.js"; export { skipToken } from "./constants.js"; export type { SkipToken } from "./constants.js"; diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 85d4b3026f1..e6a97e1446f 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -5,8 +5,37 @@ import { __use } from "./internal/index.js"; import { toApolloError } from "./useSuspenseQuery.js"; import { invariant } from "../../utilities/globals/index.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; +import type { ApolloError } from "../../errors/index.js"; +import type { NetworkStatus } from "../../core/index.js"; -export function useReadQuery(queryRef: QueryReference) { +export interface UseReadQueryResult { + /** + * An object containing the result of your GraphQL query after it completes. + * + * This value might be `undefined` if a query results in one or more errors + * (depending on the query's `errorPolicy`). + */ + data: TData; + /** + * If the query produces one or more errors, this object contains either an + * array of `graphQLErrors` or a single `networkError`. Otherwise, this value + * is `undefined`. + * + * This property can be ignored when using the default `errorPolicy` or an + * `errorPolicy` of `none`. The hook will throw the error instead of setting + * this property. + */ + error: ApolloError | undefined; + /** + * A number indicating the current network state of the query's associated + * request. {@link https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/core/networkStatus.ts#L4 | See possible values}. + */ + networkStatus: NetworkStatus; +} + +export function useReadQuery( + queryRef: QueryReference +): UseReadQueryResult { const internalQueryRef = unwrapQueryRef(queryRef); invariant( internalQueryRef.promiseCache, diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index 8dd2b3be043..eb18f3b3a9f 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -1,6 +1,6 @@ import React from "react"; import { DocumentNode } from "graphql"; -import { render, waitFor } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import gql from "graphql-tag"; import { itAsync, MockedResponse, MockLink } from "../../core"; @@ -708,6 +708,70 @@ describe("General use", () => { }).then(resolve, reject); } ); + + it("should support loading state testing with delay", async () => { + function Component({ username }: Variables) { + const { loading, data } = useQuery(query, { variables }); + + if (loading || data === undefined) return

Loading the user ID...

; + + return

The user ID is '{data.user.id}'

; + } + + const mocks: ReadonlyArray = [ + { + delay: 30, // prevent React from batching the loading state away + request: { + query, + variables, + }, + result: { data: { user } }, + }, + ]; + + render( + + + + ); + + expect( + await screen.findByText("Loading the user ID...") + ).toBeInTheDocument(); + expect( + await screen.findByText("The user ID is 'user_id'") + ).toBeInTheDocument(); + }); + + it("should support loading state testing with infinite delay", async () => { + function Component({ username }: Variables) { + const { loading, data } = useQuery(query, { variables }); + + if (loading || data === undefined) return

Loading the user ID...

; + + return

The user ID is '{data.user.id}'

; + } + + const mocks: ReadonlyArray = [ + { + delay: Infinity, // keep loading forever. + request: { + query, + variables, + }, + }, + ]; + + render( + + + + ); + + expect( + await screen.findByText("Loading the user ID...") + ).toBeInTheDocument(); + }); }); describe("@client testing", () => {