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

Feature: add ability to refetch certain queries imperatively #7431

Merged
merged 2 commits into from Dec 11, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/api/core/ApolloClient.mdx
Expand Up @@ -101,6 +101,7 @@ different value for the same option in individual function calls.
<TypescriptApiBox name="ApolloClient.onClearStore" />
<TypescriptApiBox name="ApolloClient.stop" />
<TypescriptApiBox name="ApolloClient.reFetchObservableQueries" />
<TypescriptApiBox name="ApolloClient.refetchQueries" />

## Types

Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/client.ts
Expand Up @@ -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([
() =>
Expand Down
19 changes: 19 additions & 0 deletions src/core/ApolloClient.ts
Expand Up @@ -22,6 +22,7 @@ import {
MutationOptions,
SubscriptionOptions,
WatchQueryFetchPolicy,
RefetchQueryDescription,
} from './watchQueryOptions';

import {
Expand Down Expand Up @@ -516,6 +517,24 @@ export class ApolloClient<TCacheShape> 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<ApolloQueryResult<any>[]> {
return Promise.all(this.queryManager.refetchQueries(queries));
}

/**
* Exposes the cache's complete state, in a serializable format for later restoration.
*/
Expand Down
63 changes: 35 additions & 28 deletions src/core/QueryManager.ts
Expand Up @@ -28,6 +28,7 @@ import {
MutationOptions,
WatchQueryFetchPolicy,
ErrorPolicy,
RefetchQueryDescription,
} from './watchQueryOptions';
import { ObservableQuery } from './ObservableQuery';
import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus';
Expand Down Expand Up @@ -263,34 +264,7 @@ export class QueryManager<TStore> {
refetchQueries = refetchQueries(storeResult!);
}

const refetchQueryPromises: Promise<
ApolloQueryResult<any>[] | 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 : [],
Expand Down Expand Up @@ -1023,6 +997,39 @@ export class QueryManager<TStore> {
return concast;
}

public refetchQueries(queries: RefetchQueryDescription):
Promise<ApolloQueryResult<any>>[] {
const self = this;
const refetchQueryPromises: Promise<ApolloQueryResult<any>>[] = [];

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<TData, TVars>(
queryInfo: QueryInfo,
options: WatchQueryOptions<TVars, TData>,
Expand Down
91 changes: 91 additions & 0 deletions src/core/__tests__/QueryManager/index.ts
Expand Up @@ -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<any>({ query });
const observable2 = queryManager.watchQuery<any>({ 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`
Expand Down