Repository: apollographql/apollo-client

Problem statement:
[v3.4 Regression] Changing variables uses nextFetchPolicy instead of fetchPolicy
**Intended outcome:**
I have a query with `fetchPolicy: 'network-only'` and `nextFetchPolicy: 'cache-first'`. When I change the variables, it should fetch new results from the server regardless of what's in the cache.

**Actual outcome:**
Changing the variables appears to use the `nextFetchPolicy` and does not fetch new data from the server.

**How to reproduce the issue:**
https://github.com/dylanwulf/react-apollo-error-template/tree/changing-variables-nextFetchPolicy
Please use branch `changing-variables-nextFetchPolicy`.
Reproduction instructions: Open dev tools and watch the console. Click any of the unselected radio buttons, see that a request to the server is made (this is printed to the console instead of the network tab due to the apollo link). Click "All", see that a request to the server is NOT made.

It looks like this problem first showed up in `v3.4.0-rc.5` and still exists in `v3.4.0-rc.14`


Hints:
Related:
- https://github.com/apollographql/apollo-client/issues/7437
- https://github.com/apollographql/apollo-client/issues/7311
- https://github.com/apollographql/apollo-client/issues/6839
@brainkim I thought it was interesting that #7437 is happening in v3.3 whereas I only saw my issue in v3.4. I played around a little and I was able to get the issue to happen in v3.3 if `fetchPolicy` and `nextFetchPolicy` are set in `defaultOptions` instead of directly in `useQuery` options. Possibly related to #6839? (which is another issue I would love to be fixed)
@dylanwulf The operative change in `3.4.0-rc.5` was the addition of this line in PR #8346:
https://github.com/apollographql/apollo-client/blob/f11a163a4e507441bd51c2e0f9571aad853d5856/src/react/data/QueryData.ts#L203
Calling `applyNextFetchPolicy` here advances immediately to `nextFetchPolicy` without ever using `fetchPolicy`, which seems incorrect to me. Removing this line fixes the reproduction you provided, though you might want to remove `notifyOnNetworkStatusChange: true` from your `useQuery` options to avoid receiving intermediate `loading: true` results with cache data.

On a more general note, since you mentioned #6839, I have to admit I'm not comfortable with the role `useQuery` plays in resetting `fetchPolicy` and `nextFetchPolicy`, which happens simply because `useQuery` is called again with (very likely) the same initial options object, every time the component rerenders. I happen to think `fetchPolicy` should be reset when variables change, but I don't think `useQuery` is the right/best mechanism to accomplish that.
@benjamn thanks for looking into this! awesome to hear you found the root cause
> though you might want to remove `notifyOnNetworkStatusChange: true` from your `useQuery` options to avoid receiving intermediate `loading: true` results with cache data.

When I change the variables, I seem to be receiving intermediate `loading: true` results with cache data whether `notifyOnNetworkStatusChange` is `true` or `false` (both in v3.3, and v3.4 with that change to `QueryData.ts` you described). But I can always add in some simple logic to hide results whenever `loading` is `true`.

> which happens simply because `useQuery` is called again with (very likely) the same initial options object, every time the component rerenders. I happen to think `fetchPolicy` should be reset when variables change, but I don't think useQuery is the right/best mechanism to accomplish that.

I agree, I think that when the variables change it should use the `fetchPolicy` that's being passed into the options (whether that's in the `useQuery` options or the `defaultOptions`)

Between #6839 and https://github.com/apollographql/apollo-client/issues/8426#issuecomment-869912317, `defaultOptions` seem pretty much unusable since I have to explicitly specify `fetchPolicy` and `nextFetchPolicy` in every single `useQuery` anyway. But at least that's a problem with a workaround.
> When I change the variables, I seem to be receiving intermediate `loading: true` results with cache data whether `notifyOnNetworkStatusChange` is `true` or `false` (both in v3.3, and v3.4 with that change to `QueryData.ts` you described).

@dylanwulf I looked into this, and it seems to be a consequence of using React `useState` to store/modify the `gender` variable. Changing the `gender` using `setGender` forces React to rerender the component immediately, even though we might want the update to take place in the background for `network-only` fetches. Since `useQuery` can't return the final data yet, it returns a `loading: true` result, regardless of `notifyOnNetworkStatusChange`.

When using `client.watchQuery` and the `ObservableQuery` API directly, changing the variables with `notifyOnNetworkStatusChange: false` should skip the `loading: true` result, so you only see the final update. I'm not sure there's much we can do about the React situation right now, other than reporting the truth about the `loading: true` state of the results `useQuery` is forced to render.

For what it's worth, we are actively rethinking these issues with `useQuery` in the context of the a new `useFragment` hook (see #8236).
> forces React to rerender the component immediately

Oooh ok that makes sense, thank you for the explanation
Alright, except for the weirdness about `notifyOnNetworkStatusChange: false` not working with React and `useQuery`, I believe this is fixed in `@apollo/client@3.4.0-rc.18`. Thanks for pushing this issue (and #6839) forward @dylanwulf!

Patch:
diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts
--- a/src/core/ObservableQuery.ts
+++ b/src/core/ObservableQuery.ts
@@ -24,6 +24,7 @@ import {
   FetchMoreQueryOptions,
   SubscribeToMoreOptions,
   WatchQueryFetchPolicy,
+  NextFetchPolicyContext,
 } from './watchQueryOptions';
 import { QueryInfo } from './QueryInfo';
 import { MissingFieldError } from '../cache';
@@ -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,
@@ -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;
         }
@@ -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;
-  }
-}
diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts
--- a/src/core/QueryManager.ts
+++ b/src/core/QueryManager.ts
@@ -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,
@@ -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;
diff --git a/src/core/index.ts b/src/core/index.ts
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -12,7 +12,6 @@ export {
   ObservableQuery,
   FetchMoreOptions,
   UpdateQueryOptions,
-  applyNextFetchPolicy,
 } from './ObservableQuery';
 export {
   QueryOptions,
diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts
--- a/src/core/watchQueryOptions.ts
+++ b/src/core/watchQueryOptions.ts
@@ -11,6 +11,7 @@ import {
   InternalRefetchQueriesInclude,
 } from './types';
 import { ApolloCache } from '../cache';
+import { ObservableQuery } from './ObservableQuery';
 
 /**
  * fetchPolicy determines where the client may return a result from. The options are:
@@ -125,7 +126,8 @@ export interface WatchQueryOptions<TVariables = OperationVariables, TData = any>
    */
   nextFetchPolicy?: WatchQueryFetchPolicy | ((
     this: WatchQueryOptions<TVariables, TData>,
-    lastFetchPolicy: WatchQueryFetchPolicy,
+    currentFetchPolicy: WatchQueryFetchPolicy,
+    context: NextFetchPolicyContext<TData, TVariables>,
   ) => WatchQueryFetchPolicy);
   /**
    * Specifies whether a {@link NetworkStatus.refetch} operation should merge
@@ -136,6 +138,15 @@ export interface WatchQueryOptions<TVariables = OperationVariables, TData = any>
   refetchWritePolicy?: RefetchWritePolicy;
 }
 
+export interface NextFetchPolicyContext<TData, TVariables> {
+  reason:
+    | "after-fetch"
+    | "variables-changed";
+  observable: ObservableQuery<TData, TVariables>;
+  options: WatchQueryOptions<TVariables, TData>;
+  initialPolicy: WatchQueryFetchPolicy;
+}
+
 export interface FetchMoreQueryOptions<TVariables, TData = any> {
   query?: DocumentNode | TypedDocumentNode<TData, TVariables>;
   variables?: Partial<TVariables>;


Test patch:
diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap
--- a/src/__tests__/__snapshots__/exports.ts.snap
+++ b/src/__tests__/__snapshots__/exports.ts.snap
@@ -16,7 +16,6 @@ Array [
   "NetworkStatus",
   "Observable",
   "ObservableQuery",
-  "applyNextFetchPolicy",
   "checkFetcher",
   "concat",
   "createHttpLink",
@@ -93,7 +92,6 @@ Array [
   "NetworkStatus",
   "Observable",
   "ObservableQuery",
-  "applyNextFetchPolicy",
   "checkFetcher",
   "concat",
   "createHttpLink",
diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts
--- a/src/__tests__/client.ts
+++ b/src/__tests__/client.ts
@@ -3312,10 +3312,16 @@ 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.
@@ -3323,6 +3329,7 @@ describe('@connection', () => {
                 ++nextFetchPolicyCallCount;
                 return "cache-first";
               };
+
               return "cache-and-network";
             },
           },
diff --git a/src/core/__tests__/fetchPolicies.ts b/src/core/__tests__/fetchPolicies.ts
--- a/src/core/__tests__/fetchPolicies.ts
+++ b/src/core/__tests__/fetchPolicies.ts
@@ -9,6 +9,10 @@ import {
   itAsync,
   mockSingleLink,
 } from '../../testing';
+import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { WatchQueryFetchPolicy, WatchQueryOptions } from '../watchQueryOptions';
+import { ApolloQueryResult } from '../types';
+import { ObservableQuery } from '../ObservableQuery';
 
 const query = gql`
   query {
@@ -745,3 +749,436 @@ describe('cache-and-network', function() {
     });
   });
 });
+
+describe("nextFetchPolicy", () => {
+  type TData = {
+    linkCounter: number;
+    opName: string;
+    opVars: Record<string, any>;
+  }
+
+  const EchoQuery: TypedDocumentNode<TData> = gql`
+    query EchoQuery {
+      linkCounter
+      opName
+      opVars
+    }
+  `;
+
+  function makeLink() {
+    let linkCounter = 0;
+    return new ApolloLink(request => new Observable(observer => {
+      setTimeout(() => {
+        observer.next({
+          data: {
+            linkCounter: ++linkCounter,
+            opName: request.operationName,
+            opVars: request.variables,
+          },
+        });
+        observer.complete();
+      }, 10);
+    }));
+  }
+
+  const checkNextFetchPolicy = <TData, TVars>(args: {
+    fetchPolicy: WatchQueryFetchPolicy;
+    nextFetchPolicy: WatchQueryOptions<TVars, TData>["nextFetchPolicy"];
+    useDefaultOptions: boolean;
+    onResult(info: {
+      count: number;
+      result: ApolloQueryResult<TData>;
+      observable: ObservableQuery<TData, TVars>;
+      resolve(result?: any): void;
+      reject(reason?: any): void;
+    }): void;
+  }) => itAsync(`transitions ${args.fetchPolicy} to ${
+    typeof args.nextFetchPolicy === "function"
+      ? args.nextFetchPolicy.name
+      : args.nextFetchPolicy
+  } (${
+    args.useDefaultOptions ? "" : "not "
+  }using defaults)`, (resolve, reject) => {
+    const client = new ApolloClient({
+      link: makeLink(),
+      cache: new InMemoryCache(),
+      defaultOptions: {
+        watchQuery: args.useDefaultOptions ? {
+          nextFetchPolicy: args.nextFetchPolicy,
+        } : {},
+      },
+    });
+
+    const watchQueryOptions: WatchQueryOptions<TVars, TData> = {
+      query: EchoQuery,
+      fetchPolicy: args.fetchPolicy,
+    };
+
+    if (!args.useDefaultOptions) {
+      watchQueryOptions.nextFetchPolicy = args.nextFetchPolicy;
+    }
+
+    const observable = client.watchQuery(watchQueryOptions);
+
+    expect(observable.options.fetchPolicy).toBe(args.fetchPolicy);
+
+    subscribeAndCount(reject, observable, (count, result) => {
+      return args.onResult({
+        observable,
+        count,
+        result,
+        resolve,
+        reject,
+      });
+    });
+  });
+
+  type CheckOptions = Parameters<typeof checkNextFetchPolicy>[0];
+  type NextFetchPolicy = CheckOptions["nextFetchPolicy"];
+  type OnResultCallback = CheckOptions["onResult"];
+
+  // We'll use this same OnResultCallback for multiple tests, to make it easier
+  // to tell that the behavior of the tests is the same.
+  const onResultNetworkOnlyToCacheFirst: OnResultCallback = ({
+    observable,
+    count,
+    result,
+    resolve,
+    reject,
+  }) => {
+    if (count === 1) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 1,
+        opName: "EchoQuery",
+        opVars: {},
+      });
+
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      observable.refetch({
+        refetching: true,
+      }).then(result => {
+        expect(result.data).toEqual({
+          linkCounter: 2,
+          opName: "EchoQuery",
+          opVars: {
+            refetching: true,
+          },
+        });
+      }).catch(reject);
+
+    } else if (count === 2) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 2,
+        opName: "EchoQuery",
+        opVars: {
+          refetching: true,
+        },
+      });
+
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      observable.reobserve({
+        variables: {
+          refetching: false,
+        },
+      }).then(result => {
+        expect(result.loading).toBe(false);
+        expect(result.data).toEqual({
+          linkCounter: 3,
+          opName: "EchoQuery",
+          opVars: {
+            refetching: false,
+          },
+        });
+      }).catch(reject);
+
+      // Changing variables resets the fetchPolicy to its initial value.
+      expect(observable.options.fetchPolicy).toBe("network-only");
+
+    } else if (count === 3) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 3,
+        opName: "EchoQuery",
+        opVars: {
+          refetching: false,
+        },
+      });
+
+      // But nextFetchPolicy is applied again after the first request.
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      setTimeout(resolve, 20);
+    } else {
+      reject(`Too many results (${count})`);
+    }
+  };
+
+  checkNextFetchPolicy({
+    useDefaultOptions: false,
+    fetchPolicy: "network-only",
+    nextFetchPolicy: "cache-first",
+    onResult: onResultNetworkOnlyToCacheFirst,
+  });
+
+  checkNextFetchPolicy({
+    useDefaultOptions: true,
+    fetchPolicy: "network-only",
+    nextFetchPolicy: "cache-first",
+    onResult: onResultNetworkOnlyToCacheFirst,
+  });
+
+  const nextFetchPolicyNetworkOnlyToCacheFirst: NextFetchPolicy = function (
+    currentFetchPolicy,
+    context,
+  ): WatchQueryFetchPolicy {
+    expect(currentFetchPolicy).toBe(context.options.fetchPolicy);
+    switch (context.reason) {
+      case "variables-changed":
+        return context.initialPolicy;
+      default:
+      case "after-fetch":
+        return "cache-first";
+    }
+  };
+
+  checkNextFetchPolicy({
+    useDefaultOptions: false,
+    fetchPolicy: "network-only",
+    nextFetchPolicy: nextFetchPolicyNetworkOnlyToCacheFirst,
+    onResult: onResultNetworkOnlyToCacheFirst,
+  });
+
+  checkNextFetchPolicy({
+    useDefaultOptions: true,
+    fetchPolicy: "network-only",
+    nextFetchPolicy: nextFetchPolicyNetworkOnlyToCacheFirst,
+    onResult: onResultNetworkOnlyToCacheFirst,
+  });
+
+  const onResultCacheAndNetworkToCacheFirst: OnResultCallback = ({
+    observable,
+    count,
+    result,
+    resolve,
+    reject,
+  }) => {
+    if (count === 1) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 1,
+        opName: "EchoQuery",
+        opVars: {},
+      });
+
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      observable.refetch({
+        refetching: true,
+      }).then(result => {
+        expect(result.data).toEqual({
+          linkCounter: 2,
+          opName: "EchoQuery",
+          opVars: {
+            refetching: true,
+          },
+        });
+      }).catch(reject);
+
+    } else if (count === 2) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 2,
+        opName: "EchoQuery",
+        opVars: {
+          refetching: true,
+        },
+      });
+
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      observable.reobserve({
+        variables: {
+          refetching: false,
+        },
+      }).then(result => {
+        expect(result.loading).toBe(false);
+        expect(result.data).toEqual({
+          linkCounter: 3,
+          opName: "EchoQuery",
+          opVars: {
+            refetching: false,
+          },
+        });
+      }).catch(reject);
+
+      // Changing variables resets the fetchPolicy to its initial value.
+      // expect(observable.options.fetchPolicy).toBe("cache-and-network");
+
+    } else if (count === 3) {
+      expect(result.loading).toBe(true);
+      expect(result.data).toEqual({
+        linkCounter: 2,
+        opName: "EchoQuery",
+        opVars: {
+          refetching: true,
+        },
+      });
+
+      // But nextFetchPolicy is applied again after the first request.
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+    } else if (count === 4) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 3,
+        opName: "EchoQuery",
+        opVars: {
+          refetching: false,
+        },
+      });
+
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      setTimeout(resolve, 20);
+    } else {
+      reject(`Too many results (${count})`);
+    }
+  };
+
+  checkNextFetchPolicy({
+    useDefaultOptions: false,
+    fetchPolicy: "cache-and-network",
+    nextFetchPolicy: "cache-first",
+    onResult: onResultCacheAndNetworkToCacheFirst,
+  });
+
+  checkNextFetchPolicy({
+    useDefaultOptions: true,
+    fetchPolicy: "cache-and-network",
+    nextFetchPolicy: "cache-first",
+    onResult: onResultCacheAndNetworkToCacheFirst,
+  });
+
+  const nextFetchPolicyCacheAndNetworkToCacheFirst: NextFetchPolicy = function (
+    currentFetchPolicy,
+    context,
+  ): WatchQueryFetchPolicy {
+    expect(currentFetchPolicy).toBe(context.options.fetchPolicy);
+    switch (context.reason) {
+      case "variables-changed":
+        return context.initialPolicy;
+      default:
+      case "after-fetch":
+        return "cache-first";
+    }
+  };
+
+  checkNextFetchPolicy({
+    useDefaultOptions: false,
+    fetchPolicy: "cache-and-network",
+    nextFetchPolicy: nextFetchPolicyCacheAndNetworkToCacheFirst,
+    onResult: onResultCacheAndNetworkToCacheFirst,
+  });
+
+  checkNextFetchPolicy({
+    useDefaultOptions: true,
+    fetchPolicy: "cache-and-network",
+    nextFetchPolicy: nextFetchPolicyCacheAndNetworkToCacheFirst,
+    onResult: onResultCacheAndNetworkToCacheFirst,
+  });
+
+  const nextFetchPolicyAlwaysCacheFirst: NextFetchPolicy = function (
+    currentFetchPolicy,
+    context,
+  ): WatchQueryFetchPolicy {
+    expect(currentFetchPolicy).toBe(context.options.fetchPolicy);
+    // Return cache-first no matter what context.reason was.
+    return "cache-first";
+  };
+
+  const onResultCacheAndNetworkAlwaysCacheFirst: OnResultCallback = ({
+    observable,
+    count,
+    result,
+    resolve,
+    reject,
+  }) => {
+    if (count === 1) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 1,
+        opName: "EchoQuery",
+        opVars: {},
+      });
+
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      observable.refetch({
+        refetching: true,
+      }).then(result => {
+        expect(result.data).toEqual({
+          linkCounter: 2,
+          opName: "EchoQuery",
+          opVars: {
+            refetching: true,
+          },
+        });
+      }).catch(reject);
+
+    } else if (count === 2) {
+      expect(result.loading).toBe(false);
+      expect(result.data).toEqual({
+        linkCounter: 2,
+        opName: "EchoQuery",
+        opVars: {
+          refetching: true,
+        },
+      });
+
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      observable.reobserve({
+        variables: {
+          refetching: false,
+        },
+      }).then(result => {
+        expect(result.loading).toBe(false);
+        expect(result.data).toEqual({
+          linkCounter: 2,
+          opName: "EchoQuery",
+          opVars: {
+            refetching: true,
+          },
+        });
+      }).catch(reject);
+
+      // The nextFetchPolicy function we provided always returnes cache-first,
+      // even when context.reason is variables-changed (which by default
+      // resets the fetchPolicy to context.initialPolicy), so cache-first is
+      // still what we see here.
+      expect(observable.options.fetchPolicy).toBe("cache-first");
+
+      setTimeout(resolve, 20);
+    } else {
+      reject(`Too many results (${count})`);
+    }
+  };
+
+  checkNextFetchPolicy({
+    useDefaultOptions: false,
+    fetchPolicy: "cache-and-network",
+    nextFetchPolicy: nextFetchPolicyAlwaysCacheFirst,
+    onResult: onResultCacheAndNetworkAlwaysCacheFirst,
+  });
+
+  checkNextFetchPolicy({
+    useDefaultOptions: true,
+    fetchPolicy: "cache-and-network",
+    nextFetchPolicy: nextFetchPolicyAlwaysCacheFirst,
+    onResult: onResultCacheAndNetworkAlwaysCacheFirst,
+  });
+});