From b211f4b8a7876aa6c1af2c29eba77cafa72daf41 Mon Sep 17 00:00:00 2001 From: hwillson Date: Sat, 26 Sep 2020 20:12:58 -0400 Subject: [PATCH] Provide a previousData property in useQuery/useLazyQuery results Alongside their returned `data` property, `useQuery` and `useLazyQuery` now also return a `previousData` property. Before a new `data` value is set, its current value is stored in `previousData`. This allows more fine-grained control over component loading states, where developers might want to leverage previous data until new data has fully loaded. Fixes #6603 --- CHANGELOG.md | 3 + docs/shared/query-result.mdx | 1 + .../client/__snapshots__/Query.test.tsx.snap | 1 + src/react/data/QueryData.ts | 6 ++ .../hooks/__tests__/useLazyQuery.test.tsx | 85 ++++++++++++++++++- src/react/hooks/__tests__/useQuery.test.tsx | 78 +++++++++++++++++ src/react/types/types.ts | 2 + 7 files changed, 175 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa43090a6fa..1640b627030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ - Avoid displaying `Cache data may be lost...` warnings for scalar field values that happen to be objects, such as JSON data.
[@benjamn](https://github.com/benjamn) in [#7075](https://github.com/apollographql/apollo-client/pull/7075) +- Alongside their returned `data` property, `useQuery` and `useLazyQuery` now also return a `previousData` property. Before a new `data` value is set, its current value is stored in `previousData`. This allows more fine-grained control over component loading states, where you might want to leverage previous data until new data has fully loaded.
+ [@hwillson](https://github.com/hwillson) in [#X](https://github.com/apollographql/apollo-client/pull/X) + ## Apollo Client 3.2.1 ## Bug Fixes diff --git a/docs/shared/query-result.mdx b/docs/shared/query-result.mdx index 623036c7757..d52f894b57d 100644 --- a/docs/shared/query-result.mdx +++ b/docs/shared/query-result.mdx @@ -1,6 +1,7 @@ | Property | Type | Description | | - | - | - | | `data` | TData | An object containing the result of your GraphQL query. Defaults to `undefined`. | +| `previousData` | TData | An object containing the previous result of your GraphQL query (the last result before a new `data` value was set). Defaults to `undefined`. | | `loading` | boolean | A boolean that indicates whether the request is in flight | | `error` | ApolloError | A runtime error with `graphQLErrors` and `networkError` properties | | `variables` | { [key: string]: any } | An object containing the variables the query was called with | diff --git a/src/react/components/__tests__/client/__snapshots__/Query.test.tsx.snap b/src/react/components/__tests__/client/__snapshots__/Query.test.tsx.snap index 44acb4cc106..8e19a2575ff 100644 --- a/src/react/components/__tests__/client/__snapshots__/Query.test.tsx.snap +++ b/src/react/components/__tests__/client/__snapshots__/Query.test.tsx.snap @@ -26,6 +26,7 @@ Object { "fetchMore": [Function], "loading": true, "networkStatus": 1, + "previousData": undefined, "refetch": [Function], "startPolling": [Function], "stopPolling": [Function], diff --git a/src/react/data/QueryData.ts b/src/react/data/QueryData.ts index 4de10ff7c1b..58582e781f5 100644 --- a/src/react/data/QueryData.ts +++ b/src/react/data/QueryData.ts @@ -403,6 +403,12 @@ export class QueryData extends OperationData { this.setOptions(options, true); this.previousData.loading = this.previousData.result && this.previousData.result.loading || false; + + // Ensure the returned result contains previous data as a separate + // property, to give developers the flexibility of leveraging previous + // data when new data is being loaded. + result.previousData = this.previousData.result?.data; + this.previousData.result = result; // Any query errors that exist are now available in `result`, so we'll diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index 568a2013b59..968070c2b9e 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -6,7 +6,7 @@ import { render, wait } from '@testing-library/react'; import { ApolloClient } from '../../../core'; import { InMemoryCache } from '../../../cache'; import { ApolloProvider } from '../../context'; -import { MockedProvider } from '../../../testing'; +import { itAsync, MockedProvider } from '../../../testing'; import { useLazyQuery } from '../useLazyQuery'; describe('useLazyQuery Hook', () => { @@ -391,4 +391,87 @@ describe('useLazyQuery Hook', () => { }); } ); + + itAsync('should persist previous data when a query is re-run', (resolve, reject) => { + const query = gql` + query car { + car { + id + make + } + } + `; + + const data1 = { + car: { + id: 1, + make: 'Venturi', + __typename: 'Car', + } + }; + + const data2 = { + car: { + id: 2, + make: 'Wiesmann', + __typename: 'Car', + } + }; + + const mocks = [ + { request: { query }, result: { data: data1 } }, + { request: { query }, result: { data: data2 } } + ]; + + let renderCount = 0; + function App() { + const [execute, { loading, data, previousData, refetch }] = useLazyQuery( + query, + { notifyOnNetworkStatusChange: true }, + ); + + switch (++renderCount) { + case 1: + expect(loading).toEqual(false); + expect(data).toBeUndefined(); + expect(previousData).toBeUndefined(); + setTimeout(execute); + break; + case 2: + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + expect(previousData).toBeUndefined(); + break; + case 3: + expect(loading).toBeFalsy(); + expect(data).toEqual(data1); + expect(previousData).toBeUndefined(); + setTimeout(refetch!); + break; + case 4: + expect(loading).toBeTruthy(); + expect(data).toEqual(data1); + expect(previousData).toEqual(data1); + break; + case 5: + expect(loading).toBeFalsy(); + expect(data).toEqual(data2); + expect(previousData).toEqual(data1); + break; + default: // Do nothing + } + + return null; + } + + render( + + + + ); + + return wait(() => { + expect(renderCount).toBe(5); + }).then(resolve, reject); + }); }); diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 462bd4a6e93..829edeb0fe5 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -2226,4 +2226,82 @@ describe('useQuery Hook', () => { }).then(resolve, reject); }); }); + + describe('Previous data', () => { + itAsync('should persist previous data when a query is re-run', (resolve, reject) => { + const query = gql` + query car { + car { + id + make + } + } + `; + + const data1 = { + car: { + id: 1, + make: 'Venturi', + __typename: 'Car', + } + }; + + const data2 = { + car: { + id: 2, + make: 'Wiesmann', + __typename: 'Car', + } + }; + + const mocks = [ + { request: { query }, result: { data: data1 } }, + { request: { query }, result: { data: data2 } } + ]; + + let renderCount = 0; + function App() { + const { loading, data, previousData, refetch } = useQuery(query, { + notifyOnNetworkStatusChange: true, + }); + + switch (++renderCount) { + case 1: + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + expect(previousData).toBeUndefined(); + break; + case 2: + expect(loading).toBeFalsy(); + expect(data).toEqual(data1); + expect(previousData).toBeUndefined(); + setTimeout(refetch); + break; + case 3: + expect(loading).toBeTruthy(); + expect(data).toEqual(data1); + expect(previousData).toEqual(data1); + break; + case 4: + expect(loading).toBeFalsy(); + expect(data).toEqual(data2); + expect(previousData).toEqual(data1); + break; + default: // Do nothing + } + + return null; + } + + render( + + + + ); + + return wait(() => { + expect(renderCount).toBe(4); + }).then(resolve, reject); + }); + }); }); diff --git a/src/react/types/types.ts b/src/react/types/types.ts index cbd52ad284f..d333285cff0 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -81,6 +81,7 @@ export interface QueryResult extends ObservableQueryFields { client: ApolloClient; data: TData | undefined; + previousData?: TData; error?: ApolloError; loading: boolean; networkStatus: NetworkStatus; @@ -125,6 +126,7 @@ type UnexecutedLazyFields = { networkStatus: NetworkStatus.ready; called: false; data: undefined; + previousData?: undefined; } type Impartial = {