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..579ce2493a4 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();
+ 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 = {