Skip to content

Commit

Permalink
Merge pull request apollographql#9823 from apollographql/issue-9765-l…
Browse files Browse the repository at this point in the history
…eave-fetchPolicy-unchanged-when-skip-true

Leave `fetchPolicy` unchanged when `skip: true` (or in standby) and `nextFetchPolicy` is available
  • Loading branch information
benjamn authored Jun 19, 2022
2 parents eb48232 + 21b4f56 commit a3aefcb
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 44 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Apollo Client 3.6.9 (unreleased)

### Bug Fixes

- Leave `fetchPolicy` unchanged when `skip: true` (or in standby) and `nextFetchPolicy` is available, even if `variables` change. <br/>
[@benjamn](https://github.com/benjamn) in [#9823](https://github.com/apollographql/apollo-client/pull/9823)

## Apollo Client 3.6.8 (2022-06-10)

### Bug Fixes
Expand Down
31 changes: 18 additions & 13 deletions src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,18 +653,19 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
initialFetchPolicy = fetchPolicy,
} = options;

// When someone chooses "cache-and-network" or "network-only" as their
// initial FetchPolicy, they often do not want future cache updates to
// trigger unconditional network requests, which is what repeatedly
// applying the "cache-and-network" or "network-only" policies would seem
// to imply. Instead, when the cache reports an update after the initial
// network request, it may be desirable for subsequent network requests to
// be triggered only if the cache result is incomplete. To that end, the
// options.nextFetchPolicy option provides an easy way to update
// options.fetchPolicy after the initial network request, without having to
// call observableQuery.setOptions.

if (typeof options.nextFetchPolicy === "function") {
if (fetchPolicy === "standby") {
// Do nothing, leaving options.fetchPolicy unchanged.
} else if (typeof options.nextFetchPolicy === "function") {
// When someone chooses "cache-and-network" or "network-only" as their
// initial FetchPolicy, they often do not want future cache updates to
// trigger unconditional network requests, which is what repeatedly
// applying the "cache-and-network" or "network-only" policies would
// seem to imply. Instead, when the cache reports an update after the
// initial network request, it may be desirable for subsequent network
// requests to be triggered only if the cache result is incomplete. To
// that end, the options.nextFetchPolicy option provides an easy way to
// update options.fetchPolicy after the initial network request, without
// having to call observableQuery.setOptions.
options.fetchPolicy = options.nextFetchPolicy(fetchPolicy, {
reason,
options,
Expand Down Expand Up @@ -809,7 +810,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
newOptions &&
newOptions.variables &&
!equal(newOptions.variables, oldVariables) &&
(!newOptions.fetchPolicy || newOptions.fetchPolicy === oldFetchPolicy)
// Don't mess with the fetchPolicy if it's currently "standby".
options.fetchPolicy !== "standby" &&
// If we're changing the fetchPolicy anyway, don't try to change it here
// using applyNextFetchPolicy. The explicit options.fetchPolicy wins.
options.fetchPolicy === oldFetchPolicy
) {
this.applyNextFetchPolicy("variables-changed", options);
if (newNetworkStatus === void 0) {
Expand Down
31 changes: 21 additions & 10 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
asyncMap,
isNonEmptyArray,
Concast,
ConcastSourcesIterable,
ConcastSourcesArray,
makeUniqueId,
isDocumentNode,
isNonNullObject,
Expand Down Expand Up @@ -1122,16 +1122,33 @@ export class QueryManager<TStore> {
// modify its properties here, rather than creating yet another new
// WatchQueryOptions object.
normalized.variables = variables;
return this.fetchQueryByPolicy<TData, TVars>(

const concastSources = this.fetchQueryByPolicy<TData, TVars>(
queryInfo,
normalized,
networkStatus,
);

if (
// If we're in standby, postpone advancing options.fetchPolicy using
// applyNextFetchPolicy.
normalized.fetchPolicy !== "standby" &&
// The "standby" policy currently returns [] from fetchQueryByPolicy, so
// this is another way to detect when nothing was done/fetched.
concastSources.length > 0 &&
queryInfo.observableQuery
) {
queryInfo.observableQuery["applyNextFetchPolicy"]("after-fetch", options);
}

return concastSources;
};

// This cancel function needs to be set before the concast is created,
// in case concast creation synchronously cancels the request.
const cleanupCancelFn = () => this.fetchCancelFns.delete(queryId);
this.fetchCancelFns.set(queryId, reason => {
cleanupCancelFn();
// This delay ensures the concast variable has been initialized.
setTimeout(() => concast.cancel(reason));
});
Expand All @@ -1156,13 +1173,7 @@ export class QueryManager<TStore> {
: fromVariables(normalized.variables!)
);

concast.cleanup(() => {
this.fetchCancelFns.delete(queryId);

if (queryInfo.observableQuery) {
queryInfo.observableQuery["applyNextFetchPolicy"]("after-fetch", options);
}
});
concast.promise.then(cleanupCancelFn, cleanupCancelFn);

return concast;
}
Expand Down Expand Up @@ -1338,7 +1349,7 @@ export class QueryManager<TStore> {
// NetworkStatus.loading, but also possibly fetchMore, poll, refetch,
// or setVariables.
networkStatus: NetworkStatus,
): ConcastSourcesIterable<ApolloQueryResult<TData>> {
): ConcastSourcesArray<ApolloQueryResult<TData>> {
const oldNetworkStatus = queryInfo.networkStatus;

queryInfo.init({
Expand Down
30 changes: 26 additions & 4 deletions src/core/__tests__/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import gql from 'graphql-tag';
import { GraphQLError } from 'graphql';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';

import { ApolloClient, NetworkStatus } from '../../core';
import {
ApolloClient,
NetworkStatus,
WatchQueryFetchPolicy,
} from '../../core';
import { ObservableQuery } from '../ObservableQuery';
import { QueryManager } from '../QueryManager';

Expand Down Expand Up @@ -1190,11 +1194,21 @@ describe('ObservableQuery', () => {
},
);

const usedFetchPolicies: WatchQueryFetchPolicy[] = [];
const observable = queryManager.watchQuery({
query: queryWithVars,
variables: variables1,
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
nextFetchPolicy(currentFetchPolicy, info) {
if (info.reason === "variables-changed") {
return info.initialFetchPolicy;
}
usedFetchPolicies.push(currentFetchPolicy);
if (info.reason === "after-fetch") {
return "cache-first";
}
return currentFetchPolicy;
},
notifyOnNetworkStatusChange: true,
});

Expand Down Expand Up @@ -1222,7 +1236,7 @@ describe('ObservableQuery', () => {
}).then(result => {
expect(result.data).toEqual(data);
}).catch(reject);
expect(observable.options.fetchPolicy).toBe('cache-and-network');
expect(observable.options.fetchPolicy).toBe('cache-first');
} else if (handleCount === 4) {
expect(result.loading).toBe(true);
expect(result.networkStatus).toBe(NetworkStatus.setVariables);
Expand All @@ -1236,7 +1250,7 @@ describe('ObservableQuery', () => {
}).then(result => {
expect(result.data).toEqual(data2);
}).catch(reject);
expect(observable.options.fetchPolicy).toBe('cache-and-network');
expect(observable.options.fetchPolicy).toBe('cache-first');
} else if (handleCount === 6) {
expect(result.data).toEqual(data2);
expect(result.loading).toBe(true);
Expand All @@ -1245,6 +1259,14 @@ describe('ObservableQuery', () => {
expect(result.data).toEqual(data2);
expect(result.loading).toBe(false);
expect(observable.options.fetchPolicy).toBe('cache-first');

expect(usedFetchPolicies).toEqual([
"cache-and-network",
"network-only",
"cache-and-network",
"cache-and-network",
]);

setTimeout(resolve, 10);
} else {
reject(`too many renders (${handleCount})`);
Expand Down
2 changes: 1 addition & 1 deletion src/core/__tests__/fetchPolicies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ describe("nextFetchPolicy", () => {
}).catch(reject);

// Changing variables resets the fetchPolicy to its initial value.
expect(observable.options.fetchPolicy).toBe("network-only");
expect(observable.options.fetchPolicy).toBe("cache-first");

} else if (count === 3) {
expect(result.loading).toBe(false);
Expand Down
47 changes: 34 additions & 13 deletions src/react/hoc/__tests__/queries/loading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, waitFor } from '@testing-library/react';
import gql from 'graphql-tag';
import { DocumentNode } from 'graphql';

import { ApolloClient, NetworkStatus } from '../../../../core';
import { ApolloClient, NetworkStatus, WatchQueryFetchPolicy } from '../../../../core';
import { ApolloProvider } from '../../../context';
import { InMemoryCache as Cache } from '../../../../cache';
import { itAsync, mockSingleLink } from '../../../../testing';
Expand Down Expand Up @@ -360,32 +360,46 @@ describe('[queries] loading', () => {

let count = 0;

const usedFetchPolicies: WatchQueryFetchPolicy[] = [];
const Container = graphql<{}, Data>(query, {
options: {
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-first',
nextFetchPolicy(currentFetchPolicy, info) {
if (info.reason === "variables-changed") {
return info.initialFetchPolicy;
}
usedFetchPolicies.push(currentFetchPolicy);
if (info.reason === "after-fetch") {
return "cache-first";
}
return currentFetchPolicy;
}
},
})(
class extends React.Component<ChildProps<{}, Data>> {
render() {
++count;
if (count === 1) {
expect(this.props.data!.loading).toBe(true);
expect(this.props.data!.allPeople).toBeUndefined();
} else if (count === 2) {
expect(this.props.data!.loading).toBe(false);
expect(this.props.data!.allPeople!.people[0].name).toMatch(
/Darth Skywalker - /
);
// Has data
setTimeout(() => {
render(App);
}, 0);
}
if (count === 2) {
setTimeout(() => render(App));
} else if (count === 3) {
// Loading after remount
expect(this.props.data!.loading).toBeTruthy();
}
if (count === 3) {
expect(this.props.data!.loading).toBe(true);
expect(this.props.data!.allPeople).toBeUndefined();
} else if (count >= 4) {
// Fetched data loading after remount
expect(this.props.data!.loading).toBeFalsy();
expect(this.props.data!.loading).toBe(false);
expect(this.props.data!.allPeople!.people[0].name).toMatch(
/Darth Skywalker - /
);
}
count += 1;
return null;
}
}
Expand All @@ -399,7 +413,14 @@ describe('[queries] loading', () => {

render(App);

waitFor(() => expect(count).toBe(5)).then(resolve, reject);
return waitFor(() => {
expect(usedFetchPolicies).toEqual([
"network-only",
"network-only",
"cache-first",
]);
expect(count).toBe(6);
}).then(resolve, reject);
});

itAsync('correctly sets loading state on remounted component with changed variables', (resolve, reject) => {
Expand Down
2 changes: 1 addition & 1 deletion src/react/hoc/__tests__/queries/skip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ describe('[queries] skip', () => {
break;
case 4:
expect(this.props.skip).toBe(false);
expect(this.props.data!.loading).toBe(true);
expect(this.props.data!.loading).toBe(false);
expect(this.props.data.allPeople).toEqual(data.allPeople);
expect(ranQuery).toBe(2);
break;
Expand Down
4 changes: 2 additions & 2 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ describe('useQuery Hook', () => {

expect(
result.current.observable.options.fetchPolicy
).toBe("network-only");
).toBe("cache-first");

expect(result.current.observable.variables).toEqual({
sourceOfVar: "reobserve",
Expand Down Expand Up @@ -1094,7 +1094,7 @@ describe('useQuery Hook', () => {

expect(
result.current.observable.options.fetchPolicy
).toBe("network-only");
).toBe("cache-first");

expect(result.current.observable.variables).toEqual({
sourceOfVar: "reobserve without variable merge",
Expand Down
1 change: 1 addition & 0 deletions src/utilities/observables/Concast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function isPromiseLike<T>(value: MaybeAsync<T>): value is PromiseLike<T> {
type Source<T> = MaybeAsync<Observable<T>>;

export type ConcastSourcesIterable<T> = Iterable<Source<T>>;
export type ConcastSourcesArray<T> = Array<Source<T>>;

// A Concast<T> observable concatenates the given sources into a single
// non-overlapping sequence of Ts, automatically unwrapping any promises,
Expand Down

0 comments on commit a3aefcb

Please sign in to comment.