From bc7e533c12754112fec0d14e04b6d241ca13178d Mon Sep 17 00:00:00 2001 From: Danny Cochran Date: Fri, 11 Dec 2020 10:11:06 -0800 Subject: [PATCH] Feature: support client.refetchQueries for refetching queries imperatively (#7431) --- docs/source/api/core/ApolloClient.mdx | 1 + src/__tests__/client.ts | 13 ++++ src/core/ApolloClient.ts | 19 +++++ src/core/QueryManager.ts | 63 ++++++++-------- src/core/__tests__/QueryManager/index.ts | 91 ++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 28 deletions(-) diff --git a/docs/source/api/core/ApolloClient.mdx b/docs/source/api/core/ApolloClient.mdx index 992b4c903f2..a32ae6b9ced 100644 --- a/docs/source/api/core/ApolloClient.mdx +++ b/docs/source/api/core/ApolloClient.mdx @@ -101,6 +101,7 @@ different value for the same option in individual function calls. + ## Types diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 9b38bd585cb..dcf4f745c8c 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -2426,6 +2426,19 @@ describe('client', () => { expect(spy).toHaveBeenCalled(); }); + it('has a refetchQueries method which calls QueryManager', async () => { + // TODO(dannycochran) + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache(), + }); + + // @ts-ignore + const spy = jest.spyOn(client.queryManager, 'refetchQueries'); + await client.refetchQueries(['Author1']); + expect(spy).toHaveBeenCalled(); + }); + itAsync('should propagate errors from network interface to observers', (resolve, reject) => { const link = ApolloLink.from([ () => diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 70017b859a5..ec183fa7f0d 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -22,6 +22,7 @@ import { MutationOptions, SubscriptionOptions, WatchQueryFetchPolicy, + RefetchQueryDescription, } from './watchQueryOptions'; import { @@ -516,6 +517,24 @@ export class ApolloClient implements DataProxy { return this.queryManager.reFetchObservableQueries(includeStandby); } + /** + * Refetches specified active queries. Similar to "reFetchObservableQueries()" but with a specific list of queries. + * + * `refetchQueries()` is useful for use cases to imperatively refresh a selection of queries. + * + * It is important to remember that `refetchQueries()` *will* refetch specified active + * queries. This means that any components that might be mounted will execute + * their queries again using your network interface. If you do not want to + * re-execute any queries then you should make sure to stop watching any + * active queries. + * Takes optional parameter `includeStandby` which will include queries in standby-mode when refetching. + */ + public refetchQueries( + queries: RefetchQueryDescription, + ): Promise[]> { + return Promise.all(this.queryManager.refetchQueries(queries)); + } + /** * Exposes the cache's complete state, in a serializable format for later restoration. */ diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 827b91b7128..abb70d60844 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -28,6 +28,7 @@ import { MutationOptions, WatchQueryFetchPolicy, ErrorPolicy, + RefetchQueryDescription, } from './watchQueryOptions'; import { ObservableQuery } from './ObservableQuery'; import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus'; @@ -263,34 +264,7 @@ export class QueryManager { refetchQueries = refetchQueries(storeResult!); } - const refetchQueryPromises: Promise< - ApolloQueryResult[] | ApolloQueryResult<{}> - >[] = []; - - if (isNonEmptyArray(refetchQueries)) { - refetchQueries.forEach(refetchQuery => { - if (typeof refetchQuery === 'string') { - self.queries.forEach(({ observableQuery }) => { - if (observableQuery && - observableQuery.queryName === refetchQuery) { - refetchQueryPromises.push(observableQuery.refetch()); - } - }); - } else { - const queryOptions: QueryOptions = { - query: refetchQuery.query, - variables: refetchQuery.variables, - fetchPolicy: 'network-only', - }; - - if (refetchQuery.context) { - queryOptions.context = refetchQuery.context; - } - - refetchQueryPromises.push(self.query(queryOptions)); - } - }); - } + const refetchQueryPromises = self.refetchQueries(refetchQueries); Promise.all( awaitRefetchQueries ? refetchQueryPromises : [], @@ -1023,6 +997,39 @@ export class QueryManager { return concast; } + public refetchQueries(queries: RefetchQueryDescription): + Promise>[] { + const self = this; + const refetchQueryPromises: Promise>[] = []; + + if (isNonEmptyArray(queries)) { + queries.forEach(refetchQuery => { + if (typeof refetchQuery === 'string') { + self.queries.forEach(({ observableQuery }) => { + if (observableQuery && + observableQuery.queryName === refetchQuery) { + refetchQueryPromises.push(observableQuery.refetch()); + } + }); + } else { + const queryOptions: QueryOptions = { + query: refetchQuery.query, + variables: refetchQuery.variables, + fetchPolicy: 'network-only', + }; + + if (refetchQuery.context) { + queryOptions.context = refetchQuery.context; + } + + refetchQueryPromises.push(self.query(queryOptions)); + } + }); + } + + return refetchQueryPromises; + } + private fetchQueryByPolicy( queryInfo: QueryInfo, options: WatchQueryOptions, diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index db5669aa2f1..26e052834e9 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -4243,6 +4243,97 @@ describe('QueryManager', () => { }); }); + describe('refetching specified queries', () => { + itAsync('returns a promise resolving when all queries have been refetched', (resolve, reject) => { + const query = gql` + query GetAuthor { + author { + firstName + lastName + } + } + `; + + const data = { + author: { + firstName: 'John', + lastName: 'Smith', + }, + }; + + const dataChanged = { + author: { + firstName: 'John changed', + lastName: 'Smith', + }, + }; + + const query2 = gql` + query GetAuthor2 { + author2 { + firstName + lastName + } + } + `; + + const data2 = { + author2: { + firstName: 'John', + lastName: 'Smith', + }, + }; + + const data2Changed = { + author2: { + firstName: 'John changed', + lastName: 'Smith', + }, + }; + + const queryManager = createQueryManager({ + link: mockSingleLink({ + request: { query }, + result: { data }, + }, { + request: { query: query2 }, + result: { data: data2 }, + }, { + request: { query }, + result: { data: dataChanged }, + }, { + request: { query: query2 }, + result: { data: data2Changed }, + }).setOnError(reject), + }); + + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); + + return Promise.all([ + observableToPromise({ observable }, result => + expect(stripSymbols(result.data)).toEqual(data), + ), + observableToPromise({ observable: observable2 }, result => + expect(stripSymbols(result.data)).toEqual(data2), + ), + ]).then(() => { + observable.subscribe({ next: () => null }); + observable2.subscribe({ next: () => null }); + + return Promise.all(queryManager.refetchQueries(['GetAuthor', 'GetAuthor2'])).then(() => { + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(stripSymbols(result.data)).toEqual(dataChanged); + + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(stripSymbols(result2.data)).toEqual(data2Changed); + }); + }).then(resolve, reject); + }); + }); + describe('loading state', () => { itAsync('should be passed as false if we are not watching a query', (resolve, reject) => { const query = gql`