diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 7db43eaeceb..6cc07be8ffd 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -20,6 +20,7 @@ import { isNonEmptyArray } from '../utilities/common/arrays'; export type ApolloCurrentQueryResult = ApolloQueryResult & { error?: ApolloError; + partial?: boolean; }; export interface FetchMoreOptions< @@ -130,9 +131,18 @@ export class ObservableQuery< }); } + /** + * Return the result of the query from the local cache as well as some fetching status + * `loading` and `networkStatus` allow to know if a request is in flight + * `partial` lets you know if the result from the local cache is complete or partial + */ public getCurrentResult(): ApolloCurrentQueryResult { - const { lastResult, lastError } = this; - const { fetchPolicy } = this.options; + const { + lastResult, + lastError, + options: { fetchPolicy }, + } = this; + const isNetworkFetchPolicy = fetchPolicy === 'network-only' || fetchPolicy === 'no-cache'; @@ -155,6 +165,9 @@ export class ObservableQuery< return result; } + const { data, partial } = this.queryManager.getCurrentQueryResult(this); + Object.assign(result, { data, partial }); + const queryStoreValue = this.queryManager.queryStore.get(this.queryId); if (queryStoreValue) { const { networkStatus } = queryStoreValue; @@ -191,9 +204,27 @@ export class ObservableQuery< if (queryStoreValue.graphQLErrors && this.options.errorPolicy === 'all') { result.errors = queryStoreValue.graphQLErrors; } + + } else { + // We need to be careful about the loading state we show to the user, to try + // and be vaguely in line with what the user would have seen from .subscribe() + // but to still provide useful information synchronously when the query + // will not end up hitting the server. + // See more: https://github.com/apollographql/apollo-client/issues/707 + // Basically: is there a query in flight right now? + const loading = isNetworkFetchPolicy || + (partial && fetchPolicy !== 'cache-only'); + + Object.assign(result, { + loading, + networkStatus: loading ? NetworkStatus.loading : NetworkStatus.ready, + }); } - this.updateLastResult(result); + if (!partial) { + result.stale = false; + this.updateLastResult(result); + } return result; } diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index f6cb7a57702..391476e2dc2 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1052,7 +1052,7 @@ export class QueryManager { this.queries.delete(queryId); } - private getCurrentQueryResult( + public getCurrentQueryResult( observableQuery: ObservableQuery, optimistic: boolean = true, ): { diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index 07dfe0dbfcd..e9846c9f83a 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -1450,6 +1450,7 @@ describe('ObservableQuery', () => { loading: false, networkStatus: 7, stale: false, + partial: false, }); resolve(); }); @@ -1459,6 +1460,7 @@ describe('ObservableQuery', () => { data: undefined, networkStatus: 1, stale: false, + partial: true, }); setTimeout( @@ -1468,12 +1470,40 @@ describe('ObservableQuery', () => { data: undefined, networkStatus: 1, stale: false, + partial: true, }); }), 0, ); }); + itAsync('returns results from the store immediately', (resolve, reject) => { + const queryManager = mockQueryManager(reject, { + request: { query, variables }, + result: { data: dataOne }, + }); + + return queryManager.query({ query, variables }).then((result: any) => { + expect(stripSymbols(result)).toEqual({ + data: dataOne, + loading: false, + networkStatus: 7, + stale: false, + }); + const observable = queryManager.watchQuery({ + query, + variables, + }); + expect(stripSymbols(observable.getCurrentResult())).toEqual({ + data: dataOne, + loading: true, + networkStatus: NetworkStatus.loading, + stale: false, + partial: false, + }); + }).then(resolve, reject); + }); + itAsync('returns errors from the store immediately', (resolve, reject) => { const queryManager = mockQueryManager(reject, { request: { query, variables }, @@ -1563,7 +1593,7 @@ describe('ObservableQuery', () => { }).then(resolve, reject); }); - itAsync('returns partial data from the store', (resolve, reject) => { + itAsync('returns partial data from the store immediately', (resolve, reject) => { const superQuery = gql` query superQuery($id: ID!) { people_one(id: $id) { @@ -1600,10 +1630,11 @@ describe('ObservableQuery', () => { }); expect(observable.getCurrentResult()).toEqual({ - data: void 0, + data: dataOne, loading: true, networkStatus: 1, stale: false, + partial: true, }); // we can use this to trigger the query @@ -1618,28 +1649,19 @@ describe('ObservableQuery', () => { if (handleCount === 1) { expect(subResult).toEqual({ - data: void 0, + data: dataOne, loading: true, networkStatus: 1, stale: false, }); } else if (handleCount === 2) { - expect(subResult).toEqual({ - data: dataOne, - loading: false, - networkStatus: 7, - stale: false, - }); - - } else if (handleCount === 3) { expect(subResult).toEqual({ data: superDataOne, loading: false, networkStatus: 7, stale: false, }); - resolve(); } }); @@ -1670,6 +1692,7 @@ describe('ObservableQuery', () => { loading: true, networkStatus: 1, stale: false, + partial: false, }); subscribeAndCount(reject, observable, (handleCount, subResult) => { @@ -1678,7 +1701,6 @@ describe('ObservableQuery', () => { loading, networkStatus, } = observable.getCurrentResult(); - expect(subResult).toEqual({ data, loading, @@ -1686,22 +1708,13 @@ describe('ObservableQuery', () => { stale: false, }); - if (handleCount === 1) { - expect(stripSymbols(subResult)).toEqual({ - data: void 0, - loading: true, - networkStatus: NetworkStatus.loading, - stale: false, - }); - - } else if (handleCount === 2) { + if (handleCount === 2) { expect(stripSymbols(subResult)).toEqual({ data: dataTwo, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: 7, stale: false, }); - resolve(); } }); @@ -1727,12 +1740,12 @@ describe('ObservableQuery', () => { variables, fetchPolicy: 'no-cache', }); - expect(stripSymbols(observable.getCurrentResult())).toEqual({ data: undefined, loading: true, networkStatus: 1, stale: false, + partial: false, }); subscribeAndCount(reject, observable, (handleCount, subResult) => { @@ -1748,6 +1761,7 @@ describe('ObservableQuery', () => { loading, networkStatus, stale: false, + partial: false, }); } else if (handleCount === 2) { expect(stripSymbols(subResult)).toEqual({ diff --git a/src/react/data/QueryData.ts b/src/react/data/QueryData.ts index fe56dc4f3ef..a1ed59e5c06 100644 --- a/src/react/data/QueryData.ts +++ b/src/react/data/QueryData.ts @@ -353,7 +353,7 @@ export class QueryData extends OperationData { } else { // Fetch the current result (if any) from the store. const currentResult = this.currentObservable.query!.getCurrentResult(); - const { loading, networkStatus, errors } = currentResult; + const { loading, partial, networkStatus, errors } = currentResult; let { error, data } = currentResult; // Until a set naming convention for networkError and graphQLErrors is @@ -390,6 +390,7 @@ export class QueryData extends OperationData { const { partialRefetch } = options; if ( partialRefetch && + partial && (!data || Object.keys(data).length === 0) && fetchPolicy !== 'cache-only' ) {