Skip to content

Commit

Permalink
Provide a previousData property in useQuery/useLazyQuery results
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hwillson committed Sep 27, 2020
1 parent 7103996 commit b211f4b
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br/>
[@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. <br/>
[@hwillson](https://github.com/hwillson) in [#X](https://github.com/apollographql/apollo-client/pull/X)

## Apollo Client 3.2.1

## Bug Fixes
Expand Down
1 change: 1 addition & 0 deletions docs/shared/query-result.mdx
Original file line number Diff line number Diff line change
@@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Object {
"fetchMore": [Function],
"loading": true,
"networkStatus": 1,
"previousData": undefined,
"refetch": [Function],
"startPolling": [Function],
"stopPolling": [Function],
Expand Down
6 changes: 6 additions & 0 deletions src/react/data/QueryData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,12 @@ export class QueryData<TData, TVariables> 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
Expand Down
85 changes: 84 additions & 1 deletion src/react/hooks/__tests__/useLazyQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
<MockedProvider mocks={mocks}>
<App />
</MockedProvider>
);

return wait(() => {
expect(renderCount).toBe(5);
}).then(resolve, reject);
});
});
78 changes: 78 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<MockedProvider mocks={mocks}>
<App />
</MockedProvider>
);

return wait(() => {
expect(renderCount).toBe(4);
}).then(resolve, reject);
});
});
});
2 changes: 2 additions & 0 deletions src/react/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface QueryResult<TData = any, TVariables = OperationVariables>
extends ObservableQueryFields<TData, TVariables> {
client: ApolloClient<any>;
data: TData | undefined;
previousData?: TData;
error?: ApolloError;
loading: boolean;
networkStatus: NetworkStatus;
Expand Down Expand Up @@ -125,6 +126,7 @@ type UnexecutedLazyFields = {
networkStatus: NetworkStatus.ready;
called: false;
data: undefined;
previousData?: undefined;
}

type Impartial<T> = {
Expand Down

0 comments on commit b211f4b

Please sign in to comment.