Skip to content

Commit

Permalink
Merge pull request #9222 from apollographql/provide-nextFetchPolicy-c…
Browse files Browse the repository at this point in the history
…ontext

Provide more context to `nextFetchPolicy` functions
  • Loading branch information
benjamn authored Mar 10, 2022
2 parents 2e138aa + e557dce commit f018f5e
Show file tree
Hide file tree
Showing 9 changed files with 598 additions and 42 deletions.
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;
}
}

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

0 comments on commit f018f5e

Please sign in to comment.