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 more context to nextFetchPolicy functions #9222

Merged
merged 5 commits into from
Mar 10, 2022
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
94 changes: 94 additions & 0 deletions docs/source/data/queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,100 @@ const { loading, error, data } = useQuery(GET_DOGS, {

For example, this is helpful if you want a query to always make an initial network request, but you're comfortable reading from the cache after that.

#### `nextFetchPolicy` functions

If you want to apply a single `nextFetchPolicy` by default, because you find yourself manually providing `nextFetchPolicy` for most of your queries, you can configure `defaultOptions.watchQuery.nextFetchPolicy` when creating your `ApolloClient` instance:

```js
new ApolloClient({
link,
client,
defaultOptions: {
watchQuery: {
nextFetchPolicy: "cache-only",
},
},
})
```

This configuration applies to all `client.watchQuery` calls and `useQuery` calls that do not otherwise configure `nextFetchPolicy`.

If you want more control over how `nextFetchPolicy` behaves, you can provide a function instead of a `WatchQueryFetchPolicy` string:

```js
new ApolloClient({
link,
client,
defaultOptions: {
watchQuery: {
nextFetchPolicy(currentFetchPolicy) {
if (
currentFetchPolicy === "network-only" ||
currentFetchPolicy === "cache-and-network"
) {
// Demote the network policies (except "no-cache") to "cache-first"
// after the first request.
return "cache-first";
}
// Leave all other fetch policies unchanged.
return currentFetchPolicy;
},
},
},
})
```

This `nextFetchPolicy` function will be called after each request, and uses the `currentFetchPolicy` parameter to decide how to modify the fetch policy.

In addition to being called after each request, your `nextFetchPolicy` function will also be called when variables change, which by default resets the `fetchPolicy` to its initial value, which is often important to trigger a fresh network request for queries that started out with `cache-and-network` or `network-only` fetch policies.

To intercept and handle the `variables-changed` case yourself, you can use the `NextFetchPolicyContext` object passed as the second argument to your `nextFetchPolicy` function:

```js
new ApolloClient({
link,
client,
defaultOptions: {
watchQuery: {
nextFetchPolicy(currentFetchPolicy, {
// Either "after-fetch" or "variables-changed", indicating why the
// nextFetchPolicy function was invoked.
reason,
// The rest of the options (currentFetchPolicy === options.fetchPolicy).
options,
// The original value of options.fetchPolicy, before nextFetchPolicy was
// applied for the first time.
initialPolicy,
// The ObservableQuery associated with this client.watchQuery call.
observable,
}) {
// When variables change, the default behavior is to reset
// options.fetchPolicy to context.initialPolicy. If you omit this logic,
// your nextFetchPolicy function can override this default behavior to
// prevent options.fetchPolicy from changing in this case.
if (reason === "variables-changed") {
return initialPolicy;
}

if (
currentFetchPolicy === "network-only" ||
currentFetchPolicy === "cache-and-network"
) {
// Demote the network policies (except "no-cache") to "cache-first"
// after the first request.
return "cache-first";
}

// Leave all other fetch policies unchanged.
return currentFetchPolicy;
},
},
},
})
```

In order to debug these `nextFetchPolicy` transitions, it can be useful to add `console.log` or `debugger` statements to the function body, to see when and why the function is called.

### Supported fetch policies

<table class="field-table">
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.min.cjs",
"maxSize": "28.95kB"
"maxSize": "29kB"
}
],
"engines": {
Expand Down
2 changes: 0 additions & 2 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Array [
"NetworkStatus",
"Observable",
"ObservableQuery",
"applyNextFetchPolicy",
"checkFetcher",
"concat",
"createHttpLink",
Expand Down Expand Up @@ -93,7 +92,6 @@ Array [
"NetworkStatus",
"Observable",
"ObservableQuery",
"applyNextFetchPolicy",
"checkFetcher",
"concat",
"createHttpLink",
Expand Down
9 changes: 8 additions & 1 deletion src/__tests__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3312,17 +3312,24 @@ describe('@connection', () => {

defaultOptions: {
watchQuery: {
nextFetchPolicy(fetchPolicy) {
nextFetchPolicy(fetchPolicy, context) {
expect(++nextFetchPolicyCallCount).toBe(1);
expect(this.query).toBe(query);
expect(fetchPolicy).toBe("cache-first");

expect(context.reason).toBe("after-fetch");
expect(context.observable).toBe(obs);
expect(context.options).toBe(obs.options);
expect(context.initialPolicy).toBe("cache-first");

// Usually options.nextFetchPolicy applies only once, but a
// nextFetchPolicy function can set this.nextFetchPolicy
// again to perform an additional transition.
this.nextFetchPolicy = fetchPolicy => {
++nextFetchPolicyCallCount;
return "cache-first";
};

return "cache-and-network";
},
},
Expand Down
75 changes: 41 additions & 34 deletions src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
FetchMoreQueryOptions,
SubscribeToMoreOptions,
WatchQueryFetchPolicy,
NextFetchPolicyContext,
} from './watchQueryOptions';
import { QueryInfo } from './QueryInfo';
import { MissingFieldError } from '../cache';
Expand Down Expand Up @@ -583,6 +584,45 @@ once, rather than every time you call fetchMore.`);
this.updatePolling();
}

// Update options.fetchPolicy according to options.nextFetchPolicy.
private applyNextFetchPolicy(
reason: NextFetchPolicyContext<TData, TVariables>["reason"],
// It's possible to use this method to apply options.nextFetchPolicy to
// options.fetchPolicy even if options !== this.options, though that happens
// most often when the options are temporary, used for only one request and
// then thrown away, so nextFetchPolicy may not end up mattering.
options: WatchQueryOptions<TVariables, TData> = this.options,
) {
if (options.nextFetchPolicy) {
const { fetchPolicy = "cache-first" } = 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") {
options.fetchPolicy = options.nextFetchPolicy(fetchPolicy, {
reason,
options,
observable: this,
initialPolicy: this.initialFetchPolicy,
});
} else if (reason === "variables-changed") {
options.fetchPolicy = this.initialFetchPolicy;
} else {
options.fetchPolicy = options.nextFetchPolicy;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this change affect this scenario described here?

Wondering the behavior described in 4 will remain consistent:

Discovered that setting fetchPolicy in the useQuery hook, would implicitly set nextFetchPolicy with the same policy, but only if there were no defaultOptions set in the ApolloClient instance.

Thanks for your time and confirmation here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dancrew32 Since you asked this question, I added some tests to verify the behavior should be the same as before, though it's possible I haven't understood/captured exactly what you mean. The tests exercise defaultOptions.watchQuery.nextFetchPolicy in a number of ways, for example. Please take a look at the new tests in src/core/__tests__/fetchPolicies.ts, try modifying them if you like, and let us know if anything seems worth adding to improve the test suite.

One difference that might be worth calling out: if (and only if) you were already using a nextFetchPolicy function (uncommon, but supported since #6893 or AC v3.2.0), that function will now be called to handle the variables-changed event as well as the after-fetch event, whereas previously the fetchPolicy was reset to its initial value when variables changed with no chance to intercept that (possibly unwanted) behavior. I'm mildly concerned this could be a breaking change for existing nextFetchPolicy functions that are not prepared to be called in the variables-changed case, though I'm hopeful most existing nextFetchPolicy functions map their own previous result to itself (so cache-first stays cache-first).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My organization has never needed nextFetchPolicy in any useQuery call sites, so this change shouldn't affect us. I can report back after we upgrade in the future, as I am interested in removing defaultOptions overrides in our ApolloClient instantiation. Thanks for getting back to me, testing, and documenting all of this.


return options.fetchPolicy;
}

private fetch(
options: WatchQueryOptions<TVariables, TData>,
newNetworkStatus?: NetworkStatus,
Expand Down Expand Up @@ -709,7 +749,7 @@ once, rather than every time you call fetchMore.`);
!newOptions.fetchPolicy &&
!equal(newOptions.variables, oldVariables)
) {
options.fetchPolicy = this.initialFetchPolicy;
this.applyNextFetchPolicy("variables-changed");
if (newNetworkStatus === void 0) {
newNetworkStatus = NetworkStatus.setVariables;
}
Expand Down Expand Up @@ -831,36 +871,3 @@ export function logMissingFieldErrors(
}`, missing);
}
}

// Adopt options.nextFetchPolicy (if defined) as a replacement for
// options.fetchPolicy. Since this method also removes options.nextFetchPolicy
// from options, the adoption tends to be idempotent, unless nextFetchPolicy
// is a function that keeps setting options.nextFetchPolicy (uncommon).
export function applyNextFetchPolicy<TData, TVars>(
options: Pick<
WatchQueryOptions<TVars, TData>,
| "fetchPolicy"
| "nextFetchPolicy"
>,
) {
const {
fetchPolicy = "cache-first",
nextFetchPolicy,
} = options;

if (nextFetchPolicy) {
// 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 = typeof nextFetchPolicy === "function"
? nextFetchPolicy.call(options, fetchPolicy)
: nextFetchPolicy;
}
}
7 changes: 5 additions & 2 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
ErrorPolicy,
MutationFetchPolicy,
} from './watchQueryOptions';
import { ObservableQuery, applyNextFetchPolicy, logMissingFieldErrors } from './ObservableQuery';
import { ObservableQuery, logMissingFieldErrors } from './ObservableQuery';
import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus';
import {
ApolloQueryResult,
Expand Down Expand Up @@ -1155,7 +1155,10 @@ export class QueryManager<TStore> {

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

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

return concast;
Expand Down
Loading