Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a previousData property in useQuery/useLazyQuery results #7082

Merged
merged 6 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

- In addition to the `result.data` property, `useQuery` and `useLazyQuery` will now provide a `result.previousData` property, which can be useful when a network request is pending and `result.data` is undefined, since `result.previousData` can be rendered instead of rendering an empty/loading state. <br/>
[@hwillson](https://github.com/hwillson) in [#7082](https://github.com/apollographql/apollo-client/pull/7082)

## 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.cjs.min.js",
"maxSize": "25 kB"
"maxSize": "25.25 kB"
}
],
"peerDependencies": {
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
63 changes: 42 additions & 21 deletions src/react/data/QueryData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { equal } from '@wry/equality';
import { ApolloError } from '../../errors';

import {
ApolloClient,
NetworkStatus,
FetchMoreQueryOptions,
SubscribeToMoreOptions,
ObservableQuery,
FetchMoreOptions,
UpdateQueryOptions
UpdateQueryOptions,
DocumentNode,
TypedDocumentNode
} from '../../core';

import {
Expand All @@ -18,7 +21,6 @@ import {
import { DocumentType } from '../parser';
import {
QueryResult,
QueryPreviousData,
QueryDataOptions,
QueryTuple,
QueryLazyOptions,
Expand All @@ -28,12 +30,19 @@ import { OperationData } from './OperationData';

export class QueryData<TData, TVariables> extends OperationData {
public onNewData: () => void;

private previousData: QueryPreviousData<TData, TVariables> = {};
private currentObservable?: ObservableQuery<TData, TVariables>;
private currentSubscription?: ObservableSubscription;
private runLazy: boolean = false;
private lazyOptions?: QueryLazyOptions<TVariables>;
private previous: {
client?: ApolloClient<object>;
query?: DocumentNode | TypedDocumentNode<TData, TVariables>;
observableQueryOptions?: {};
result?: QueryResult<TData, TVariables>;
loading?: boolean;
options?: QueryDataOptions<TData, TVariables>;
error?: ApolloError;
} = Object.create(null);

constructor({
options,
Expand All @@ -52,9 +61,9 @@ export class QueryData<TData, TVariables> extends OperationData {
this.refreshClient();

const { skip, query } = this.getOptions();
if (skip || query !== this.previousData.query) {
if (skip || query !== this.previous.query) {
this.removeQuerySubscription();
this.previousData.query = query;
this.previous.query = query;
}

this.updateObservableQuery();
Expand Down Expand Up @@ -99,7 +108,7 @@ export class QueryData<TData, TVariables> extends OperationData {
public cleanup() {
this.removeQuerySubscription();
delete this.currentObservable;
delete this.previousData.result;
delete this.previous.result;
}

public getOptions() {
Expand Down Expand Up @@ -158,7 +167,7 @@ export class QueryData<TData, TVariables> extends OperationData {
// If SSR has been explicitly disabled, and this function has been called
// on the server side, return the default loading state.
if (ssrDisabled && (this.ssrInitiated() || fetchDisabled)) {
this.previousData.result = ssrLoading;
this.previous.result = ssrLoading;
return ssrLoading;
}

Expand Down Expand Up @@ -209,7 +218,7 @@ export class QueryData<TData, TVariables> extends OperationData {
if (!this.currentObservable) {
const observableQueryOptions = this.prepareObservableQueryOptions();

this.previousData.observableQueryOptions = {
this.previous.observableQueryOptions = {
...observableQueryOptions,
children: null
};
Expand Down Expand Up @@ -243,10 +252,10 @@ export class QueryData<TData, TVariables> extends OperationData {
if (
!equal(
newObservableQueryOptions,
this.previousData.observableQueryOptions
this.previous.observableQueryOptions
)
) {
this.previousData.observableQueryOptions = newObservableQueryOptions;
this.previous.observableQueryOptions = newObservableQueryOptions;
this.currentObservable
.setOptions(newObservableQueryOptions)
// The error will be passed to the child container, so we don't
Expand All @@ -268,7 +277,7 @@ export class QueryData<TData, TVariables> extends OperationData {

this.currentSubscription = this.currentObservable!.subscribe({
next: ({ loading, networkStatus, data }) => {
const previousResult = this.previousData.result;
const previousResult = this.previous.result;

// Make sure we're not attempting to re-render similar results
if (
Expand All @@ -286,12 +295,12 @@ export class QueryData<TData, TVariables> extends OperationData {
this.resubscribeToQuery();
if (!error.hasOwnProperty('graphQLErrors')) throw error;

const previousResult = this.previousData.result;
const previousResult = this.previous.result;
if (
(previousResult && previousResult.loading) ||
!equal(error, this.previousData.error)
!equal(error, this.previous.error)
) {
this.previousData.error = error;
this.previous.error = error;
onNewData();
}
}
Expand Down Expand Up @@ -401,9 +410,21 @@ export class QueryData<TData, TVariables> extends OperationData {
result.client = this.client;
// Store options as this.previousOptions.
this.setOptions(options, true);
this.previousData.loading =
this.previousData.result && this.previousData.result.loading || false;
this.previousData.result = result;

const previousResult = this.previous.result;

this.previous.loading =
previousResult && previousResult.loading || false;

// Ensure the returned result contains previousData as a separate
// property, to give developers the flexibility of leveraging outdated
// data while new data is loading from the network. Falling back to
// previousResult.previousData when previousResult.data is falsy here
// allows result.previousData to persist across multiple results.
result.previousData = previousResult &&
(previousResult.data || previousResult.previousData);

this.previous.result = result;

// Any query errors that exist are now available in `result`, so we'll
// remove the original errors from the `ObservableQuery` query store to
Expand All @@ -415,9 +436,9 @@ export class QueryData<TData, TVariables> extends OperationData {
}

private handleErrorOrCompleted() {
if (!this.currentObservable || !this.previousData.result) return;
if (!this.currentObservable || !this.previous.result) return;

const { data, loading, error } = this.previousData.result;
const { data, loading, error } = this.previous.result;

if (!loading) {
const {
Expand All @@ -431,7 +452,7 @@ export class QueryData<TData, TVariables> extends OperationData {
// No changes, so we won't call onError/onCompleted.
if (
this.previousOptions &&
!this.previousData.loading &&
!this.previous.loading &&
equal(this.previousOptions.query, query) &&
equal(this.previousOptions.variables, variables)
) {
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);
});
});
Loading