Repository: apollographql/apollo-client

Problem statement:
ApolloProvider causing rerenders in consumers if ApolloProvider is rerendered even with the same client argument
<!--
  Thanks for filing an issue on Apollo Client!

  Please make sure that you include the following information to ensure that your issue is actionable.

  If you don't follow the template, your issue may end up being closed without anyone looking at it carefully, because it is not actionable for us without the information in this template.

  **PLEASE NOTE:** Feature requests and non-bug related discussions are no longer managed in this repo. Feature requests should be opened in https://github.com/apollographql/apollo-feature-requests.
-->

**Intended outcome:**
Whenever `ApolloProvider` is rerendered (e.g. if the parent rerenders) but receives the same `client`, this should not cause the context (used by `useQuery`, `useMutation`, but also the consumers) to force a rerender.

I tried to use `ApolloProvider` to provide the Apollo Client inside a separate React environment (React-Konva). I tried to utilize the new type policies to prevent passing down props by using queries that fetch data from the cache directly by id. All this again to optimize the rather slow React-Konva scene. However, even without the cache changing, simply moving the scene around (which rerenders the parent component of ApolloProvider and thus ApolloProvider itself) caused all objects in the scene with queries to rerender. 

**Actual outcome:**
Instead of objects not being rerendered as long as no data changes in the Apollo cache, all objects rerender whenever something changes up regardless of whether `client` remains the same.

**How to reproduce the issue:**
Create an `ApolloProvider` in a component that will rerender, notice that all memoized components deeper down in the tree still rerender even when the stored cache value doesn't change. For a working example (with an alternative that doesn't have this issue) see:

https://codesandbox.io/s/apollo-context-rerenders-umlrx

From what I gather, the following happens:
- ApolloProvider is a functional component
- If this is used inside React-Konva, it will not find the ApolloContext.Provider value from above, which causes the embedded `if` to execute (`value === undefined/null`).
- This embedded `if` uses `Object.assign({}, context, { client })` to create a context. 
- However, given that it's a functional component, it will do this for every instantiation.
- So each time, `context` is different and React will rerender all context dependent components, causing this issue.

Locally, I fixed this by simply using my own provider:
```js
export const SimpleApolloProvider = ({client, children}) => {
	const ApolloContext = getApolloContext();

	// Use useMemo to prevent context from updating during
	// rerenders with the same client. Generally the client
	// will remain the same throughout the lifetime of the
	// application, but when {client} is used, (or as official
	// implementation context = Object.assign({}, context, { client }))
	// each rerender will create a new object, causing *all*
	// useQuery/useMutation hooks to rerender due to changed context
	// value.
	const context = useMemo(() => ({
		client
	}), [client])

	return (
		<ApolloContext.Provider value={context}>
			{children}
		</ApolloContext.Provider>
	)
}
```
I don't need the reuse feature from ApolloProvider here so this is sufficient for my use case. But I believe it's useful to cache the `context` object value so the (potentially many) consumers don't needlessly rerender.

**Versions**
```
  System:
    OS: Linux 5.10 Arch Linux
  Binaries:
    Node: 6.14.4 - ~/.nvm/versions/node/v6.14.4/bin/node
    Yarn: 1.21.1 - ~/.nvm/versions/node/v6.14.4/bin/yarn
    npm: 3.10.10 - ~/.nvm/versions/node/v6.14.4/bin/npm
  Browsers:
    Firefox: 84.0.2
  npmPackages:
    @apollo/client: ^3.3.7 => 3.3.7 
    apollo-link-queue: ^3.0.0 => 3.0.0 
    apollo-link-serialize: ^3.1.1 => 3.1.1 
    apollo-utilities: ^1.3.3 => 1.3.3 
```

Duplicate `@wry/trie` in dependency tree
The `@wry/trie` dependency is currently duplicated in Apollo's dependency tree.
An update to `optimism@0.17.5` would be required to get rid of this duplication, see https://github.com/benjamn/optimism/pull/551.


Hints:
Running into this issue as well on latest npm apollo 3.4.16 


Patch:
diff --git a/.size-limit.cjs b/.size-limit.cjs
--- a/.size-limit.cjs
+++ b/.size-limit.cjs
@@ -1,16 +1,16 @@
 const checks = [
   {
     path: "dist/apollo-client.min.cjs",
-    limit: "36.64kb"
+    limit: "38056",
   },
   {
     path: "dist/main.cjs",
-    import: "{ ApolloClient, InMemoryCache, HttpLink }"
+    import: "{ ApolloClient, InMemoryCache, HttpLink }",
   },
   {
     path: "dist/index.js",
     import: "{ ApolloClient, InMemoryCache, HttpLink }",
-    limit: "34.99kb"
+    limit: "31971",
   },
   ...[
     "ApolloProvider",
@@ -18,40 +18,53 @@ const checks = [
     "useLazyQuery",
     "useMutation",
     "useSubscription",
-    //"useSuspenseQuery_experimental",
-    "useFragment_experimental"
+    "useSuspenseQuery",
+    "useBackgroundQuery",
+    "useReadQuery",
+    "useFragment",
   ].map((name) => ({ path: "dist/react/index.js", import: `{ ${name} }` })),
-].map((config) => ({
-  ...config,
-  name: config.name || config.import ? `import ${config.import} from "${config.path}"` : config.path,
-  ignore: [
-    ...(config.ignore || []),
-    "react",
-    "react-dom",
-    "@graphql-typed-document-node/core",
-    "@wry/context",
-    "@wry/equality",
-    "@wry/trie",
-    "graphql-tag",
-    "hoist-non-react-statics",
-    "optimism",
-    "prop-types",
-    "response-iterator",
-    "symbol-observable",
-    "ts-invariant",
-    "tslib",
-    "zen-observable-ts"
-  ],
-})).flatMap((value) => value.path == "dist/apollo-client.min.cjs" ? value : [{...value, limit: undefined}, {
-  ...value,
-  name: `${value.name} (production)`,
-  modifyEsbuildConfig(config){
-    config.define = {
-      "__DEV__": `false`,
-      "globalThis.__DEV__": `false`,
-    }
-    return config
-  }
-}]);
+]
+  .map((config) => ({
+    ...config,
+    name:
+      config.name || config.import
+        ? `import ${config.import} from "${config.path}"`
+        : config.path,
+    ignore: [
+      ...(config.ignore || []),
+      "react",
+      "react-dom",
+      "@graphql-typed-document-node/core",
+      "@wry/context",
+      "@wry/equality",
+      "@wry/trie",
+      "graphql-tag",
+      "hoist-non-react-statics",
+      "optimism",
+      "prop-types",
+      "response-iterator",
+      "symbol-observable",
+      "ts-invariant",
+      "tslib",
+      "zen-observable-ts",
+    ],
+  }))
+  .flatMap((value) =>
+    value.path == "dist/apollo-client.min.cjs"
+      ? value
+      : [
+          { ...value, limit: undefined },
+          {
+            ...value,
+            name: `${value.name} (production)`,
+            modifyEsbuildConfig(config) {
+              config.define = {
+                "globalThis.__DEV__": `false`,
+              };
+              return config;
+            },
+          },
+        ]
+  );
 
 module.exports = checks;
diff --git a/config/bundlesize.ts b/config/bundlesize.ts
--- a/config/bundlesize.ts
+++ b/config/bundlesize.ts
@@ -3,21 +3,19 @@ import { join } from "path";
 import { gzipSync } from "zlib";
 import bytes from "bytes";
 
-const gzipBundleByteLengthLimit = bytes("33.07KB");
+const gzipBundleByteLengthLimit = bytes("35.25KB");
 const minFile = join("dist", "apollo-client.min.cjs");
 const minPath = join(__dirname, "..", minFile);
 const gzipByteLen = gzipSync(readFileSync(minPath)).byteLength;
 const overLimit = gzipByteLen > gzipBundleByteLengthLimit;
 
-const message = `Minified + GZIP-encoded bundle size for ${
-  minFile
-} = ${
-  bytes(gzipByteLen, { unit: "KB" })
-}, ${
-  overLimit ? "exceeding" : "under"
-} limit ${
-  bytes(gzipBundleByteLengthLimit, { unit: "KB" })
-}`;
+const message = `Minified + GZIP-encoded bundle size for ${minFile} = ${bytes(
+  gzipByteLen,
+  { unit: "KB" }
+)}, ${overLimit ? "exceeding" : "under"} limit ${bytes(
+  gzipBundleByteLengthLimit,
+  { unit: "KB" }
+)}`;
 
 if (overLimit) {
   throw new Error(message);
diff --git a/config/distSpotFixes.ts b/config/distSpotFixes.ts
deleted file mode 100644
--- a/config/distSpotFixes.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as fs from "fs";
-import { EOL } from "os";
-import { distDir } from './helpers';
-
-export function applyDistSpotFixes() {
-  sanitizeDEV();
-}
-
-function sanitizeDEV() {
-  const globalDTsPath = `${distDir}/utilities/globals/global.d.ts`;
-  const globalDTs = fs.readFileSync(globalDTsPath, "utf8");
-  // The global.d.ts types are useful within the @apollo/client codebase to
-  // provide a type for the global __DEV__ constant, but actually shipping that
-  // declaration as a globally-declared type runs too much risk of conflict with
-  // other __DEV__ declarations attempting to achieve the same thing, most
-  // notably the one in @types/react-native/index.d.ts. We preserve the default
-  // export so that index.d.ts can remain unchanged, but otherwise we remove all
-  // traces of __DEV__ from global.d.ts.
-  if (/__DEV__/.test(globalDTs)) {
-    fs.writeFileSync(globalDTsPath, [
-      "declare const _default: typeof globalThis;",
-      "export default _default;",
-    ].join(EOL));
-  }
-}
diff --git a/config/entryPoints.js b/config/entryPoints.js
--- a/config/entryPoints.js
+++ b/config/entryPoints.js
@@ -1,44 +1,46 @@
 const entryPoints = [
   { dirs: [], bundleName: "main" },
-  { dirs: ['cache'] },
-  { dirs: ['core'] },
-  { dirs: ['errors'] },
-  { dirs: ['link', 'batch'] },
-  { dirs: ['link', 'batch-http'] },
-  { dirs: ['link', 'context'] },
-  { dirs: ['link', 'core'] },
-  { dirs: ['link', 'error'] },
-  { dirs: ['link', 'http'] },
-  { dirs: ['link', 'persisted-queries'] },
-  { dirs: ['link', 'retry'] },
-  { dirs: ['link', 'schema'] },
-  { dirs: ['link', 'subscriptions'] },
-  { dirs: ['link', 'utils'] },
-  { dirs: ['link', 'ws'] },
-  { dirs: ['react'] },
-  { dirs: ['react', 'components'] },
-  { dirs: ['react', 'context'] },
-  { dirs: ['react', 'hoc'] },
-  { dirs: ['react', 'hooks'] },
-  { dirs: ['react', 'parser'] },
-  { dirs: ['react', 'ssr'] },
-  { dirs: ['testing'], extensions: [".js", ".jsx"] },
-  { dirs: ['testing', 'core'] },
-  { dirs: ['utilities'] },
-  { dirs: ['utilities', 'globals'], sideEffects: true },
+  { dirs: ["cache"] },
+  { dirs: ["core"] },
+  { dirs: ["dev"] },
+  { dirs: ["errors"] },
+  { dirs: ["link", "batch"] },
+  { dirs: ["link", "batch-http"] },
+  { dirs: ["link", "context"] },
+  { dirs: ["link", "core"] },
+  { dirs: ["link", "error"] },
+  { dirs: ["link", "http"] },
+  { dirs: ["link", "persisted-queries"] },
+  { dirs: ["link", "retry"] },
+  { dirs: ["link", "remove-typename"] },
+  { dirs: ["link", "schema"] },
+  { dirs: ["link", "subscriptions"] },
+  { dirs: ["link", "utils"] },
+  { dirs: ["link", "ws"] },
+  { dirs: ["react"] },
+  { dirs: ["react", "components"] },
+  { dirs: ["react", "context"] },
+  { dirs: ["react", "hoc"] },
+  { dirs: ["react", "hooks"] },
+  { dirs: ["react", "parser"] },
+  { dirs: ["react", "ssr"] },
+  { dirs: ["testing"], extensions: [".js", ".jsx"] },
+  { dirs: ["testing", "core"] },
+  { dirs: ["utilities"] },
+  { dirs: ["utilities", "globals"], sideEffects: true },
 ];
 
 const lookupTrie = Object.create(null);
-entryPoints.forEach(info => {
+entryPoints.forEach((info) => {
   let node = lookupTrie;
-  info.dirs.forEach(dir => {
+  info.dirs.forEach((dir) => {
     const dirs = node.dirs || (node.dirs = Object.create(null));
     node = dirs[dir] || (dirs[dir] = { isEntry: false });
   });
   node.isEntry = true;
 });
 
-exports.forEach = function(callback, context) {
+exports.forEach = function (callback, context) {
   entryPoints.forEach(callback, context);
 };
 
@@ -74,9 +76,11 @@ exports.check = function (id, parentId) {
         return false;
       }
 
-      console.warn(`Risky cross-entry-point nested import of ${id} in ${
-        partsAfterDist(parentId).join("/")
-      }`);
+      console.warn(
+        `Risky cross-entry-point nested import of ${id} in ${partsAfterDist(
+          parentId
+        ).join("/")}`
+      );
     }
   }
 
@@ -86,6 +90,9 @@ exports.check = function (id, parentId) {
 function partsAfterDist(id) {
   const parts = id.split(path.sep);
   const distIndex = parts.lastIndexOf("dist");
+  if (/^index.jsx?$/.test(parts[parts.length - 1])) {
+    parts.pop();
+  }
   if (distIndex >= 0) {
     return parts.slice(distIndex + 1);
   }
diff --git a/config/helpers.ts b/config/helpers.ts
--- a/config/helpers.ts
+++ b/config/helpers.ts
@@ -5,17 +5,17 @@ import glob = require("glob");
 
 export const distDir = path.resolve(__dirname, "..", "dist");
 
-export function eachFile(dir: string, callback: (
-  absPath: string,
-  relPath: string,
-) => any) {
+export function eachFile(
+  dir: string,
+  callback: (absPath: string, relPath: string) => any
+) {
   const promises: Promise<any>[] = [];
 
   return new Promise<void>((resolve, reject) => {
-    glob(`${dir.replace(/\\/g, '/')}/**/*.js`, (error, files) => {
+    glob(`${dir.replace(/\\/g, "/")}/**/*.js`, (error, files) => {
       if (error) return reject(error);
 
-      files.sort().forEach(file => {
+      files.sort().forEach((file) => {
         const relPath = path.relative(dir, file);
 
         // Outside the distDir, somehow.
@@ -32,9 +32,11 @@ export function eachFile(dir: string, callback: (
         // This file is not meant to be imported or processed.
         if (relPath.endsWith("invariantErrorCodes.js")) return;
 
-        promises.push(new Promise(resolve => {
-          resolve(callback(file, relPath));
-        }));
+        promises.push(
+          new Promise((resolve) => {
+            resolve(callback(file, relPath));
+          })
+        );
       });
 
       resolve();
diff --git a/config/jest.config.js b/config/jest.config.js
--- a/config/jest.config.js
+++ b/config/jest.config.js
@@ -2,34 +2,46 @@ const defaults = {
   rootDir: "src",
   preset: "ts-jest",
   testEnvironment: "jsdom",
-  setupFiles: ["<rootDir>/config/jest/setup.ts"],
+  setupFilesAfterEnv: ["<rootDir>/config/jest/setup.ts"],
+  globals: {
+    __DEV__: true,
+  },
   testEnvironmentOptions: {
     url: "http://localhost",
   },
   snapshotFormat: {
     escapeString: true,
-    printBasicPrototype: true
+    printBasicPrototype: true,
   },
   transform: {
-    '^.+\\.tsx?$': [
-      'ts-jest',
+    "^.+\\.tsx?$": [
+      "ts-jest",
       {
         diagnostics: {
-          warnOnly: process.env.TEST_ENV !== 'ci'
+          warnOnly: process.env.TEST_ENV !== "ci",
         },
       },
     ],
   },
+  resolver: "ts-jest-resolver",
 };
 
-const ignoreTSFiles = '.ts$';
-const ignoreTSXFiles = '.tsx$';
+const ignoreTSFiles = ".ts$";
+const ignoreTSXFiles = ".tsx$";
+
+const react17TestFileIgnoreList = [
+  ignoreTSFiles,
+  // For now, we only support useSuspenseQuery with React 18, so no need to test
+  // it with React 17
+  "src/react/hooks/__tests__/useSuspenseQuery.test.tsx",
+  "src/react/hooks/__tests__/useBackgroundQuery.test.tsx",
+];
 
 const tsStandardConfig = {
   ...defaults,
-  displayName: 'Core Tests',
+  displayName: "Core Tests",
   testPathIgnorePatterns: [ignoreTSXFiles],
-}
+};
 
 // For both React (Jest) "projects", ignore core tests (.ts files) as they
 // do not import React, to avoid running them twice.
@@ -42,7 +54,7 @@ const standardReact18Config = {
 const standardReact17Config = {
   ...defaults,
   displayName: "ReactDOM 17",
-  testPathIgnorePatterns: [ignoreTSFiles],
+  testPathIgnorePatterns: react17TestFileIgnoreList,
   moduleNameMapper: {
     "^react$": "react-17",
     "^react-dom$": "react-dom-17",
@@ -53,9 +65,5 @@ const standardReact17Config = {
 };
 
 module.exports = {
-  projects: [
-    tsStandardConfig,
-    standardReact17Config,
-    standardReact18Config,
-  ],
+  projects: [tsStandardConfig, standardReact17Config, standardReact18Config],
 };
diff --git a/config/postprocessDist.ts b/config/postprocessDist.ts
--- a/config/postprocessDist.ts
+++ b/config/postprocessDist.ts
@@ -1,141 +1,14 @@
-import * as fs from "fs";
-import * as path from "path";
-import resolve from "resolve";
-import { distDir, eachFile, reparse, reprint } from './helpers';
-
-import { applyDistSpotFixes } from "./distSpotFixes";
-applyDistSpotFixes();
-
-// The primary goal of the 'npm run resolve' script is to make ECMAScript
-// modules exposed by Apollo Client easier to consume natively in web browsers,
-// without bundling, and without help from package.json files. It accomplishes
-// this goal by rewriting internal ./ and ../ (relative) imports to refer to a
-// specific ESM module (not a directory), including its file extension. Because
-// of this limited goal, this script only touches ESM modules that have .js file
-// extensions, not .cjs CommonJS bundles.
-
-// A secondary goal of this script is to enforce that any module using the
-// __DEV__ global constant imports the @apollo/client/utilities/globals polyfill
-// module first.
-
-eachFile(distDir, (file, relPath) => new Promise((resolve, reject) => {
-  fs.readFile(file, "utf8", (error, source) => {
-    if (error) return reject(error);
-
-    const tr = new Transformer;
-    const output = tr.transform(source, file);
-
-    if (
-      /\b__DEV__\b/.test(source) &&
-      // Ignore modules that reside within @apollo/client/utilities/globals.
-      relPath.split(path.sep, 2).join("/") !== "utilities/globals"
-    ) {
-      let importsUtilitiesGlobals = false;
-
-      tr.absolutePaths.forEach(absPath => {
-        const distRelativePath =
-          path.relative(distDir, absPath).split(path.sep).join("/");
-        if (distRelativePath === "utilities/globals/index.js") {
-          importsUtilitiesGlobals = true;
-        }
-      });
-
-      if (!importsUtilitiesGlobals) {
-        reject(new Error(`Module ${
-          relPath
-        } uses __DEV__ but does not import @apollo/client/utilities/globals`));
-      }
-    }
-
-    if (source === output) {
-      resolve(file);
-    } else {
-      fs.writeFile(file, output, "utf8", error => {
-        error ? reject(error) : resolve(file);
-      });
-    }
-  });
-}));
-
-import * as recast from "recast";
-const n = recast.types.namedTypes;
-type Node = recast.types.namedTypes.Node;
-
-class Transformer {
-  absolutePaths = new Set<string>();
-
-  transform(code: string, file: string) {
-    const ast = reparse(code);
-    const transformer = this;
-
-    recast.visit(ast, {
-      visitImportDeclaration(path) {
-        this.traverse(path);
-        transformer.normalizeSourceString(file, path.node.source);
-      },
-
-      visitImportExpression(path) {
-        this.traverse(path);
-        transformer.normalizeSourceString(file, path.node.source);
-      },
-
-      visitExportAllDeclaration(path) {
-        this.traverse(path);
-        transformer.normalizeSourceString(file, path.node.source);
-      },
-
-      visitExportNamedDeclaration(path) {
-        this.traverse(path);
-        transformer.normalizeSourceString(file, path.node.source);
-      },
-    });
-
-    return reprint(ast);
-  }
-
-  isRelative(id: string) {
-    return id.startsWith("./") || id.startsWith("../");
-  }
-
-  normalizeSourceString(file: string, source?: Node | null) {
-    if (source && n.StringLiteral.check(source)) {
-      // We mostly only worry about normalizing _relative_ module identifiers,
-      // which start with a ./ or ../ and refer to other modules within the
-      // @apollo/client package, but we also manually normalize one non-relative
-      // identifier, ts-invariant/process, to prevent webpack 5 errors
-      // containing the phrase "failed to resolve only because it was resolved
-      // as fully specified," referring to webpack's resolve.fullySpecified
-      // option, which is apparently now true by default when the enclosing
-      // package's package.json file has "type": "module" (which became true for
-      // Apollo Client in v3.5).
-      if (source.value.split("/", 2).join("/") === "ts-invariant/process") {
-        source.value = "ts-invariant/process/index.js";
-      } else if (this.isRelative(source.value)) {
-        try {
-          source.value = this.normalizeId(source.value, file);
-        } catch (error) {
-          console.error(`Failed to resolve ${source.value} in ${file} with error ${error}`);
-          process.exit(1);
-        }
-      }
-    }
-  }
-
-  normalizeId(id: string, file: string) {
-    const basedir = path.dirname(file);
-    const absPath = resolve.sync(id, {
-      basedir,
-      extensions: [".mjs", ".js"],
-      packageFilter(pkg) {
-        return pkg.module ? {
-          ...pkg,
-          main: pkg.module,
-        } : pkg;
-      },
-    });
-    this.absolutePaths.add(absPath);
-    const relPath = path.relative(basedir, absPath);
-    const relId = relPath.split(path.sep).join('/');
-    return this.isRelative(relId) ? relId : "./" + relId;
-  }
-}
+import { distDir } from "./helpers.ts";
+import fs from "node:fs";
+import path from "node:path";
+
+const globalTypesFile = path.resolve(distDir, "utilities/globals/global.d.ts");
+fs.writeFileSync(
+  globalTypesFile,
+  fs
+    .readFileSync(globalTypesFile, "utf8")
+    .split("\n")
+    .filter((line) => line.trim() !== "const __DEV__: boolean;")
+    .join("\n"),
+  "utf8"
+);
diff --git a/config/precheck.js b/config/precheck.js
--- a/config/precheck.js
+++ b/config/precheck.js
@@ -1,17 +1,10 @@
-const {
-  lockfileVersion,
-} = require("../package-lock.json");
+const { lockfileVersion } = require("../package-lock.json");
 
 const expectedVersion = 2;
 
-if (typeof lockfileVersion !== "number" ||
-    lockfileVersion < expectedVersion) {
+if (typeof lockfileVersion !== "number" || lockfileVersion < expectedVersion) {
   throw new Error(
-    `Old lockfileVersion (${
-      lockfileVersion
-    }) found in package-lock.json (expected ${
-      expectedVersion
-    } or later)`
+    `Old lockfileVersion (${lockfileVersion}) found in package-lock.json (expected ${expectedVersion} or later)`
   );
 }
 
diff --git a/config/prepareDist.js b/config/prepareDist.js
--- a/config/prepareDist.js
+++ b/config/prepareDist.js
@@ -11,23 +11,22 @@
 // - Create a new `package.json` for each sub-set bundle we support, and
 //   store it in the appropriate dist sub-directory.
 
-const fs = require('fs');
-const path = require('path');
-const recast = require('recast');
+const fs = require("fs");
+const path = require("path");
+const recast = require("recast");
 
 const distRoot = `${__dirname}/../dist`;
 
-
 /* @apollo/client */
 
-const packageJson = require('../package.json');
-const entryPoints = require('./entryPoints.js');
+const packageJson = require("../package.json");
+const entryPoints = require("./entryPoints.js");
 
 // Enable default interpretation of .js files as ECMAScript modules. We don't
 // put this in the source ../package.json file because it interferes with tools
 // like ts-node, which we use to run various ../config/*.ts scripts.
 // TODO(benjamn) Fully diagnose that interference.
-packageJson.type = 'module';
+packageJson.type = "module";
 
 // The root package.json is marked as private to prevent publishing
 // from happening in the root of the project. This sets the package back to
@@ -43,14 +42,19 @@ delete packageJson.engines;
 // on-going package development (e.g. running tests, supporting npm link, etc.).
 // When publishing from "dist" however, we need to update the package.json
 // to point to the files within the same directory.
-const distPackageJson = JSON.stringify(packageJson, (_key, value) => {
-  if (typeof value === 'string' && value.startsWith('./dist/')) {
-    const parts = value.split('/');
-    parts.splice(1, 1); // remove dist
-    return parts.join('/');
-  }
-  return value;
-}, 2) + "\n";
+const distPackageJson =
+  JSON.stringify(
+    packageJson,
+    (_key, value) => {
+      if (typeof value === "string" && value.startsWith("./dist/")) {
+        const parts = value.split("/");
+        parts.splice(1, 1); // remove dist
+        return parts.join("/");
+      }
+      return value;
+    },
+    2
+  ) + "\n";
 
 // Save the modified package.json to "dist"
 fs.writeFileSync(`${distRoot}/package.json`, distPackageJson);
@@ -58,8 +62,8 @@ fs.writeFileSync(`${distRoot}/package.json`, distPackageJson);
 // Copy supporting files into "dist"
 const srcDir = `${__dirname}/..`;
 const destDir = `${srcDir}/dist`;
-fs.copyFileSync(`${srcDir}/README.md`,  `${destDir}/README.md`);
-fs.copyFileSync(`${srcDir}/LICENSE`,  `${destDir}/LICENSE`);
+fs.copyFileSync(`${srcDir}/README.md`, `${destDir}/README.md`);
+fs.copyFileSync(`${srcDir}/LICENSE`, `${destDir}/LICENSE`);
 
 // Create individual bundle package.json files, storing them in their
 // associated dist directory. This helps provide a way for the Apollo Client
@@ -73,14 +77,18 @@ entryPoints.forEach(function buildPackageJson({
 }) {
   if (!dirs.length) return;
   fs.writeFileSync(
-    path.join(distRoot, ...dirs, 'package.json'),
-    JSON.stringify({
-      name: path.posix.join('@apollo', 'client', ...dirs),
-      type: "module",
-      main: `${bundleName}.cjs`,
-      module: 'index.js',
-      types: 'index.d.ts',
-      sideEffects,
-    }, null, 2) + "\n",
+    path.join(distRoot, ...dirs, "package.json"),
+    JSON.stringify(
+      {
+        name: path.posix.join("@apollo", "client", ...dirs),
+        type: "module",
+        main: `${bundleName}.cjs`,
+        module: "index.js",
+        types: "index.d.ts",
+        sideEffects,
+      },
+      null,
+      2
+    ) + "\n"
   );
 });
diff --git a/config/processInvariants.ts b/config/processInvariants.ts
--- a/config/processInvariants.ts
+++ b/config/processInvariants.ts
@@ -1,6 +1,7 @@
 import * as fs from "fs";
-import * as path from "path";
-import { distDir, eachFile, reparse, reprint } from './helpers';
+import { posix, join as osPathJoin } from "path";
+import { distDir, eachFile, reparse, reprint } from "./helpers.ts";
+import type { ExpressionKind } from "ast-types/lib/gen/kinds";
 
 eachFile(distDir, (file, relPath) => {
   const source = fs.readFileSync(file, "utf8");
@@ -10,10 +11,10 @@ eachFile(distDir, (file, relPath) => {
   }
 }).then(() => {
   fs.writeFileSync(
-    path.join(distDir, "invariantErrorCodes.js"),
-    recast.print(errorCodeManifest, {
+    osPathJoin(distDir, "invariantErrorCodes.js"),
+    recast.print(program, {
       tabWidth: 2,
-    }).code + "\n",
+    }).code + "\n"
   );
 });
 
@@ -21,48 +22,110 @@ import * as recast from "recast";
 const b = recast.types.builders;
 const n = recast.types.namedTypes;
 type Node = recast.types.namedTypes.Node;
-type NumericLiteral = recast.types.namedTypes.NumericLiteral;
 type CallExpression = recast.types.namedTypes.CallExpression;
 type NewExpression = recast.types.namedTypes.NewExpression;
 let nextErrorCode = 1;
 
-const errorCodeManifest = b.objectExpression([
-  b.property("init",
-    b.stringLiteral("@apollo/client version"),
-    b.stringLiteral(require("../package.json").version),
+const program = b.program([]);
+const allExports = {
+  errorCodes: getExportObject("errorCodes"),
+  devDebug: getExportObject("devDebug"),
+  devLog: getExportObject("devLog"),
+  devWarn: getExportObject("devWarn"),
+  devError: getExportObject("devError"),
+};
+type ExportName = keyof typeof allExports;
+
+allExports.errorCodes.comments = [
+  b.commentLine(
+    " This file is used by the error message display website and the",
+    true
   ),
-]);
-
-errorCodeManifest.comments = [
-  b.commentLine(' This file is meant to help with looking up the source of errors like', true),
-  b.commentLine(' "Invariant Violation: 35" and is automatically generated by the file', true),
-  b.commentLine(' @apollo/client/config/processInvariants.ts for each @apollo/client', true),
-  b.commentLine(' release. The numbers may change from release to release, so please', true),
-  b.commentLine(' consult the @apollo/client/invariantErrorCodes.js file specific to', true),
-  b.commentLine(' your @apollo/client version. This file is not meant to be imported.', true),
+  b.commentLine(" @apollo/client/includeErrors entry point.", true),
+  b.commentLine(" This file is not meant to be imported manually.", true),
 ];
 
+function getExportObject(exportName: string) {
+  const object = b.objectExpression([]);
+  program.body.push(
+    b.exportNamedDeclaration(
+      b.variableDeclaration("const", [
+        b.variableDeclarator(b.identifier(exportName), object),
+      ])
+    )
+  );
+  return object;
+}
+
 function getErrorCode(
   file: string,
   expr: CallExpression | NewExpression,
-): NumericLiteral {
-  const numLit = b.numericLiteral(nextErrorCode++);
-  errorCodeManifest.properties.push(
-    b.property("init", numLit, b.objectExpression([
-      b.property("init", b.identifier("file"), b.stringLiteral("@apollo/client/" + file)),
-      b.property("init", b.identifier("node"), expr),
-    ])),
-  );
-  return numLit;
-}
+  type: keyof typeof allExports
+): ExpressionKind {
+  if (isIdWithName(expr.callee, "invariant")) {
+    return extractString(
+      file,
+      allExports[type].properties,
+      expr.arguments[1],
+      expr.arguments[0]
+    );
+  } else {
+    return extractString(file, allExports[type].properties, expr.arguments[0]);
+  }
+
+  function extractString(
+    file: string,
+    target: (typeof allExports)[ExportName]["properties"],
+    message: recast.types.namedTypes.SpreadElement | ExpressionKind,
+    condition?: recast.types.namedTypes.SpreadElement | ExpressionKind
+  ): ExpressionKind {
+    if (message.type === "ConditionalExpression") {
+      return b.conditionalExpression(
+        message.test,
+        extractString(file, target, message.consequent, condition),
+        extractString(file, target, message.alternate, condition)
+      );
+    } else if (isStringOnly(message)) {
+      const messageText = reprint(message);
+      if (messageText.includes("Apollo DevTools")) {
+        return message;
+      }
+
+      const obj = b.objectExpression([]);
+      const numLit = b.numericLiteral(nextErrorCode++);
+      target.push(b.property("init", numLit, obj));
+
+      obj.properties.push(
+        b.property(
+          "init",
+          b.identifier("file"),
+          b.stringLiteral("@apollo/client/" + file)
+        )
+      );
+      if (condition) {
+        obj.properties.push(
+          b.property(
+            "init",
+            b.identifier("condition"),
+            b.stringLiteral(reprint(expr.arguments[0]))
+          )
+        );
+      }
+      obj.properties.push(b.property("init", b.identifier("message"), message));
+
+      return numLit;
+    } else {
+      throw new Error(`invariant minification error: node cannot have dynamical error argument!
+        file: ${posix.join(distDir, file)}:${expr.loc?.start.line}
+        code:
 
-function transform(code: string, file: string) {
-  // If the code doesn't seem to contain anything invariant-related, we
-  // can skip parsing and transforming it.
-  if (!/invariant/i.test(code)) {
-    return code;
+        ${reprint(message)}
+      `);
+    }
   }
+}
 
+function transform(code: string, relativeFilePath: string) {
   const ast = reparse(code);
 
   recast.visit(ast, {
@@ -71,81 +134,113 @@ function transform(code: string, file: string) {
       const node = path.node;
 
       if (isCallWithLength(node, "invariant", 1)) {
-        if (isDEVConditional(path.parent.node)) {
-          return;
-        }
+        const newArgs = [...node.arguments];
+        newArgs.splice(
+          1,
+          1,
+          getErrorCode(relativeFilePath, node, "errorCodes")
+        );
 
-        const newArgs = node.arguments.slice(0, 1);
-        newArgs.push(getErrorCode(file, node));
+        return b.callExpression.from({
+          ...node,
+          arguments: newArgs,
+        });
+      }
 
-        return b.conditionalExpression(
-          makeDEVExpr(),
-          node,
-          b.callExpression.from({
-            ...node,
-            arguments: newArgs,
-          }),
+      if (isCallWithLength(node, "newInvariantError", 0)) {
+        const newArgs = [...node.arguments];
+        newArgs.splice(
+          0,
+          1,
+          getErrorCode(relativeFilePath, node, "errorCodes")
         );
-      }
 
-      if (node.callee.type === "MemberExpression" &&
-          isIdWithName(node.callee.object, "invariant") &&
-          isIdWithName(node.callee.property, "debug", "log", "warn", "error")) {
-        if (isDEVLogicalAnd(path.parent.node)) {
-          return;
-        }
-        return b.logicalExpression("&&", makeDEVExpr(), node);
+        return b.callExpression.from({
+          ...node,
+          arguments: newArgs,
+        });
       }
-    },
-
-    visitNewExpression(path) {
-      this.traverse(path);
-      const node = path.node;
-      if (isCallWithLength(node, "InvariantError", 0)) {
-        if (isDEVConditional(path.parent.node)) {
-          return;
-        }
 
-        const newArgs = [getErrorCode(file, node)];
+      if (
+        node.callee.type === "MemberExpression" &&
+        isIdWithName(node.callee.object, "invariant") &&
+        isIdWithName(node.callee.property, "debug", "log", "warn", "error")
+      ) {
+        let newNode = node;
+        if (node.arguments[0].type !== "Identifier") {
+          const prop = node.callee.property;
+          if (!n.Identifier.check(prop)) throw new Error("unexpected type");
 
-        return b.conditionalExpression(
-          makeDEVExpr(),
-          node,
-          b.newExpression.from({
+          const newArgs = [...node.arguments];
+          newArgs.splice(
+            0,
+            1,
+            getErrorCode(
+              relativeFilePath,
+              node,
+              ("dev" + capitalize(prop.name)) as ExportName
+            )
+          );
+          newNode = b.callExpression.from({
             ...node,
             arguments: newArgs,
-          }),
-        );
+          });
+        }
+
+        if (isDEVLogicalAnd(path.parent.node)) {
+          return newNode;
+        }
+        return b.logicalExpression("&&", makeDEVExpr(), newNode);
       }
-    }
+    },
   });
 
+  if (
+    !["utilities/globals/index.js", "config/jest/setup.js"].includes(
+      relativeFilePath
+    )
+  )
+    recast.visit(ast, {
+      visitIdentifier(path) {
+        this.traverse(path);
+        const node = path.node;
+        if (isDEVExpr(node)) {
+          return b.binaryExpression(
+            "!==",
+            b.memberExpression(
+              b.identifier("globalThis"),
+              b.identifier("__DEV__")
+            ),
+            b.literal(false)
+          );
+        }
+        return node;
+      },
+    });
+
   return reprint(ast);
 }
 
-function isIdWithName(node: Node, ...names: string[]) {
-  return n.Identifier.check(node) &&
-    names.some(name => name === node.name);
+function isIdWithName(node: Node | null | undefined, ...names: string[]) {
+  return (
+    node && n.Identifier.check(node) && names.some((name) => name === node.name)
+  );
 }
 
 function isCallWithLength(
   node: CallExpression | NewExpression,
   name: string,
-  length: number,
+  length: number
 ) {
-  return isIdWithName(node.callee, name) &&
-    node.arguments.length > length;
-}
-
-function isDEVConditional(node: Node) {
-  return n.ConditionalExpression.check(node) &&
-    isDEVExpr(node.test);
+  return isIdWithName(node.callee, name) && node.arguments.length > length;
 }
 
 function isDEVLogicalAnd(node: Node) {
-  return n.LogicalExpression.check(node) &&
+  return (
+    n.LogicalExpression.check(node) &&
     node.operator === "&&" &&
-    isDEVExpr(node.left);
+    isDEVExpr(node.left)
+  );
 }
 
 function makeDEVExpr() {
@@ -155,3 +250,28 @@ function makeDEVExpr() {
 function isDEVExpr(node: Node) {
   return isIdWithName(node, "__DEV__");
 }
+
+function isStringOnly(
+  node: recast.types.namedTypes.ASTNode
+): node is ExpressionKind {
+  switch (node.type) {
+    case "StringLiteral":
+    case "Literal":
+      return true;
+    case "TemplateLiteral":
+      return (node.expressions as recast.types.namedTypes.ASTNode[]).every(
+        isStringOnly
+      );
+    case "BinaryExpression":
+      return (
+        node.operator == "+" &&
+        isStringOnly(node.left) &&
+        isStringOnly(node.right)
+      );
+  }
+  return false;
+}
+
+function capitalize(str: string) {
+  return str[0].toUpperCase() + str.slice(1);
+}
diff --git a/config/rewriteSourceMaps.ts b/config/rewriteSourceMaps.ts
--- a/config/rewriteSourceMaps.ts
+++ b/config/rewriteSourceMaps.ts
@@ -1,9 +1,9 @@
 import * as fs from "fs";
 import * as path from "path";
-import { distDir } from './helpers';
+import { distDir } from "./helpers.ts";
 import glob = require("glob");
 
-glob(`${distDir.replace(/\\/g, '/')}/**/*.js.map`, (error, files) => {
+glob(`${distDir.replace(/\\/g, "/")}/**/*.js.map`, (error, files) => {
   if (error) throw error;
 
   const rootDir = path.dirname(distDir);
@@ -11,34 +11,38 @@ glob(`${distDir.replace(/\\/g, '/')}/**/*.js.map`, (error, files) => {
   const startTime = Date.now();
   let rewriteCount = 0;
 
-  Promise.all(files.map(async file => {
-    const content = await fs.promises.readFile(file, "utf8");
-    const map = JSON.parse(content);
-    if (map.sourcesContent) return;
-    if (map.sources) {
-      map.sourcesContent = await Promise.all(
-        map.sources.map((relSourcePath: string) => {
-          const sourcePath = path.normalize(
-            path.join(path.dirname(file), relSourcePath));
-          const relPath = path.relative(rootDir, sourcePath);
-          // Disallow reading paths outside rootDir.
-          if (relPath.startsWith("../")) {
-            throw new Error(`Bad path: ${sourcePath}`);
-          }
-          return fs.promises.readFile(sourcePath, "utf8");
-        })
+  Promise.all(
+    files.map(async (file) => {
+      const content = await fs.promises.readFile(file, "utf8");
+      const map = JSON.parse(content);
+      if (map.sourcesContent) return;
+      if (map.sources) {
+        map.sourcesContent = await Promise.all(
+          map.sources.map((relSourcePath: string) => {
+            const sourcePath = path.normalize(
+              path.join(path.dirname(file), relSourcePath)
+            );
+            const relPath = path.relative(rootDir, sourcePath);
+            // Disallow reading paths outside rootDir.
+            if (relPath.startsWith("../")) {
+              throw new Error(`Bad path: ${sourcePath}`);
+            }
+            return fs.promises.readFile(sourcePath, "utf8");
+          })
+        );
+        ++rewriteCount;
+        return fs.promises.writeFile(file, JSON.stringify(map));
+      }
+    })
+  ).then(
+    () => {
+      console.log(
+        `Rewrote ${rewriteCount} source maps in ${Date.now() - startTime}ms`
       );
-      ++rewriteCount;
-      return fs.promises.writeFile(file, JSON.stringify(map));
+    },
+    (error) => {
+      console.error(error);
+      process.exit(-1);
     }
-  })).then(() => {
-    console.log(`Rewrote ${
-      rewriteCount
-    } source maps in ${
-      Date.now() - startTime
-    }ms`);
-  }, error => {
-    console.error(error);
-    process.exit(-1);
-  });
+  );
 });
diff --git a/config/rollup.config.js b/config/rollup.config.js
--- a/config/rollup.config.js
+++ b/config/rollup.config.js
@@ -1,38 +1,32 @@
-import path from 'path';
+import path, { resolve, dirname } from "path";
 import { promises as fs } from "fs";
 
-import nodeResolve from '@rollup/plugin-node-resolve';
-import { terser as minify } from 'rollup-plugin-terser';
+import nodeResolve from "@rollup/plugin-node-resolve";
+import { terser as minify } from "rollup-plugin-terser";
 
-const entryPoints = require('./entryPoints');
-const distDir = './dist';
+const entryPoints = require("./entryPoints");
+const distDir = "./dist";
 
 function isExternal(id, parentId, entryPointsAreExternal = true) {
-  let posixId = toPosixPath(id)
+  let posixId = toPosixPath(id);
   const posixParentId = toPosixPath(parentId);
   // Rollup v2.26.8 started passing absolute id strings to this function, thanks
   // apparently to https://github.com/rollup/rollup/pull/3753, so we relativize
   // the id again in those cases.
   if (path.isAbsolute(id)) {
-    posixId = path.posix.relative(
-      path.posix.dirname(posixParentId),
-      posixId,
-    );
+    posixId = path.posix.relative(path.posix.dirname(posixParentId), posixId);
     if (!posixId.startsWith(".")) {
       posixId = "./" + posixId;
     }
   }
 
-  const isRelative =
-    posixId.startsWith("./") ||
-    posixId.startsWith("../");
+  const isRelative = posixId.startsWith("./") || posixId.startsWith("../");
 
   if (!isRelative) {
     return true;
   }
 
-  if (entryPointsAreExternal &&
-      entryPoints.check(posixId, posixParentId)) {
+  if (entryPointsAreExternal && entryPoints.check(posixId, posixParentId)) {
     return true;
   }
 
@@ -43,14 +37,14 @@ function isExternal(id, parentId, entryPointsAreExternal = true) {
 function toPosixPath(p) {
   // Sometimes, you can have a path like \Users\IEUser on windows, and this
   // actually means you want C:\Users\IEUser
-  if (p[0] === '\\') {
+  if (p[0] === "\\") {
     p = process.env.SystemDrive + p;
   }
 
-  p = p.replace(/\\/g, '/');
-  if (p[1] === ':') {
+  p = p.replace(/\\/g, "/");
+  if (p[1] === ":") {
     // Transform "C:/bla/bla" to "/c/bla/bla"
-    p = '/' + p[0] + p.slice(2);
+    p = "/" + p[0] + p.slice(2);
   }
 
   return p;
@@ -64,14 +58,12 @@ function prepareCJS(input, output) {
     },
     output: {
       file: output,
-      format: 'cjs',
+      format: "cjs",
       sourcemap: true,
-      exports: 'named',
+      exports: "named",
       externalLiveBindings: false,
     },
-    plugins: [
-      nodeResolve(),
-    ],
+    plugins: [nodeResolve()],
   };
 }
 
@@ -79,8 +71,8 @@ function prepareCJSMinified(input) {
   return {
     input,
     output: {
-      file: input.replace('.cjs', '.min.cjs'),
-      format: 'cjs',
+      file: input.replace(".cjs", ".min.cjs"),
+      format: "cjs",
     },
     plugins: [
       minify({
@@ -90,7 +82,7 @@ function prepareCJSMinified(input) {
         compress: {
           toplevel: true,
           global_defs: {
-            '@__DEV__': 'false',
+            "@globalThis.__DEV__": "false",
           },
         },
       }),
@@ -109,26 +101,48 @@ function prepareBundle({
 
   return {
     input: inputFile,
-    external(id, parentId) {
-      return isExternal(id, parentId, true);
-    },
+    // the external check is done by the `'externalize-dependency'` plugin
+    // external(id, parentId) {}
     output: {
       file: outputFile,
-      format: 'cjs',
+      format: "cjs",
       sourcemap: true,
-      exports: 'named',
+      exports: "named",
       externalLiveBindings: false,
     },
     plugins: [
+      {
+        name: "externalize-dependency",
+        resolveId(id, parentId) {
+          if (!parentId) {
+            return null;
+          }
+          function removeIndex(filename) {
+            if (filename.endsWith(`${path.sep}index.js`)) {
+              return filename.slice(0, -`${path.sep}index.js`.length);
+            }
+            return filename;
+          }
+
+          const external = isExternal(id, parentId, true);
+          if (external) {
+            if (id.startsWith(".")) {
+              return {
+                id: removeIndex(resolve(dirname(parentId), id)),
+                external: true,
+              };
+            }
+            return { id: removeIndex(id), external: true };
+          }
+          return null;
+        },
+      },
       extensions ? nodeResolve({ extensions }) : nodeResolve(),
       {
         name: "copy *.cjs to *.cjs.native.js",
         async writeBundle({ file }) {
           const buffer = await fs.readFile(file);
-          await fs.writeFile(
-            file + ".native.js",
-            buffer,
-          );
+          await fs.writeFile(file + ".native.js", buffer);
         },
       },
     ],
@@ -138,11 +152,6 @@ function prepareBundle({
 export default [
   ...entryPoints.map(prepareBundle),
   // Convert the ESM entry point to a single CJS bundle.
-  prepareCJS(
-    './dist/index.js',
-    './dist/apollo-client.cjs',
-  ),
-  prepareCJSMinified(
-    './dist/apollo-client.cjs',
-  ),
+  prepareCJS("./dist/index.js", "./dist/apollo-client.cjs"),
+  prepareCJSMinified("./dist/apollo-client.cjs"),
 ];
diff --git a/config/version.js b/config/version.js
--- a/config/version.js
+++ b/config/version.js
@@ -7,8 +7,9 @@ const pkgJsonPath = path.join(__dirname, "..", "package.json");
 
 const { version } = JSON.parse(fs.readFileSync(pkgJsonPath));
 assert.strictEqual(
-  typeof version, "string",
-  '"version" field missing from package.json',
+  typeof version,
+  "string",
+  '"version" field missing from package.json'
 );
 
 switch (process.argv[2]) {
@@ -18,8 +19,9 @@ switch (process.argv[2]) {
       .replace(/\blocal\b/, version);
 
     assert.notEqual(
-      updated.indexOf(version), -1,
-      "Failed to update dist/version.js with @apollo/client version",
+      updated.indexOf(version),
+      -1,
+      "Failed to update dist/version.js with @apollo/client version"
     );
 
     fs.writeFileSync(versionPath, updated);
@@ -28,15 +30,16 @@ switch (process.argv[2]) {
   }
 
   case "verify": {
-    const {
-      ApolloClient,
-      InMemoryCache,
-    } = require(path.join(distRoot, "core", "core.cjs"));
+    const { ApolloClient, InMemoryCache } = require(path.join(
+      distRoot,
+      "core",
+      "core.cjs"
+    ));
 
     // Though this may seem like overkill, verifying that ApolloClient is
     // constructible in Node.js is actually pretty useful, too!
     const client = new ApolloClient({
-      cache: new InMemoryCache,
+      cache: new InMemoryCache(),
     });
 
     // Probably not necessary, but it seems wise to clean up any resources
@@ -50,17 +53,16 @@ switch (process.argv[2]) {
     // convenient because dist/version.js uses ECMAScript module syntax, and is
     // thus not importable in all versions of Node.js.
     assert.strictEqual(
-      client.version, version,
-      "Failed to update dist/version.js and dist/core/core.cjs",
+      client.version,
+      version,
+      "Failed to update dist/version.js and dist/core/core.cjs"
     );
 
     break;
   }
 
   default:
-    throw new Error(
-      "Pass either 'update' or 'verify' to config/version.js"
-    );
+    throw new Error("Pass either 'update' or 'verify' to config/version.js");
 }
 
 console.log("ok");
diff --git a/integration-tests/browser-esm/playwright.config.ts b/integration-tests/browser-esm/playwright.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/browser-esm/playwright.config.ts
@@ -0,0 +1,4 @@
+import { baseConfig } from "shared/playwright.config";
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig(baseConfig);
diff --git a/integration-tests/cra4/playwright.config.ts b/integration-tests/cra4/playwright.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/cra4/playwright.config.ts
@@ -0,0 +1,4 @@
+import { baseConfig } from "shared/playwright.config";
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig(baseConfig);
diff --git a/integration-tests/cra5/playwright.config.ts b/integration-tests/cra5/playwright.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/cra5/playwright.config.ts
@@ -0,0 +1,4 @@
+import { baseConfig } from "shared/playwright.config";
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig(baseConfig);
diff --git a/integration-tests/next/next-env.d.ts b/integration-tests/next/next-env.d.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/next/next-env.d.ts
@@ -0,0 +1,6 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+/// <reference types="next/navigation-types/compat/navigation" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/integration-tests/next/next.config.js b/integration-tests/next/next.config.js
new file mode 100644
--- /dev/null
+++ b/integration-tests/next/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+module.exports = nextConfig;
diff --git a/integration-tests/next/playwright.config.ts b/integration-tests/next/playwright.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/next/playwright.config.ts
@@ -0,0 +1,4 @@
+import { baseConfig } from "shared/playwright.config";
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig(baseConfig);
diff --git a/integration-tests/next/src/app/client.ts b/integration-tests/next/src/app/client.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/next/src/app/client.ts
@@ -0,0 +1,10 @@
+import { schemaLink } from "@/libs/schemaLink";
+import { ApolloClient, InMemoryCache } from "@apollo/client";
+import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";
+
+export const { getClient } = registerApolloClient(() => {
+  return new ApolloClient({
+    cache: new InMemoryCache(),
+    link: schemaLink,
+  });
+});
diff --git a/integration-tests/next/src/libs/apolloClient.ts b/integration-tests/next/src/libs/apolloClient.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/next/src/libs/apolloClient.ts
@@ -0,0 +1,111 @@
+import { useRef } from "react";
+import type { NormalizedCacheObject } from "@apollo/client";
+import {
+  ApolloClient,
+  HttpLink,
+  InMemoryCache,
+  from,
+  ApolloLink,
+  Observable,
+} from "@apollo/client";
+import { onError } from "@apollo/client/link/error";
+import merge from "deepmerge";
+import isEqual from "lodash/isEqual";
+import type { GetServerSidePropsResult } from "next";
+import { schemaLink } from "./schemaLink";
+
+export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";
+
+let apolloClient: ApolloClient<NormalizedCacheObject>;
+
+const errorLink = onError(({ graphQLErrors, networkError }) => {
+  if (graphQLErrors)
+    graphQLErrors.forEach(({ message, locations, path }) =>
+      console.log(
+        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
+      )
+    );
+  if (networkError) console.log(`[Network error]: ${networkError}`);
+});
+
+const delayLink = new ApolloLink((operation, forward) => {
+  return new Observable((observer) => {
+    const handle = setTimeout(() => {
+      forward(operation).subscribe(observer);
+    }, 1000);
+
+    return () => clearTimeout(handle);
+  });
+});
+
+const httpLink = new HttpLink({
+  uri: "https://main--hack-the-e-commerce.apollographos.net/graphql",
+});
+
+function createApolloClient() {
+  return new ApolloClient({
+    ssrMode: typeof window === "undefined",
+    link: from([
+      errorLink,
+      delayLink,
+      typeof window === "undefined" ? schemaLink : httpLink,
+    ]),
+    cache: new InMemoryCache(),
+  });
+}
+
+export function initializeApollo(
+  initialState: NormalizedCacheObject | null = null
+) {
+  const _apolloClient = apolloClient ?? createApolloClient();
+  // If your page has Next.js data fetching methods that use Apollo Client,
+  //  the initial state gets hydrated here
+  if (initialState) {
+    // Get existing cache, loaded during client side data fetching
+    const existingCache = _apolloClient.extract();
+
+    // Merge the initialState from getStaticProps/getServerSideProps
+    // in the existing cache
+    const data = merge(existingCache, initialState, {
+      // combine arrays using object equality (like in sets)
+      arrayMerge: (destinationArray, sourceArray) => [
+        ...sourceArray,
+        ...destinationArray.filter((d) =>
+          sourceArray.every((s) => !isEqual(d, s))
+        ),
+      ],
+    });
+    // Restore the cache with the merged data
+    _apolloClient.cache.restore(data);
+  }
+  // For SSG and SSR always create a new Apollo Client
+  if (typeof window === "undefined") return _apolloClient;
+  // Create the Apollo Client once in the client
+  if (!apolloClient) apolloClient = _apolloClient;
+  return _apolloClient;
+}
+
+interface ApolloProps {
+  [APOLLO_STATE_PROP_NAME]: NormalizedCacheObject;
+}
+
+export function addApolloState(
+  client: ApolloClient<NormalizedCacheObject>,
+  pageProps: GetServerSidePropsResult<Partial<ApolloProps>> & {
+    props: Partial<ApolloProps>;
+  }
+) {
+  if (pageProps?.props) {
+    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
+  }
+  return pageProps;
+}
+
+export function useApollo(pageProps?: ApolloProps) {
+  const state = pageProps?.[APOLLO_STATE_PROP_NAME];
+  const storeRef = useRef<ApolloClient<NormalizedCacheObject>>();
+  if (!storeRef.current) {
+    storeRef.current = initializeApollo(state);
+  }
+  return storeRef.current;
+}
diff --git a/integration-tests/next/src/libs/schemaLink.ts b/integration-tests/next/src/libs/schemaLink.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/next/src/libs/schemaLink.ts
@@ -0,0 +1,51 @@
+import { makeExecutableSchema } from "@graphql-tools/schema";
+import { gql } from "graphql-tag";
+import { SchemaLink } from "@apollo/client/link/schema";
+
+const typeDefs = gql`
+  type Product {
+    id: String!
+    title: String!
+  }
+  type Query {
+    products: [Product!]!
+  }
+`;
+
+const resolvers = {
+  Query: {
+    products: async () => [
+      {
+        id: "product:5",
+        title: "Soft Warm Apollo Beanie",
+      },
+      {
+        id: "product:2",
+        title: "Stainless Steel Water Bottle",
+      },
+      {
+        id: "product:3",
+        title: "Athletic Baseball Cap",
+      },
+      {
+        id: "product:4",
+        title: "Baby Onesies",
+      },
+      {
+        id: "product:1",
+        title: "The Apollo T-Shirt",
+      },
+      {
+        id: "product:6",
+        title: "The Apollo Socks",
+      },
+    ],
+  },
+};
+
+export const schema = makeExecutableSchema({
+  typeDefs,
+  resolvers,
+});
+
+export const schemaLink = new SchemaLink({ schema });
diff --git a/integration-tests/node-esm/test-cjs.cjs b/integration-tests/node-esm/test-cjs.cjs
new file mode 100644
--- /dev/null
+++ b/integration-tests/node-esm/test-cjs.cjs
@@ -0,0 +1,45 @@
+const assert = require("node:assert");
+const path = require("path");
+
+const { ApolloClient } = require("@apollo/client");
+const { useQuery } = require("@apollo/client/react");
+const { HttpLink } = require("@apollo/client/link/http");
+
+console.log("Testing Node with CJS imports...");
+
+function checkFunctionName(fn, name, category) {
+  console.log(`Checking ${category} '${name}' === '${fn.name}'`);
+  assert(
+    fn.name === name,
+    `${category} \`${name}\` did not import correctly (name: '${fn.name}')`
+  );
+}
+
+const entries = [
+  [ApolloClient, "ApolloClient", "Barrel Import"],
+  [useQuery, "useQuery", "Apollo React"],
+  [HttpLink, "HttpLink", "Link"],
+];
+
+for (let [fn, name, category] of entries) {
+  try {
+    checkFunctionName(fn, name, category);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+const moduleNames = [
+  ["@apollo/client", "/main.cjs"],
+  ["@apollo/client/react", "/react/react.cjs"],
+  ["@apollo/client/link/http", "/link/http/http.cjs"],
+];
+
+for (let [moduleName, expectedFilename] of moduleNames) {
+  const modulePath = require.resolve(moduleName);
+  const posixPath = modulePath.split(path.sep).join(path.posix.sep);
+  console.log(`Module: ${moduleName}, path: ${posixPath}`);
+  assert(posixPath.endsWith(expectedFilename));
+}
+
+console.log("CJS test succeeded");
diff --git a/integration-tests/node-esm/test-esm.mjs b/integration-tests/node-esm/test-esm.mjs
new file mode 100644
--- /dev/null
+++ b/integration-tests/node-esm/test-esm.mjs
@@ -0,0 +1,50 @@
+// TODO This entire file doesn't work yet without appending `/index.js` to all imports manually!
+
+import assert from "node:assert";
+import path from "path";
+import { importMetaResolve } from "resolve-esm";
+
+import { ApolloClient } from "@apollo/client/index.js";
+import { useQuery } from "@apollo/client/react/index.js";
+import { HttpLink } from "@apollo/client/link/http/index.js";
+
+console.log(
+  "Testing Node with ESM imports...  (user-side workaround with `/index.js)"
+);
+
+function checkFunctionName(fn, name, category) {
+  console.log(`Checking ${category} '${name}' === '${fn.name}'`);
+  assert(
+    fn.name === name,
+    `${category} \`${name}\` did not import correctly (name: '${fn.name}')`
+  );
+}
+
+const entries = [
+  [ApolloClient, "ApolloClient", "Barrel Import"],
+  [useQuery, "useQuery", "Apollo React"],
+  [HttpLink, "HttpLink", "Link"],
+];
+
+for (let [fn, name, category] of entries) {
+  try {
+    checkFunctionName(fn, name, category);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+const moduleNames = [
+  ["@apollo/client/index.js", "/index.js"],
+  ["@apollo/client/react/index.js", "/react/index.js"],
+  ["@apollo/client/link/http/index.js", "/link/http/index.js"],
+];
+
+(async () => {
+  for (let [moduleName, expectedFilename] of moduleNames) {
+    const modulePath = await importMetaResolve(moduleName);
+    const posixPath = modulePath.split(path.sep).join(path.posix.sep);
+    console.log(`Module: ${moduleName}, path: ${posixPath}`);
+    assert(posixPath.endsWith(expectedFilename));
+  }
+})();
diff --git a/integration-tests/node-standard/test-cjs.js b/integration-tests/node-standard/test-cjs.js
new file mode 100644
--- /dev/null
+++ b/integration-tests/node-standard/test-cjs.js
@@ -0,0 +1,45 @@
+const assert = require("node:assert");
+const path = require("path");
+
+const { ApolloClient } = require("@apollo/client");
+const { useQuery } = require("@apollo/client/react");
+const { HttpLink } = require("@apollo/client/link/http");
+
+console.log("Testing Node with CJS imports...");
+
+function checkFunctionName(fn, name, category) {
+  console.log(`Checking ${category} '${name}' === '${fn.name}'`);
+  assert(
+    fn.name === name,
+    `${category} \`${name}\` did not import correctly (name: '${fn.name}')`
+  );
+}
+
+const entries = [
+  [ApolloClient, "ApolloClient", "Barrel Import"],
+  [useQuery, "useQuery", "Apollo React"],
+  [HttpLink, "HttpLink", "Link"],
+];
+
+for (let [fn, name, category] of entries) {
+  try {
+    checkFunctionName(fn, name, category);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+const moduleNames = [
+  ["@apollo/client", "/main.cjs"],
+  ["@apollo/client/react", "/react/react.cjs"],
+  ["@apollo/client/link/http", "/link/http/http.cjs"],
+];
+
+for (let [moduleName, expectedFilename] of moduleNames) {
+  const modulePath = require.resolve(moduleName);
+  const posixPath = modulePath.split(path.sep).join(path.posix.sep);
+  console.log(`Module: ${moduleName}, path: ${posixPath}`);
+  assert(posixPath.endsWith(expectedFilename));
+}
+
+console.log("CJS test succeeded");
diff --git a/integration-tests/node-standard/test-esm.mjs b/integration-tests/node-standard/test-esm.mjs
new file mode 100644
--- /dev/null
+++ b/integration-tests/node-standard/test-esm.mjs
@@ -0,0 +1,50 @@
+// TODO This entire file doesn't work yet without appending `/index.js` to all imports manually!
+
+import assert from "node:assert";
+import path from "path";
+import { importMetaResolve } from "resolve-esm";
+
+import { ApolloClient } from "@apollo/client/index.js";
+import { useQuery } from "@apollo/client/react/index.js";
+import { HttpLink } from "@apollo/client/link/http/index.js";
+
+console.log(
+  "Testing Node with ESM imports... (user-side workaround with `/index.js)"
+);
+
+function checkFunctionName(fn, name, category) {
+  console.log(`Checking ${category} '${name}' === '${fn.name}'`);
+  assert(
+    fn.name === name,
+    `${category} \`${name}\` did not import correctly (name: '${fn.name}')`
+  );
+}
+
+const entries = [
+  [ApolloClient, "ApolloClient", "Barrel Import"],
+  [useQuery, "useQuery", "Apollo React"],
+  [HttpLink, "HttpLink", "Link"],
+];
+
+for (let [fn, name, category] of entries) {
+  try {
+    checkFunctionName(fn, name, category);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+const moduleNames = [
+  ["@apollo/client/index.js", "/index.js"],
+  ["@apollo/client/react/index.js", "/react/index.js"],
+  ["@apollo/client/link/http/index.js", "/link/http/index.js"],
+];
+
+(async () => {
+  for (let [moduleName, expectedFilename] of moduleNames) {
+    const modulePath = await importMetaResolve(moduleName);
+    const posixPath = modulePath.split(path.sep).join(path.posix.sep);
+    console.log(`Module: ${moduleName}, path: ${posixPath}`);
+    assert(posixPath.endsWith(expectedFilename));
+  }
+})();
diff --git a/integration-tests/shared/fixture.ts b/integration-tests/shared/fixture.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/shared/fixture.ts
@@ -0,0 +1,27 @@
+import { test as base, expect } from "@playwright/test";
+
+export const test = base.extend<{
+  withHar: import("@playwright/test").Page;
+  blockRequest: import("@playwright/test").Page;
+}>({
+  page: async ({ page }, use) => {
+    page.on("pageerror", (error) => {
+      expect(error.stack || error).toBe("no error");
+    });
+    await use(page);
+  },
+  withHar: async ({ page }, use) => {
+    await page.routeFromHAR("../api.har", {
+      url: "**/graphql",
+      notFound: "abort",
+    });
+    await use(page);
+  },
+  blockRequest: async ({ page }, use) => {
+    await page.routeFromHAR("../empty.har", {
+      url: "**/graphql",
+      notFound: "abort",
+    });
+    await use(page);
+  },
+});
diff --git a/integration-tests/shared/playwright.config.ts b/integration-tests/shared/playwright.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/shared/playwright.config.ts
@@ -0,0 +1,15 @@
+export const baseConfig = {
+  webServer: {
+    command: "yarn serve-app",
+    url: "http://localhost:3000",
+    timeout: 120 * 1000,
+    reuseExistingServer: !process.env.CI,
+  },
+  timeout: 120 * 1000,
+  use: {
+    headless: true,
+    viewport: { width: 1280, height: 720 },
+    ignoreHTTPSErrors: true,
+  },
+  testDir: "tests/playwright/",
+} as const;
diff --git a/integration-tests/vite-swc/playwright.config.ts b/integration-tests/vite-swc/playwright.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/vite-swc/playwright.config.ts
@@ -0,0 +1,4 @@
+import { baseConfig } from "shared/playwright.config.ts";
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig(baseConfig);
diff --git a/integration-tests/vite-swc/vite.config.ts b/integration-tests/vite-swc/vite.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/vite-swc/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [react()],
+});
diff --git a/integration-tests/vite/playwright.config.ts b/integration-tests/vite/playwright.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/vite/playwright.config.ts
@@ -0,0 +1,4 @@
+import { baseConfig } from "shared/playwright.config.ts";
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig(baseConfig);
diff --git a/integration-tests/vite/vite.config.ts b/integration-tests/vite/vite.config.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/vite/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [react()],
+});
diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts
--- a/src/cache/core/cache.ts
+++ b/src/cache/core/cache.ts
@@ -1,17 +1,20 @@
-import { DocumentNode } from 'graphql';
+import type { DocumentNode } from 'graphql';
 import { wrap } from 'optimism';
 
-import {
+import type {
   StoreObject,
-  Reference,
+  Reference} from '../../utilities/index.js';
+import {
   getFragmentQueryDocument,
-} from '../../utilities';
-import { DataProxy } from './types/DataProxy';
-import { Cache } from './types/Cache';
+} from '../../utilities/index.js';
+import type { DataProxy } from './types/DataProxy.js';
+import type { Cache } from './types/Cache.js';
 
 export type Transaction<T> = (c: ApolloCache<T>) => void;
 
 export abstract class ApolloCache<TSerialized> implements DataProxy {
+  public readonly assumeImmutableResults: boolean = false;
+
   // required to implement
   // core API
   public abstract read<TData = any, TVariables = any>(
@@ -117,7 +120,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
     return [];
   }
 
-  public modify(options: Cache.ModifyOptions): boolean {
+  public modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache.ModifyOptions<Entity>): boolean {
     return false;
   }
 
diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts
--- a/src/cache/core/types/Cache.ts
+++ b/src/cache/core/types/Cache.ts
@@ -1,6 +1,6 @@
-import { DataProxy } from './DataProxy';
-import { Modifier, Modifiers } from './common';
-import { ApolloCache } from '../cache';
+import { DataProxy } from './DataProxy.js';
+import type { AllFieldsModifier, Modifiers } from './common.js';;
+import type { ApolloCache } from '../cache.js';
 
 export namespace Cache {
   export type WatchCallback<TData = any> = (
@@ -57,9 +57,9 @@ export namespace Cache {
     discardWatches?: boolean;
   }
 
-  export interface ModifyOptions {
+  export interface ModifyOptions<Entity extends Record<string, any> = Record<string, any>> {
     id?: string;
-    fields: Modifiers | Modifier<any>;
+    fields: Modifiers<Entity> | AllFieldsModifier<Entity>;
     optimistic?: boolean;
     broadcast?: boolean;
   }
diff --git a/src/cache/core/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts
--- a/src/cache/core/types/DataProxy.ts
+++ b/src/cache/core/types/DataProxy.ts
@@ -1,8 +1,8 @@
-import { DocumentNode } from 'graphql'; // eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import type { DocumentNode } from 'graphql'; // ignore-comment eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved
+import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
 
-import { MissingFieldError } from './common';
-import { Reference } from '../../../utilities';
+import type { MissingFieldError } from './common.js';
+import type { Reference } from '../../../utilities/index.js';
 
 export namespace DataProxy {
   export interface Query<TVariables, TData> {
diff --git a/src/cache/core/types/common.ts b/src/cache/core/types/common.ts
--- a/src/cache/core/types/common.ts
+++ b/src/cache/core/types/common.ts
@@ -1,13 +1,13 @@
-import { DocumentNode, FieldNode } from 'graphql';
+import type { DocumentNode, FieldNode } from 'graphql';
 
-import {
+import type {
   Reference,
   StoreObject,
   StoreValue,
   isReference,
-} from '../../../utilities';
+} from '../../../utilities/index.js';
 
-import { StorageType } from '../../inmemory/policies';
+import type { StorageType } from '../../inmemory/policies.js';
 
 // The Readonly<T> type only really works for object types, since it marks
 // all of the object's properties as readonly, but there are many cases when
@@ -76,9 +76,14 @@ export type ToReferenceFunction = (
 
 export type CanReadFunction = (value: StoreValue) => boolean;
 
+declare const _deleteModifier: unique symbol;
+export interface DeleteModifier { [_deleteModifier]: true }
+declare const _invalidateModifier: unique symbol;
+export interface InvalidateModifier { [_invalidateModifier]: true}
+
 export type ModifierDetails = {
-  DELETE: any;
-  INVALIDATE: any;
+  DELETE: DeleteModifier;
+  INVALIDATE: InvalidateModifier;
   fieldName: string;
   storeFieldName: string;
   readField: ReadFieldFunction;
@@ -88,8 +93,28 @@ export type ModifierDetails = {
   storage: StorageType;
 }
 
-export type Modifier<T> = (value: T, details: ModifierDetails) => T;
-
-export type Modifiers = {
-  [fieldName: string]: Modifier<any>;
-};
+export type Modifier<T> = (
+  value: T,
+  details: ModifierDetails
+) => T | DeleteModifier | InvalidateModifier;
+
+type StoreObjectValueMaybeReference<StoreVal> =
+  StoreVal extends Record<string, any>[]
+  ? Readonly<StoreVal> | readonly Reference[]
+  : StoreVal extends Record<string, any>
+  ? StoreVal | Reference
+  : StoreVal;
+
+export type AllFieldsModifier<
+  Entity extends Record<string, any>
+> = Modifier<Entity[keyof Entity] extends infer Value ?
+  StoreObjectValueMaybeReference<Exclude<Value, undefined>>
+  : never>;
+
+export type Modifiers<
+  T extends Record<string, any> = Record<string, unknown>
+> = Partial<{
+  [FieldName in keyof T]: Modifier<
+    StoreObjectValueMaybeReference<Exclude<T[FieldName], undefined>>
+  >;
+}>;
diff --git a/src/cache/index.ts b/src/cache/index.ts
--- a/src/cache/index.ts
+++ b/src/cache/index.ts
@@ -1,40 +1,35 @@
-import '../utilities/globals';
+import '../utilities/globals/index.js';
 
-export { Transaction, ApolloCache } from './core/cache';
-export { Cache } from './core/types/Cache';
-export { DataProxy } from './core/types/DataProxy';
-export {
+export type { Transaction } from './core/cache.js';
+export { ApolloCache } from './core/cache.js';
+export { Cache } from './core/types/Cache.js';
+export type { DataProxy } from './core/types/DataProxy.js';
+export type {
   MissingTree,
   Modifier,
   Modifiers,
   ModifierDetails,
-  MissingFieldError,
   ReadFieldOptions
-} from './core/types/common';
+} from './core/types/common.js';
+export { MissingFieldError } from './core/types/common.js';
 
-export {
-  Reference,
-  isReference,
-  makeReference,
-} from '../utilities';
+export type { Reference } from '../utilities/index.js';
+export { isReference, makeReference } from '../utilities/index.js';
 
-export { EntityStore } from './inmemory/entityStore';
+export { EntityStore } from './inmemory/entityStore.js';
 export {
   fieldNameFromStoreName,
   defaultDataIdFromObject,
-} from './inmemory/helpers'
+} from './inmemory/helpers.js'
 
 export {
   InMemoryCache,
-} from './inmemory/inMemoryCache';
+} from './inmemory/inMemoryCache.js';
 
-export {
-  ReactiveVar,
-  makeVar,
-  cacheSlot,
-} from './inmemory/reactiveVars';
+export type { ReactiveVar } from './inmemory/reactiveVars.js';
+export { makeVar, cacheSlot } from './inmemory/reactiveVars.js';
 
-export {
+export type {
   TypePolicies,
   TypePolicy,
   FieldPolicy,
@@ -42,16 +37,14 @@ export {
   FieldMergeFunction,
   FieldFunctionOptions,
   PossibleTypesMap,
-  Policies,
-} from './inmemory/policies';
+} from './inmemory/policies.js';
+export { Policies } from './inmemory/policies.js';
 
 export {
   canonicalStringify,
-} from './inmemory/object-canon';
+} from './inmemory/object-canon.js';
 
-export {
-  FragmentRegistryAPI,
-  createFragmentRegistry,
-} from './inmemory/fragmentRegistry';
+export type { FragmentRegistryAPI } from './inmemory/fragmentRegistry.js';
+export { createFragmentRegistry } from './inmemory/fragmentRegistry.js';
 
-export * from './inmemory/types';
+export * from './inmemory/types.js';
diff --git a/src/cache/inmemory/__mocks__/optimism.ts b/src/cache/inmemory/__mocks__/optimism.ts
deleted file mode 100644
--- a/src/cache/inmemory/__mocks__/optimism.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-const optimism = jest.requireActual('optimism');
-module.exports = {
-  ...optimism,
-  wrap: jest.fn(optimism.wrap),
-};
diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts
--- a/src/cache/inmemory/entityStore.ts
+++ b/src/cache/inmemory/entityStore.ts
@@ -1,35 +1,40 @@
-import { invariant } from '../../utilities/globals';
-import { dep, OptimisticDependencyFunction } from 'optimism';
+import { invariant } from '../../utilities/globals/index.js';
+import type { OptimisticDependencyFunction } from 'optimism';
+import { dep } from 'optimism';
 import { equal } from '@wry/equality';
 import { Trie } from '@wry/trie';
 
-import {
-  isReference,
+import type {
   StoreValue,
   StoreObject,
-  Reference,
+  Reference} from '../../utilities/index.js';
+import {
+  isReference,
   makeReference,
   DeepMerger,
   maybeDeepFreeze,
   canUseWeakMap,
   isNonNullObject,
-} from '../../utilities';
-import { NormalizedCache, NormalizedCacheObject } from './types';
-import { hasOwn, fieldNameFromStoreName } from './helpers';
-import { Policies, StorageType } from './policies';
-import { Cache } from '../core/types/Cache';
-import {
+} from '../../utilities/index.js';
+import type { NormalizedCache, NormalizedCacheObject } from './types.js';
+import { hasOwn, fieldNameFromStoreName } from './helpers.js';
+import type { Policies, StorageType } from './policies.js';
+import type { Cache } from '../core/types/Cache.js';
+import type {
   SafeReadonly,
   Modifier,
   Modifiers,
   ReadFieldOptions,
   ToReferenceFunction,
   CanReadFunction,
-} from '../core/types/common';
+  InvalidateModifier,
+  DeleteModifier,
+  ModifierDetails,
+} from '../core/types/common.js';
 
-const DELETE: any = Object.create(null);
+const DELETE: DeleteModifier = Object.create(null);
 const delModifier: Modifier<any> = () => DELETE;
-const INVALIDATE: any = Object.create(null);
+const INVALIDATE: InvalidateModifier = Object.create(null);
 
 export abstract class EntityStore implements NormalizedCache {
   protected data: NormalizedCacheObject = Object.create(null);
@@ -190,7 +195,7 @@ export abstract class EntityStore implements NormalizedCache {
 
   public modify(
     dataId: string,
-    fields: Modifier<any> | Modifiers,
+    fields: Modifier<any> | Modifiers<Record<string, any>>,
   ): boolean {
     const storeObject = this.lookup(dataId);
 
@@ -215,13 +220,13 @@ export abstract class EntityStore implements NormalizedCache {
           } : fieldNameOrOptions,
           { store: this },
         ),
-      };
+      } satisfies Partial<ModifierDetails>;
 
       Object.keys(storeObject).forEach(storeFieldName => {
         const fieldName = fieldNameFromStoreName(storeFieldName);
         let fieldValue = storeObject[storeFieldName];
         if (fieldValue === void 0) return;
-        const modify: Modifier<StoreValue> = typeof fields === "function"
+        const modify: Modifier<StoreValue> | undefined = typeof fields === "function"
           ? fields
           : fields[storeFieldName] || fields[fieldName];
         if (modify) {
diff --git a/src/cache/inmemory/fragmentRegistry.ts b/src/cache/inmemory/fragmentRegistry.ts
--- a/src/cache/inmemory/fragmentRegistry.ts
+++ b/src/cache/inmemory/fragmentRegistry.ts
@@ -1,14 +1,16 @@
-import {
+import type {
   DocumentNode,
   ASTNode,
   FragmentDefinitionNode,
-  FragmentSpreadNode,
+  FragmentSpreadNode} from "graphql";
+import {
   visit,
 } from "graphql";
 
 import { wrap } from "optimism";
 
-import { FragmentMap, getFragmentDefinitions } from "../../utilities";
+import type { FragmentMap} from "../../utilities/index.js";
+import { getFragmentDefinitions } from "../../utilities/index.js";
 
 export interface FragmentRegistryAPI {
   register(...fragments: DocumentNode[]): this;
diff --git a/src/cache/inmemory/helpers.ts b/src/cache/inmemory/helpers.ts
--- a/src/cache/inmemory/helpers.ts
+++ b/src/cache/inmemory/helpers.ts
@@ -1,30 +1,31 @@
-import { DocumentNode, FragmentDefinitionNode, SelectionSetNode } from 'graphql';
+import type { DocumentNode, FragmentDefinitionNode, SelectionSetNode } from 'graphql';
 
-import {
+import type {
   NormalizedCache,
   InMemoryCacheConfig,
-} from './types';
+} from './types.js';
 
-import { KeyFieldsContext } from './policies';
-import { FragmentRegistryAPI } from './fragmentRegistry';
+import type { KeyFieldsContext } from './policies.js';
+import type { FragmentRegistryAPI } from './fragmentRegistry.js';
 
-import {
+import type {
   Reference,
-  isReference,
   StoreValue,
   StoreObject,
+  FragmentMap,
+  FragmentMapFunction} from '../../utilities/index.js';
+import {
+  isReference,
   isField,
   DeepMerger,
   resultKeyNameFromField,
   shouldInclude,
   isNonNullObject,
   compact,
-  FragmentMap,
-  FragmentMapFunction,
   createFragmentMap,
   getFragmentDefinitions,
   isArray,
-} from '../../utilities';
+} from '../../utilities/index.js';
 
 export const {
   hasOwnProperty: hasOwn,
diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts
--- a/src/cache/inmemory/inMemoryCache.ts
+++ b/src/cache/inmemory/inMemoryCache.ts
@@ -1,30 +1,36 @@
-import { invariant } from '../../utilities/globals';
+import { invariant } from '../../utilities/globals/index.js';
 
 // Make builtins like Map and Set safe to use with non-extensible objects.
-import './fixPolyfills';
+import './fixPolyfills.js';
 
-import { DocumentNode } from 'graphql';
-import { OptimisticWrapperFunction, wrap } from 'optimism';
+import type { DocumentNode } from 'graphql';
+import type { OptimisticWrapperFunction} from 'optimism';
+import { wrap } from 'optimism';
 import { equal } from '@wry/equality';
 
-import { ApolloCache } from '../core/cache';
-import { Cache } from '../core/types/Cache';
-import { MissingFieldError } from '../core/types/common';
+import { ApolloCache } from '../core/cache.js';
+import type { Cache } from '../core/types/Cache.js';
+import { MissingFieldError } from '../core/types/common.js';
+import type {
+  StoreObject,
+  Reference} from '../../utilities/index.js';
 import {
   addTypenameToDocument,
-  StoreObject,
-  Reference,
   isReference,
-} from '../../utilities';
-import { InMemoryCacheConfig, NormalizedCacheObject } from './types';
-import { StoreReader } from './readFromStore';
-import { StoreWriter } from './writeToStore';
-import { EntityStore, supportsResultCaching } from './entityStore';
-import { makeVar, forgetCache, recallCache } from './reactiveVars';
-import { Policies } from './policies';
-import { hasOwn, normalizeConfig, shouldCanonizeResults } from './helpers';
-import { canonicalStringify } from './object-canon';
-import { OperationVariables } from '../../core';
+  DocumentTransform,
+} from '../../utilities/index.js';
+import type {
+  InMemoryCacheConfig,
+  NormalizedCacheObject,
+} from './types.js';
+import { StoreReader } from './readFromStore.js';
+import { StoreWriter } from './writeToStore.js';
+import { EntityStore, supportsResultCaching } from './entityStore.js';
+import { makeVar, forgetCache, recallCache } from './reactiveVars.js';
+import { Policies } from './policies.js';
+import { hasOwn, normalizeConfig, shouldCanonizeResults } from './helpers.js';
+import { canonicalStringify } from './object-canon.js';
+import type { OperationVariables } from '../../core/index.js';
 
 type BroadcastOptions = Pick<
   Cache.BatchOptions<InMemoryCache>,
@@ -40,15 +46,19 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
   private watches = new Set<Cache.WatchOptions>();
   private addTypename: boolean;
 
-  private typenameDocumentCache = new Map<DocumentNode, DocumentNode>();
   private storeReader: StoreReader;
   private storeWriter: StoreWriter;
+  private addTypenameTransform = new DocumentTransform(addTypenameToDocument);
 
   private maybeBroadcastWatch: OptimisticWrapperFunction<
     [Cache.WatchOptions, BroadcastOptions?],
     any,
     [Cache.WatchOptions]>;
 
+  // Override the default value, since InMemoryCache result objects are frozen
+  // in development and expected to remain logically immutable in production.
+  public readonly assumeImmutableResults = true;
+
   // Dynamically imported code can augment existing typePolicies or
   // possibleTypes by calling cache.policies.addTypePolicies or
   // cache.policies.addPossibletypes.
@@ -204,7 +214,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
     }
   }
 
-  public modify(options: Cache.ModifyOptions): boolean {
+  public modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache.ModifyOptions<Entity>): boolean {
     if (hasOwn.call(options, "id") && !options.id) {
       // To my knowledge, TypeScript does not currently provide a way to
       // enforce that an optional property?:type must *not* be undefined
@@ -503,32 +513,27 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
   }
 
   public transformDocument(document: DocumentNode): DocumentNode {
-    if (this.addTypename) {
-      let result = this.typenameDocumentCache.get(document);
-      if (!result) {
-        result = addTypenameToDocument(document);
-        this.typenameDocumentCache.set(document, result);
-        // If someone calls transformDocument and then mistakenly passes the
-        // result back into an API that also calls transformDocument, make sure
-        // we don't keep creating new query documents.
-        this.typenameDocumentCache.set(result, result);
-      }
-      return result;
+    return this.addTypenameToDocument(this.addFragmentsToDocument(document));
+  }
+
+  protected broadcastWatches(options?: BroadcastOptions) {
+    if (!this.txCount) {
+      this.watches.forEach(c => this.maybeBroadcastWatch(c, options));
     }
-    return document;
   }
 
-  public transformForLink(document: DocumentNode): DocumentNode {
+  private addFragmentsToDocument(document: DocumentNode) {
     const { fragments } = this.config;
     return fragments
       ? fragments.transform(document)
       : document;
   }
 
-  protected broadcastWatches(options?: BroadcastOptions) {
-    if (!this.txCount) {
-      this.watches.forEach(c => this.maybeBroadcastWatch(c, options));
+  private addTypenameToDocument(document: DocumentNode) {
+    if (this.addTypename) {
+      return this.addTypenameTransform.transformDocument(document);
     }
+    return document;
   }
 
   // This method is wrapped by maybeBroadcastWatch, which is called by
diff --git a/src/cache/inmemory/key-extractor.ts b/src/cache/inmemory/key-extractor.ts
--- a/src/cache/inmemory/key-extractor.ts
+++ b/src/cache/inmemory/key-extractor.ts
@@ -1,18 +1,18 @@
-import { invariant } from "../../utilities/globals";
+import { invariant } from "../../utilities/globals/index.js";
 
 import {
   argumentsObjectFromField,
   DeepMerger,
   isNonEmptyArray,
   isNonNullObject,
-} from "../../utilities";
+} from "../../utilities/index.js";
 
-import { hasOwn, isArray } from "./helpers";
-import {
+import { hasOwn, isArray } from "./helpers.js";
+import type {
   KeySpecifier,
   KeyFieldsFunction,
   KeyArgsFunction,
-} from "./policies";
+} from "./policies.js";
 
 // Mapping from JSON-encoded KeySpecifier strings to associated information.
 const specifierInfoCache: Record<string, {
@@ -73,9 +73,9 @@ export function keyFieldsFnFromSpecifier(
 
         invariant(
           extracted !== void 0,
-          `Missing field '${schemaKeyPath.join('.')}' while extracting keyFields from ${
-            JSON.stringify(object)
-          }`,
+          `Missing field '%s' while extracting keyFields from %s`,
+          schemaKeyPath.join('.'),
+          object,
         );
 
         return extracted;
diff --git a/src/cache/inmemory/object-canon.ts b/src/cache/inmemory/object-canon.ts
--- a/src/cache/inmemory/object-canon.ts
+++ b/src/cache/inmemory/object-canon.ts
@@ -1,12 +1,10 @@
-import "../../utilities/globals";
-
 import { Trie } from "@wry/trie";
 import {
   canUseWeakMap,
   canUseWeakSet,
   isNonNullObject as isObjectOrArray,
-} from "../../utilities";
-import { isArray } from "./helpers";
+} from "../../utilities/index.js";
+import { isArray } from "./helpers.js";
 
 function shallowCopy<T>(value: T): T {
   if (isObjectOrArray(value)) {
diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts
--- a/src/cache/inmemory/policies.ts
+++ b/src/cache/inmemory/policies.ts
@@ -1,30 +1,31 @@
-import { invariant, InvariantError } from '../../utilities/globals';
+import { invariant, newInvariantError } from '../../utilities/globals/index.js';
 
-import {
+import type {
   InlineFragmentNode,
   FragmentDefinitionNode,
   SelectionSetNode,
   FieldNode,
 } from 'graphql';
 
-import {
+import type {
   FragmentMap,
-  storeKeyNameFromField,
   StoreValue,
   StoreObject,
+  Reference} from '../../utilities/index.js';
+import {
+  storeKeyNameFromField,
   argumentsObjectFromField,
-  Reference,
   isReference,
   getStoreKeyName,
   isNonNullObject,
   stringifyForDisplay,
-} from '../../utilities';
-import {
+} from '../../utilities/index.js';
+import type {
   IdGetter,
   MergeInfo,
   NormalizedCache,
   ReadMergeModifyContext,
-} from "./types";
+} from "./types.js";
 import {
   hasOwn,
   fieldNameFromStoreName,
@@ -33,24 +34,24 @@ import {
   TypeOrFieldNameRegExp,
   defaultDataIdFromObject,
   isArray,
-} from './helpers';
-import { cacheSlot } from './reactiveVars';
-import { InMemoryCache } from './inMemoryCache';
-import {
+} from './helpers.js';
+import { cacheSlot } from './reactiveVars.js';
+import type { InMemoryCache } from './inMemoryCache.js';
+import type {
   SafeReadonly,
   FieldSpecifier,
   ToReferenceFunction,
   ReadFieldFunction,
   ReadFieldOptions,
   CanReadFunction,
-} from '../core/types/common';
-import { WriteContext } from './writeToStore';
+} from '../core/types/common.js';
+import type { WriteContext } from './writeToStore.js';
 
 // Upgrade to a faster version of the default stable JSON.stringify function
 // used by getStoreKeyName. This function is used when computing storeFieldName
 // strings (when no keyArgs has been configured for a field).
-import { canonicalStringify } from './object-canon';
-import { keyArgsFnFromSpecifier, keyFieldsFnFromSpecifier } from './key-extractor';
+import { canonicalStringify } from './object-canon.js';
+import { keyArgsFnFromSpecifier, keyFieldsFnFromSpecifier } from './key-extractor.js';
 
 getStoreKeyName.setStringify(canonicalStringify);
 
@@ -512,7 +513,7 @@ export class Policies {
     const rootId = "ROOT_" + which.toUpperCase();
     const old = this.rootTypenamesById[rootId];
     if (typename !== old) {
-      invariant(!old || old === which, `Cannot change root ${which} __typename more than once`);
+      invariant(!old || old === which, `Cannot change root %s __typename more than once`, which);
       // First, delete any old __typename associated with this rootId from
       // rootIdsByTypename.
       if (old) delete this.rootIdsByTypename[old];
@@ -565,11 +566,33 @@ export class Policies {
       // and merge functions often need to cooperate, so changing only one
       // of them would be a recipe for inconsistency.
       //
-      // Once the TypePolicy for typename has been accessed, its
-      // properties can still be updated directly using addTypePolicies,
-      // but future changes to supertype policies will not be reflected in
-      // this policy, because this code runs at most once per typename.
-      const supertypes = this.supertypeMap.get(typename);
+      // Once the TypePolicy for typename has been accessed, its properties can
+      // still be updated directly using addTypePolicies, but future changes to
+      // inherited supertype policies will not be reflected in this subtype
+      // policy, because this code runs at most once per typename.
+      let supertypes = this.supertypeMap.get(typename);
+      if (!supertypes && this.fuzzySubtypes.size) {
+        // To make the inheritance logic work for unknown typename strings that
+        // may have fuzzy supertypes, we give this typename an empty supertype
+        // set and then populate it with any fuzzy supertypes that match.
+        supertypes = this.getSupertypeSet(typename, true)!;
+        // This only works for typenames that are directly matched by a fuzzy
+        // supertype. What if there is an intermediate chain of supertypes?
+        // While possible, that situation can only be solved effectively by
+        // specifying the intermediate relationships via possibleTypes, manually
+        // and in a non-fuzzy way.
+        this.fuzzySubtypes.forEach((regExp, fuzzy) => {
+          if (regExp.test(typename)) {
+            // The fuzzy parameter is just the original string version of regExp
+            // (not a valid __typename string), but we can look up the
+            // associated supertype(s) in this.supertypeMap.
+            const fuzzySupertypes = this.supertypeMap.get(fuzzy);
+            if (fuzzySupertypes) {
+              fuzzySupertypes.forEach(supertype => supertypes!.add(supertype));
+            }
+          }
+        });
+      }
       if (supertypes && supertypes.size) {
         supertypes.forEach(supertype => {
           const { fields, ...rest } = this.getTypePolicy(supertype);
@@ -664,7 +687,7 @@ export class Policies {
         if (supertypeSet.has(supertype)) {
           if (!typenameSupertypeSet.has(supertype)) {
             if (checkingFuzzySubtypes) {
-              invariant.warn(`Inferring subtype ${typename} of supertype ${supertype}`);
+              invariant.warn(`Inferring subtype %s of supertype %s`, typename, supertype);
             }
             // Record positive results for faster future lookup.
             // Unfortunately, we cannot safely cache negative results,
@@ -951,9 +974,7 @@ export function normalizeReadFieldOptions(
   }
 
   if (__DEV__ && options.from === void 0) {
-    invariant.warn(`Undefined 'from' passed to readField with arguments ${
-      stringifyForDisplay(Array.from(readFieldArgs))
-    }`);
+    invariant.warn(`Undefined 'from' passed to readField with arguments %s`, stringifyForDisplay(Array.from(readFieldArgs)));
   }
 
   if (void 0 === options.variables) {
@@ -968,7 +989,7 @@ function makeMergeObjectsFunction(
 ): MergeObjectsFunction {
   return function mergeObjects(existing, incoming) {
     if (isArray(existing) || isArray(incoming)) {
-      throw new InvariantError("Cannot automatically merge arrays");
+      throw newInvariantError("Cannot automatically merge arrays");
     }
 
     // These dynamic checks are necessary because the parameters of a
diff --git a/src/cache/inmemory/reactiveVars.ts b/src/cache/inmemory/reactiveVars.ts
--- a/src/cache/inmemory/reactiveVars.ts
+++ b/src/cache/inmemory/reactiveVars.ts
@@ -1,7 +1,8 @@
-import { dep, OptimisticDependencyFunction } from "optimism";
+import type { OptimisticDependencyFunction } from "optimism";
+import { dep } from "optimism";
 import { Slot } from "@wry/context";
-import { InMemoryCache } from "./inMemoryCache";
-import { ApolloCache } from '../../core';
+import type { InMemoryCache } from "./inMemoryCache.js";
+import type { ApolloCache } from '../../core/index.js';
 
 export interface ReactiveVar<T> {
   (newValue?: T): T;
diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts
--- a/src/cache/inmemory/readFromStore.ts
+++ b/src/cache/inmemory/readFromStore.ts
@@ -1,21 +1,25 @@
-import { invariant, InvariantError } from '../../utilities/globals';
+import { invariant, newInvariantError } from '../../utilities/globals/index.js';
 
-import {
+import type {
   DocumentNode,
   FieldNode,
-  Kind,
-  SelectionSetNode,
+  SelectionSetNode} from 'graphql';
+import {
+  Kind
 } from 'graphql';
-import { wrap, OptimisticWrapperFunction } from 'optimism';
+import type { OptimisticWrapperFunction } from 'optimism';
+import { wrap } from 'optimism';
 
+import type {
+  Reference,
+  StoreObject,
+  FragmentMap,
+  FragmentMapFunction} from '../../utilities/index.js';
 import {
   isField,
   resultKeyNameFromField,
-  Reference,
   isReference,
   makeReference,
-  StoreObject,
-  FragmentMap,
   shouldInclude,
   addTypenameToDocument,
   getDefaultValues,
@@ -27,22 +31,22 @@ import {
   DeepMerger,
   isNonNullObject,
   canUseWeakMap,
-  compact,
-  FragmentMapFunction,
-} from '../../utilities';
-import { Cache } from '../core/types/Cache';
-import {
+  compact
+} from '../../utilities/index.js';
+import type { Cache } from '../core/types/Cache.js';
+import type {
   DiffQueryAgainstStoreOptions,
   InMemoryCacheConfig,
   NormalizedCache,
   ReadMergeModifyContext,
-} from './types';
-import { maybeDependOnExistenceOfEntity, supportsResultCaching } from './entityStore';
-import { isArray, extractFragmentContext, getTypenameFromStoreObject, shouldCanonizeResults } from './helpers';
-import { Policies } from './policies';
-import { InMemoryCache } from './inMemoryCache';
-import { MissingFieldError, MissingTree } from '../core/types/common';
-import { canonicalStringify, ObjectCanon } from './object-canon';
+} from './types.js';
+import { maybeDependOnExistenceOfEntity, supportsResultCaching } from './entityStore.js';
+import { isArray, extractFragmentContext, getTypenameFromStoreObject, shouldCanonizeResults } from './helpers.js';
+import type { Policies } from './policies.js';
+import type { InMemoryCache } from './inMemoryCache.js';
+import type { MissingTree } from '../core/types/common.js';
+import { MissingFieldError } from '../core/types/common.js';
+import { canonicalStringify, ObjectCanon } from './object-canon.js';
 
 export type VariableMap = { [name: string]: any };
 
@@ -408,7 +412,7 @@ export class StoreReader {
         );
 
         if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) {
-          throw new InvariantError(`No fragment named ${selection.name.value}`);
+          throw newInvariantError(`No fragment named %s`, selection.name.value);
         }
 
         if (fragment && policies.fragmentMatches(fragment, typename)) {
@@ -517,9 +521,9 @@ function assertSelectionSetForIdValue(
       if (isNonNullObject(value)) {
         invariant(
           !isReference(value),
-          `Missing selection set for object of type ${
-            getTypenameFromStoreObject(store, value)
-          } returned for query field ${field.name.value}`,
+          `Missing selection set for object of type %s returned for query field %s`,
+          getTypenameFromStoreObject(store, value),
+          field.name.value
         );
         Object.values(value).forEach(workSet.add, workSet);
       }
diff --git a/src/cache/inmemory/types.ts b/src/cache/inmemory/types.ts
--- a/src/cache/inmemory/types.ts
+++ b/src/cache/inmemory/types.ts
@@ -1,29 +1,29 @@
-import { DocumentNode, FieldNode } from 'graphql';
+import type { DocumentNode, FieldNode } from 'graphql';
 
-import { Transaction } from '../core/cache';
-import {
+import type { Transaction } from '../core/cache.js';
+import type {
   StoreObject,
   StoreValue,
   Reference,
-} from '../../utilities';
-import { FieldValueGetter } from './entityStore';
-import {
+} from '../../utilities/index.js';
+import type { FieldValueGetter } from './entityStore.js';
+import type {
   TypePolicies,
   PossibleTypesMap,
   KeyFieldsFunction,
   StorageType,
   FieldMergeFunction,
-} from './policies';
-import {
-  Modifier,
+} from './policies.js';
+import type {
   Modifiers,
   ToReferenceFunction,
   CanReadFunction,
-} from '../core/types/common';
+  AllFieldsModifier,
+} from '../core/types/common.js';
 
-import { FragmentRegistryAPI } from './fragmentRegistry';
+import type { FragmentRegistryAPI } from './fragmentRegistry.js';
 
-export { StoreObject, StoreValue, Reference }
+export type { StoreObject, StoreValue, Reference }
 
 export interface IdGetterObj extends Object {
   __typename?: string;
@@ -49,7 +49,7 @@ export interface NormalizedCache {
   merge(olderId: string, newerObject: StoreObject): void;
   merge(olderObject: StoreObject, newerId: string): void;
 
-  modify(dataId: string, fields: Modifiers | Modifier<any>): boolean;
+  modify<Entity extends Record<string, any>>(dataId: string, fields: Modifiers<Entity> | AllFieldsModifier<Entity>): boolean;
   delete(dataId: string, fieldName?: string): boolean;
   clear(): void;
 
diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts
--- a/src/cache/inmemory/writeToStore.ts
+++ b/src/cache/inmemory/writeToStore.ts
@@ -1,15 +1,20 @@
-import { invariant, InvariantError } from '../../utilities/globals';
+import { invariant, newInvariantError } from '../../utilities/globals/index.js';
 import { equal } from '@wry/equality';
 import { Trie } from '@wry/trie';
-import {
+import type {
   SelectionSetNode,
-  FieldNode,
+  FieldNode} from 'graphql';
+import {
   Kind,
 } from 'graphql';
 
-import {
+import type {
   FragmentMap,
   FragmentMapFunction,
+  StoreValue,
+  StoreObject,
+  Reference} from '../../utilities/index.js';
+import {
   getFragmentFromSelection,
   getDefaultValues,
   getOperationDefinition,
@@ -17,26 +22,23 @@ import {
   makeReference,
   isField,
   resultKeyNameFromField,
-  StoreValue,
-  StoreObject,
-  Reference,
   isReference,
   shouldInclude,
   cloneDeep,
   addTypenameToDocument,
   isNonEmptyArray,
   argumentsObjectFromField,
-} from '../../utilities';
-
-import { NormalizedCache, ReadMergeModifyContext, MergeTree, InMemoryCacheConfig } from './types';
-import { isArray, makeProcessedFieldsMerger, fieldNameFromStoreName, storeValueIsStoreObject, extractFragmentContext } from './helpers';
-import { StoreReader } from './readFromStore';
-import { InMemoryCache } from './inMemoryCache';
-import { EntityStore } from './entityStore';
-import { Cache } from '../../core';
-import { canonicalStringify } from './object-canon';
-import { normalizeReadFieldOptions } from './policies';
-import { ReadFieldFunction } from '../core/types/common';
+} from '../../utilities/index.js';
+
+import type { NormalizedCache, ReadMergeModifyContext, MergeTree, InMemoryCacheConfig } from './types.js';
+import { isArray, makeProcessedFieldsMerger, fieldNameFromStoreName, storeValueIsStoreObject, extractFragmentContext } from './helpers.js';
+import type { StoreReader } from './readFromStore.js';
+import type { InMemoryCache } from './inMemoryCache.js';
+import type { EntityStore } from './entityStore.js';
+import type { Cache } from '../../core/index.js';
+import { canonicalStringify } from './object-canon.js';
+import { normalizeReadFieldOptions } from './policies.js';
+import type { ReadFieldFunction } from '../core/types/common.js';
 
 export interface WriteContext extends ReadMergeModifyContext {
   readonly written: {
@@ -148,7 +150,7 @@ export class StoreWriter {
     });
 
     if (!isReference(ref)) {
-      throw new InvariantError(`Could not identify object ${JSON.stringify(result)}`);
+      throw newInvariantError(`Could not identify object %s`, result);
     }
 
     // So far, the store has not been modified, so now it's time to process
@@ -357,11 +359,7 @@ export class StoreWriter {
         // not be cause for alarm.
         !policies.getReadFunction(typename, field.name.value)
       ) {
-        invariant.error(`Missing field '${
-          resultKeyNameFromField(field)
-        }' while writing result ${
-          JSON.stringify(result, null, 2)
-        }`.substring(0, 1000));
+        invariant.error(`Missing field '%s' while writing result %o`, resultKeyNameFromField(field), result);
       }
     });
 
@@ -568,7 +566,7 @@ export class StoreWriter {
           );
 
           if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) {
-            throw new InvariantError(`No fragment named ${selection.name.value}`);
+            throw newInvariantError(`No fragment named %s`, selection.name.value);
           }
 
           if (fragment &&
@@ -813,25 +811,27 @@ function warnAboutDataLoss(
   }
 
   invariant.warn(
-`Cache data may be lost when replacing the ${fieldName} field of a ${parentType} object.
+`Cache data may be lost when replacing the %s field of a %s object.
 
 This could cause additional (usually avoidable) network requests to fetch data that were otherwise cached.
 
-To address this problem (which is not a bug in Apollo Client), ${
-  childTypenames.length
-    ? "either ensure all objects of type " +
-        childTypenames.join(" and ") + " have an ID or a custom merge function, or "
-    : ""
-}define a custom merge function for the ${
-  typeDotName
-} field, so InMemoryCache can safely merge these objects:
+To address this problem (which is not a bug in Apollo Client), %sdefine a custom merge function for the %s field, so InMemoryCache can safely merge these objects:
 
-  existing: ${JSON.stringify(existing).slice(0, 1000)}
-  incoming: ${JSON.stringify(incoming).slice(0, 1000)}
+  existing: %s
+  incoming: %s
 
 For more information about these options, please refer to the documentation:
 
   * Ensuring entity objects have IDs: https://go.apollo.dev/c/generating-unique-identifiers
   * Defining custom merge functions: https://go.apollo.dev/c/merging-non-normalized-objects
-`);
+`,
+  fieldName,
+  parentType,
+  childTypenames.length
+    ? "either ensure all objects of type " + childTypenames.join(" and ") + " have an ID or a custom merge function, or "
+    : "",
+  typeDotName,
+  existing,
+  incoming
+);
 }
diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts
--- a/src/config/jest/setup.ts
+++ b/src/config/jest/setup.ts
@@ -1,6 +1,18 @@
 import gql from 'graphql-tag';
+import '@testing-library/jest-dom';
+import { loadErrorMessageHandler } from '../../dev/loadErrorMessageHandler.js';
+import '../../testing/matchers/index.js';
 
 // Turn off warnings for repeated fragment names
 gql.disableFragmentWarnings();
 
 process.on('unhandledRejection', () => {});
+
+loadErrorMessageHandler();
+
+function fail(reason = "fail was called in a test.") {
+  expect(reason).toBe(undefined);
+}
+
+// @ts-ignore
+globalThis.fail = fail;
\ No newline at end of file
diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts
--- a/src/core/ApolloClient.ts
+++ b/src/core/ApolloClient.ts
@@ -1,17 +1,19 @@
-import { invariant, InvariantError } from '../utilities/globals';
+import { invariant, newInvariantError } from '../utilities/globals/index.js';
 
-import { ExecutionResult, DocumentNode } from 'graphql';
+import type { ExecutionResult, DocumentNode } from 'graphql';
 
-import { ApolloLink, FetchResult, GraphQLRequest, execute } from '../link/core';
-import { ApolloCache, DataProxy, Reference } from '../cache';
-import { Observable } from '../utilities';
-import { version } from '../version';
-import { HttpLink, UriFunction } from '../link/http';
+import type { FetchResult, GraphQLRequest} from '../link/core/index.js';
+import { ApolloLink, execute } from '../link/core/index.js';
+import type { ApolloCache, DataProxy, Reference } from '../cache/index.js';
+import type { DocumentTransform, Observable } from '../utilities/index.js';
+import { version } from '../version.js';
+import type { UriFunction } from '../link/http/index.js';
+import { HttpLink } from '../link/http/index.js';
 
-import { QueryManager } from './QueryManager';
-import { ObservableQuery } from './ObservableQuery';
+import { QueryManager } from './QueryManager.js';
+import type { ObservableQuery } from './ObservableQuery.js';
 
-import {
+import type {
   ApolloQueryResult,
   DefaultContext,
   OperationVariables,
@@ -20,20 +22,21 @@ import {
   RefetchQueriesResult,
   InternalRefetchQueriesResult,
   RefetchQueriesInclude,
-} from './types';
+} from './types.js';
 
-import {
+import type {
   QueryOptions,
   WatchQueryOptions,
   MutationOptions,
   SubscriptionOptions,
   WatchQueryFetchPolicy,
-} from './watchQueryOptions';
+} from './watchQueryOptions.js';
 
+import type {
+  FragmentMatcher} from './LocalState.js';
 import {
-  LocalState,
-  FragmentMatcher,
-} from './LocalState';
+  LocalState
+} from './LocalState.js';
 
 export interface DefaultOptions {
   watchQuery?: Partial<WatchQueryOptions<any, any>>;
@@ -60,13 +63,14 @@ export type ApolloClientOptions<TCacheShape> = {
   fragmentMatcher?: FragmentMatcher;
   name?: string;
   version?: string;
+  documentTransform?: DocumentTransform
 };
 
 // Though mergeOptions now resides in @apollo/client/utilities, it was
 // previously declared and exported from this module, and then reexported from
 // @apollo/client/core. Since we need to preserve that API anyway, the easiest
 // solution is to reexport mergeOptions where it was previously declared (here).
-import { mergeOptions } from "../utilities";
+import { mergeOptions } from "../utilities/index.js";
 export { mergeOptions }
 
 /**
@@ -125,11 +129,20 @@ export class ApolloClient<TCacheShape> implements DataProxy {
    *                you are using.
    */
   constructor(options: ApolloClientOptions<TCacheShape>) {
+    if (!options.cache) {
+      throw newInvariantError(
+        "To initialize Apollo Client, you must specify a 'cache' property " +
+        "in the options object. \n" +
+        "For more information, please visit: https://go.apollo.dev/c/docs"
+      );
+    }
+
     const {
       uri,
       credentials,
       headers,
       cache,
+      documentTransform,
       ssrMode = false,
       ssrForceFetchDelay = 0,
       connectToDevTools =
@@ -141,7 +154,7 @@ export class ApolloClient<TCacheShape> implements DataProxy {
         __DEV__,
       queryDeduplication = true,
       defaultOptions,
-      assumeImmutableResults = false,
+      assumeImmutableResults = cache.assumeImmutableResults,
       resolvers,
       typeDefs,
       fragmentMatcher,
@@ -157,14 +170,6 @@ export class ApolloClient<TCacheShape> implements DataProxy {
         : ApolloLink.empty();
     }
 
-    if (!cache) {
-      throw new InvariantError(
-        "To initialize Apollo Client, you must specify a 'cache' property " +
-        "in the options object. \n" +
-        "For more information, please visit: https://go.apollo.dev/c/docs"
-      );
-    }
-
     this.link = link;
     this.cache = cache;
     this.disableNetworkFetches = ssrMode || ssrForceFetchDelay > 0;
@@ -214,7 +219,7 @@ export class ApolloClient<TCacheShape> implements DataProxy {
         if (url) {
           invariant.log(
             "Download the Apollo DevTools for a better development " +
-              "experience: " + url
+              "experience: %s", url
           );
         }
       }
@@ -233,6 +238,7 @@ export class ApolloClient<TCacheShape> implements DataProxy {
       cache: this.cache,
       link: this.link,
       defaultOptions: this.defaultOptions,
+      documentTransform,
       queryDeduplication,
       ssrMode,
       clientAwareness: {
@@ -256,6 +262,15 @@ export class ApolloClient<TCacheShape> implements DataProxy {
     });
   }
 
+  /**
+   * The `DocumentTransform` used to modify GraphQL documents before a request
+   * is made. If a custom `DocumentTransform` is not provided, this will be the
+   * default document transform.
+   */
+  get documentTransform() {
+    return this.queryManager.documentTransform;
+  }
+
   /**
    * Call this method to terminate any active client processes, making it safe
    * to dispose of this `ApolloClient` instance.
@@ -567,7 +582,7 @@ export class ApolloClient<TCacheShape> implements DataProxy {
     // result.queries and result.results instead, you shouldn't have to worry
     // about preventing uncaught rejections for the Promise.all result.
     result.catch(error => {
-      invariant.debug(`In client.refetchQueries, Promise.all promise rejected with error ${error}`);
+      invariant.debug(`In client.refetchQueries, Promise.all promise rejected with error %o`, error);
     });
 
     return result;
diff --git a/src/core/LocalState.ts b/src/core/LocalState.ts
--- a/src/core/LocalState.ts
+++ b/src/core/LocalState.ts
@@ -1,6 +1,6 @@
-import { invariant } from '../utilities/globals';
+import { invariant } from '../utilities/globals/index.js';
 
-import {
+import type {
   DocumentNode,
   OperationDefinitionNode,
   SelectionSetNode,
@@ -9,18 +9,20 @@ import {
   FragmentDefinitionNode,
   FieldNode,
   ASTNode,
-  visit,
-  BREAK,
-  isSelectionNode,
   DirectiveNode,
   FragmentSpreadNode,
-  ExecutableDefinitionNode,
+  ExecutableDefinitionNode} from 'graphql';
+import {
+  visit,
+  BREAK,
+  isSelectionNode
 } from 'graphql';
 
-import { ApolloCache } from '../cache';
-import {
+import type { ApolloCache } from '../cache/index.js';
+import type {
   FragmentMap,
-  StoreObject,
+  StoreObject} from '../utilities/index.js';
+import {
   argumentsObjectFromField,
   buildQueryFromSelectionSet,
   createFragmentMap,
@@ -34,11 +36,11 @@ import {
   removeClientSetsFromDocument,
   resultKeyNameFromField,
   shouldInclude,
-} from '../utilities';
-import { ApolloClient } from './ApolloClient';
-import { Resolvers, OperationVariables } from './types';
-import { FetchResult } from '../link/core';
-import { cacheSlot } from '../cache';
+} from '../utilities/index.js';
+import type { ApolloClient } from './ApolloClient.js';
+import type { Resolvers, OperationVariables } from './types.js';
+import type { FetchResult } from '../link/core/index.js';
+import { cacheSlot } from '../cache/index.js';
 
 export type Resolver = (
   rootValue?: any,
@@ -341,7 +343,7 @@ export class LocalState<TCacheShape> {
       } else {
         // This is a named fragment.
         fragment = fragmentMap[selection.name.value];
-        invariant(fragment, `No fragment named ${selection.name.value}`);
+        invariant(fragment, `No fragment named %s`, selection.name.value);
       }
 
       if (fragment && fragment.typeCondition) {
@@ -506,7 +508,7 @@ export class LocalState<TCacheShape> {
           },
           FragmentSpread(spread: FragmentSpreadNode, _, __, ___, ancestors) {
             const fragment = fragmentMap[spread.name.value];
-            invariant(fragment, `No fragment named ${spread.name.value}`);
+            invariant(fragment, `No fragment named %s`, spread.name.value);
 
             const fragmentSelections = collectByDefinition(fragment);
             if (fragmentSelections.size > 0) {
diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts
--- a/src/core/ObservableQuery.ts
+++ b/src/core/ObservableQuery.ts
@@ -1,38 +1,39 @@
-import { invariant } from '../utilities/globals';
-import { DocumentNode } from 'graphql';
+import { invariant } from '../utilities/globals/index.js';
+import type { DocumentNode } from 'graphql';
 import { equal } from '@wry/equality';
 
-import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus';
-import {
+import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus.js';
+import type {
   Concast,
+  Observer,
+  ObservableSubscription} from '../utilities/index.js';
+import {
   cloneDeep,
   compact,
   getOperationDefinition,
   Observable,
-  Observer,
-  ObservableSubscription,
   iterateObserversSafely,
-  isNonEmptyArray,
   fixObservableSubclass,
   getQueryDefinition,
-} from '../utilities';
-import { ApolloError } from '../errors';
-import { QueryManager } from './QueryManager';
-import {
+} from '../utilities/index.js';
+import type { ApolloError } from '../errors/index.js';
+import type { QueryManager } from './QueryManager.js';
+import type {
   ApolloQueryResult,
   OperationVariables,
   TypedDocumentNode,
-} from './types';
-import {
+} from './types.js';
+import type {
   WatchQueryOptions,
   FetchMoreQueryOptions,
   SubscribeToMoreOptions,
   NextFetchPolicyContext,
   WatchQueryFetchPolicy,
-} from './watchQueryOptions';
-import { QueryInfo } from './QueryInfo';
-import { MissingFieldError } from '../cache';
-import { MissingTree } from '../cache/core/types/common';
+} from './watchQueryOptions.js';
+import type { QueryInfo } from './QueryInfo.js';
+import type { MissingFieldError } from '../cache/index.js';
+import type { MissingTree } from '../cache/core/types/common.js';
+import { equalByQuery } from './equalByQuery.js';
 
 const {
   assign,
@@ -70,10 +71,12 @@ export class ObservableQuery<
   public readonly queryId: string;
   public readonly queryName?: string;
 
+  // The `query` computed property will always reflect the document transformed
+  // by the last run query. `this.options.query` will always reflect the raw
+  // untransformed query to ensure document transforms with runtime conditionals
+  // are run on the original document.
   public get query(): TypedDocumentNode<TData, TVariables> {
-    // This transform is heavily cached, so it should not be expensive to
-    // transform the same this.options.query document repeatedly.
-    return this.queryManager.transform(this.options.query).document;
+    return this.lastQuery || this.options.query;
   }
 
   // Computed shorthand for this.options.variables, preserved for
@@ -89,6 +92,7 @@ export class ObservableQuery<
 
   private waitForOwnResult: boolean;
   private last?: Last<TData, TVariables>;
+  private lastQuery?: DocumentNode;
 
   private queryInfo: QueryInfo;
 
@@ -248,7 +252,7 @@ export class ObservableQuery<
       // trust diff.result, since it was read from the cache without running
       // local resolvers (and it's too late to run resolvers now, since we must
       // return a result synchronously).
-      this.queryManager.transform(this.options.query).hasForcedResolvers
+      this.queryManager.getDocumentInfo(this.query).hasForcedResolvers
     ) {
       // Fall through. 
     } else if (this.waitForOwnResult) {
@@ -313,9 +317,22 @@ export class ObservableQuery<
     newResult: ApolloQueryResult<TData>,
     variables?: TVariables
   ) {
+    if (!this.last) {
+      return true;
+    }
+
+    const resultIsDifferent =
+      this.queryManager.getDocumentInfo(this.query).hasNonreactiveDirective
+        ? !equalByQuery(
+            this.query,
+            this.last.result,
+            newResult,
+            this.variables,
+          )
+        : !equal(this.last.result, newResult);
+
     return (
-      !this.last ||
-      !equal(this.last.result, newResult) ||
+      resultIsDifferent ||
       (variables && !equal(this.last.variables, variables))
     );
   }
@@ -380,12 +397,11 @@ export class ObservableQuery<
       const queryDef = getQueryDefinition(this.query);
       const vars = queryDef.variableDefinitions;
       if (!vars || !vars.some(v => v.variable.name.value === "variables")) {
-        invariant.warn(`Called refetch(${
-          JSON.stringify(variables)
-        }) for query ${
-          queryDef.name?.value || JSON.stringify(queryDef)
-        }, which does not declare a $variables variable.
-Did you mean to call refetch(variables) instead of refetch({ variables })?`);
+        invariant.warn(`Called refetch(%o) for query %o, which does not declare a $variables variable.
+Did you mean to call refetch(variables) instead of refetch({ variables })?`,
+          variables,
+          queryDef.name?.value || queryDef
+        );
       }
     }
 
@@ -416,7 +432,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
     const combinedOptions = {
       ...(fetchMoreOptions.query ? fetchMoreOptions : {
         ...this.options,
-        query: this.query,
+        query: this.options.query,
         ...fetchMoreOptions,
         variables: {
           ...this.options.variables,
@@ -431,8 +447,19 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
       fetchPolicy: "no-cache",
     } as WatchQueryOptions<TFetchVars, TFetchData>;
 
+    combinedOptions.query = this.transformDocument(combinedOptions.query);
+
     const qid = this.queryManager.generateQueryId();
 
+    // If a temporary query is passed to `fetchMore`, we don't want to store
+    // it as the last query result since it may be an optimized query for
+    // pagination. We will however run the transforms on the original document
+    // as well as the document passed in `fetchMoreOptions` to ensure the cache
+    // uses the most up-to-date document which may rely on runtime conditionals.
+    this.lastQuery = fetchMoreOptions.query
+      ? this.transformDocument(this.options.query)
+      : combinedOptions.query;
+
     // Simulate a loading result for the original query with
     // result.networkStatus === NetworkStatus.fetchMore.
     const { queryInfo } = this;
@@ -565,6 +592,13 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
     return this.reobserve(newOptions);
   }
 
+  public silentSetOptions(
+    newOptions: Partial<WatchQueryOptions<TVariables, TData>>,
+  ) {
+    const mergedOptions = compact(this.options, newOptions || {});
+    assign(this.options, mergedOptions);
+  }
+
   /**
    * Update the variables of this observable query, and fetch the new results
    * if they've changed. Most users should prefer `refetch` instead of
@@ -774,17 +808,18 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
     newResult: ApolloQueryResult<TData>,
     variables = this.variables,
   ) {
-    this.last = {
-      ...this.last,
+    let error: ApolloError | undefined = this.getLastError();
+    // Preserve this.last.error unless the variables have changed.
+    if (error && this.last && !equal(variables, this.last.variables)) {
+      error = void 0;
+    }
+    return this.last = {
       result: this.queryManager.assumeImmutableResults
         ? newResult
         : cloneDeep(newResult),
       variables,
+      ...(error ? { error } : null),
     };
-    if (!isNonEmptyArray(newResult.errors)) {
-      delete this.last.error;
-    }
-    return this.last;
   }
 
   public reobserveAsConcast(
@@ -816,6 +851,14 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
       ? mergedOptions
       : assign(this.options, mergedOptions);
 
+    // Don't update options.query with the transformed query to avoid
+    // overwriting this.options.query when we aren't using a disposable concast.
+    // We want to ensure we can re-run the custom document transforms the next
+    // time a request is made against the original query.
+    const query = this.transformDocument(options.query);
+
+    this.lastQuery = query;
+
     if (!useDisposableConcast) {
       // We can skip calling updatePolling if we're not changing this.options.
       this.updatePolling();
@@ -839,15 +882,19 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
       }
     }
 
-    this.waitForOwnResult &&= skipCacheDataFor(options.fetchPolicy);
+    // If the transform doesn't change the document, leave `options` alone and
+    // use the original object.
+    const fetchOptions = query === options.query ? options : { ...options, query };
+
+    this.waitForOwnResult &&= skipCacheDataFor(fetchOptions.fetchPolicy);
     const finishWaitingForOwnResult = () => {
       if (this.concast === concast) {
         this.waitForOwnResult = false;
       }
     };
     
-    const variables = options.variables && { ...options.variables };
-    const { concast, fromLink } = this.fetch(options, newNetworkStatus);
+    const variables = fetchOptions.variables && { ...fetchOptions.variables };
+    const { concast, fromLink } = this.fetch(fetchOptions, newNetworkStatus);
     const observer: Observer<ApolloQueryResult<TData>> = {
       next: result => {
         finishWaitingForOwnResult();
@@ -859,7 +906,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
       },
     };
 
-    if (!useDisposableConcast && fromLink) {
+    if (!useDisposableConcast && (fromLink || !this.concast)) {
       // We use the {add,remove}Observer methods directly to avoid wrapping
       // observer with an unnecessary SubscriptionObserver object.
       if (this.concast && this.observer) {
@@ -900,12 +947,16 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
     variables: TVariables | undefined,
   ) {
     const lastError = this.getLastError();
-    if (lastError || this.isDifferentFromLastResult(result, variables)) {
-      if (lastError || !result.partial || this.options.returnPartialData) {
-        this.updateLastResult(result, variables);
-      }
-
-      iterateObserversSafely(this.observers, 'next', result);
+    const isDifferent = this.isDifferentFromLastResult(result, variables);
+    // Update the last result even when isDifferentFromLastResult returns false,
+    // because the query may be using the @nonreactive directive, and we want to
+    // save the the latest version of any nonreactive subtrees (in case
+    // getCurrentResult is called), even though we skip broadcasting changes.
+    if (lastError || !result.partial || this.options.returnPartialData) {
+      this.updateLastResult(result, variables);
+    }
+    if (lastError || isDifferent) {
+      iterateObserversSafely(this.observers, "next", result);
     }
   }
 
@@ -948,6 +999,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`);
     this.observers.clear();
     this.isTornDown = true;
   }
+
+  private transformDocument(document: DocumentNode) {
+    return this.queryManager.transform(document);
+  }
 }
 
 // Necessary because the ObservableQuery constructor has a different
@@ -1000,9 +1055,7 @@ export function logMissingFieldErrors(
   missing: MissingFieldError[] | MissingTree | undefined,
 ) {
   if (__DEV__ && missing) {
-    invariant.debug(`Missing cache result fields: ${
-      JSON.stringify(missing)
-    }`, missing);
+    invariant.debug(`Missing cache result fields: %o`, missing);
   }
 }
 
diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts
--- a/src/core/QueryInfo.ts
+++ b/src/core/QueryInfo.ts
@@ -1,25 +1,27 @@
-import { DocumentNode, GraphQLError } from 'graphql';
+import type { DocumentNode, GraphQLError } from 'graphql';
 import { equal } from "@wry/equality";
 
-import { Cache, ApolloCache } from '../cache';
-import { DeepMerger } from "../utilities"
-import { mergeIncrementalData } from '../utilities/common/incrementalResult';
-import { WatchQueryOptions, ErrorPolicy } from './watchQueryOptions';
-import { ObservableQuery, reobserveCacheFirst } from './ObservableQuery';
-import { QueryListener } from './types';
-import { FetchResult } from '../link/core';
+import type { Cache, ApolloCache } from '../cache/index.js';
+import { DeepMerger } from "../utilities/index.js"
+import { mergeIncrementalData } from '../utilities/index.js';
+import type { WatchQueryOptions, ErrorPolicy } from './watchQueryOptions.js';
+import type { ObservableQuery} from './ObservableQuery.js';
+import { reobserveCacheFirst } from './ObservableQuery.js';
+import type { QueryListener, MethodKeys } from './types.js';
+import type { FetchResult } from '../link/core/index.js';
+import type {
+  ObservableSubscription} from '../utilities/index.js';
 import {
-  ObservableSubscription,
   isNonEmptyArray,
   graphQLResultHasError,
   canUseWeakMap,
-} from '../utilities';
+} from '../utilities/index.js';
 import {
   NetworkStatus,
   isNetworkRequestInFlight,
-} from './networkStatus';
-import { ApolloError } from '../errors';
-import { QueryManager } from './QueryManager';
+} from './networkStatus.js';
+import type { ApolloError } from '../errors/index.js';
+import type { QueryManager } from './QueryManager.js';
 
 export type QueryStoreValue = Pick<QueryInfo,
   | "variables"
@@ -40,7 +42,7 @@ const destructiveMethodCounts = new (
 
 function wrapDestructiveCacheMethod(
   cache: ApolloCache<any>,
-  methodName: keyof ApolloCache<any>,
+  methodName: MethodKeys<ApolloCache<any>>,
 ) {
   const original = cache[methodName];
   if (typeof original === "function") {
diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts
--- a/src/core/QueryManager.ts
+++ b/src/core/QueryManager.ts
@@ -1,17 +1,24 @@
-import { invariant, InvariantError } from '../utilities/globals';
+import { invariant, newInvariantError } from '../utilities/globals/index.js';
 
-import { DocumentNode } from 'graphql';
+import type { DocumentNode } from 'graphql';
 // TODO(brian): A hack until this issue is resolved (https://github.com/graphql/graphql-js/issues/3356)
 type OperationTypeNode = any;
 import { equal } from '@wry/equality';
 
-import { ApolloLink, execute, FetchResult } from '../link/core';
+import type { ApolloLink, FetchResult } from '../link/core/index.js';
+import { execute } from '../link/core/index.js';
 import {
+  hasDirectives,
   isExecutionPatchIncrementalResult,
   isExecutionPatchResult,
-} from '../utilities/common/incrementalResult';
-import { Cache, ApolloCache, canonicalStringify } from '../cache';
+  removeDirectivesFromDocument,
+} from '../utilities/index.js';
+import type { Cache, ApolloCache} from '../cache/index.js';
+import { canonicalStringify } from '../cache/index.js';
 
+import type {
+  ObservableSubscription,
+  ConcastSourcesArray} from '../utilities/index.js';
 import {
   getDefaultValues,
   getOperationDefinition,
@@ -19,31 +26,29 @@ import {
   hasClientExports,
   graphQLResultHasError,
   getGraphQLErrorsFromResult,
-  removeConnectionDirectiveFromDocument,
   canUseWeakMap,
-  ObservableSubscription,
   Observable,
   asyncMap,
   isNonEmptyArray,
   Concast,
-  ConcastSourcesArray,
   makeUniqueId,
   isDocumentNode,
   isNonNullObject,
-} from '../utilities';
-import { mergeIncrementalData } from '../utilities/common/incrementalResult';
-import { ApolloError, isApolloError, graphQLResultHasProtocolErrors } from '../errors';
-import {
+  DocumentTransform,
+} from '../utilities/index.js';
+import { mergeIncrementalData } from '../utilities/common/incrementalResult.js';
+import { ApolloError, isApolloError, graphQLResultHasProtocolErrors } from '../errors/index.js';
+import type {
   QueryOptions,
   WatchQueryOptions,
   SubscriptionOptions,
   MutationOptions,
   ErrorPolicy,
   MutationFetchPolicy,
-} from './watchQueryOptions';
-import { ObservableQuery, logMissingFieldErrors } from './ObservableQuery';
-import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus';
-import {
+} from './watchQueryOptions.js';
+import { ObservableQuery, logMissingFieldErrors } from './ObservableQuery.js';
+import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus.js';
+import type {
   ApolloQueryResult,
   OperationVariables,
   MutationUpdaterFunction,
@@ -52,16 +57,19 @@ import {
   InternalRefetchQueriesOptions,
   InternalRefetchQueriesResult,
   InternalRefetchQueriesMap,
-} from './types';
-import { LocalState } from './LocalState';
+} from './types.js';
+import { LocalState } from './LocalState.js';
 
+import type {
+  QueryStoreValue} from './QueryInfo.js';
 import {
   QueryInfo,
-  QueryStoreValue,
   shouldWriteResult,
   CacheWriteBehavior,
-} from './QueryInfo';
-import { PROTOCOL_ERRORS_SYMBOL, ApolloErrorOptions } from '../errors';
+} from './QueryInfo.js';
+import type { ApolloErrorOptions } from '../errors/index.js';
+import { PROTOCOL_ERRORS_SYMBOL } from '../errors/index.js';
+import { print } from '../utilities/index.js';
 
 const { hasOwnProperty } = Object.prototype;
 
@@ -75,16 +83,16 @@ interface MutationStoreValue {
 type UpdateQueries<TData> = MutationOptions<TData, any, any>["updateQueries"];
 
 interface TransformCacheEntry {
-  document: DocumentNode;
   hasClientExports: boolean;
   hasForcedResolvers: boolean;
+  hasNonreactiveDirective: boolean;
   clientQuery: DocumentNode | null;
   serverQuery: DocumentNode | null;
   defaultVars: OperationVariables;
   asQuery: DocumentNode;
 }
 
-type DefaultOptions = import("./ApolloClient").DefaultOptions;
+type DefaultOptions = import("./ApolloClient.js").DefaultOptions;
 
 export class QueryManager<TStore> {
   public cache: ApolloCache<TStore>;
@@ -92,6 +100,7 @@ export class QueryManager<TStore> {
   public defaultOptions: DefaultOptions;
 
   public readonly assumeImmutableResults: boolean;
+  public readonly documentTransform: DocumentTransform;
   public readonly ssrMode: boolean;
 
   private queryDeduplication: boolean;
@@ -109,22 +118,26 @@ export class QueryManager<TStore> {
 
   // Maps from queryId strings to Promise rejection functions for
   // currently active queries and fetches.
-  private fetchCancelFns = new Map<string, (error: any) => any>();
+  // Use protected instead of private field so
+  // @apollo/experimental-nextjs-app-support can access type info.
+  protected fetchCancelFns = new Map<string, (error: any) => any>();
 
   constructor({
     cache,
     link,
     defaultOptions,
+    documentTransform,
     queryDeduplication = false,
     onBroadcast,
     ssrMode = false,
     clientAwareness = {},
     localState,
-    assumeImmutableResults,
+    assumeImmutableResults = !!cache.assumeImmutableResults,
   }: {
     cache: ApolloCache<TStore>;
     link: ApolloLink;
     defaultOptions?: DefaultOptions;
+    documentTransform?: DocumentTransform;
     queryDeduplication?: boolean;
     onBroadcast?: () => void;
     ssrMode?: boolean;
@@ -132,6 +145,12 @@ export class QueryManager<TStore> {
     localState?: LocalState<TStore>;
     assumeImmutableResults?: boolean;
   }) {
+    const defaultDocumentTransform = new DocumentTransform(
+      (document) => this.cache.transformDocument(document),
+      // Allow the apollo cache to manage its own transform caches
+      { cache: false }
+    );
+
     this.cache = cache;
     this.link = link;
     this.defaultOptions = defaultOptions || Object.create(null);
@@ -139,7 +158,17 @@ export class QueryManager<TStore> {
     this.clientAwareness = clientAwareness;
     this.localState = localState || new LocalState({ cache });
     this.ssrMode = ssrMode;
-    this.assumeImmutableResults = !!assumeImmutableResults;
+    this.assumeImmutableResults = assumeImmutableResults;
+    this.documentTransform = documentTransform
+      ? defaultDocumentTransform
+          .concat(documentTransform)
+          // The custom document transform may add new fragment spreads or new
+          // field selections, so we want to give the cache a chance to run
+          // again. For example, the InMemoryCache adds __typename to field
+          // selections and fragments from the fragment registry.
+          .concat(defaultDocumentTransform)
+      : defaultDocumentTransform
+
     if ((this.onBroadcast = onBroadcast)) {
       this.mutationStore = Object.create(null);
     }
@@ -155,7 +184,7 @@ export class QueryManager<TStore> {
     });
 
     this.cancelPendingFetches(
-      new InvariantError('QueryManager stopped while query was in flight'),
+      newInvariantError('QueryManager stopped while query was in flight'),
     );
   }
 
@@ -196,11 +225,8 @@ export class QueryManager<TStore> {
 
     const mutationId = this.generateMutationId();
 
-    const {
-      document,
-      hasClientExports,
-    } = this.transform(mutation);
-    mutation = this.cache.transformForLink(document);
+    mutation = this.cache.transformForLink(this.transform(mutation));
+    const { hasClientExports } = this.getDocumentInfo(mutation);
 
     variables = this.getVariables(mutation, variables) as TVariables;
     if (hasClientExports) {
@@ -376,7 +402,7 @@ export class QueryManager<TStore> {
           // The cache complains if passed a mutation where it expects a
           // query, so we transform mutations and subscriptions to queries
           // (only once, thanks to this.transformCache).
-          query: this.transform(mutation.document).asQuery,
+          query: this.getDocumentInfo(mutation.document).asQuery,
           variables: mutation.variables,
           optimistic: false,
           returnPartialData: true,
@@ -474,7 +500,7 @@ export class QueryManager<TStore> {
                 // The cache complains if passed a mutation where it expects a
                 // query, so we transform mutations and subscriptions to queries
                 // (only once, thanks to this.transformCache).
-                query: this.transform(mutation.document).asQuery,
+                query: this.getDocumentInfo(mutation.document).asQuery,
                 variables: mutation.variables,
                 optimistic: false,
                 returnPartialData: true,
@@ -577,11 +603,11 @@ export class QueryManager<TStore> {
     options: WatchQueryOptions<TVars, TData>,
     networkStatus?: NetworkStatus,
   ): Promise<ApolloQueryResult<TData>> {
-    return this.fetchQueryObservable<TData, TVars>(
+    return this.fetchConcastWithInfo(
       queryId,
       options,
       networkStatus,
-    ).promise;
+    ).concast.promise;
   }
 
   public getQueryStore() {
@@ -605,35 +631,41 @@ export class QueryManager<TStore> {
     }
   }
 
+  public transform(document: DocumentNode) {
+    return this.documentTransform.transformDocument(document);
+  }
+
   private transformCache = new (
     canUseWeakMap ? WeakMap : Map
   )<DocumentNode, TransformCacheEntry>();
 
-  public transform(document: DocumentNode) {
+  public getDocumentInfo(document: DocumentNode) {
     const { transformCache } = this;
 
     if (!transformCache.has(document)) {
-      const transformed = this.cache.transformDocument(document);
-      const noConnection = removeConnectionDirectiveFromDocument(transformed);
-      const clientQuery = this.localState.clientQuery(transformed);
-      const serverQuery = noConnection && this.localState.serverQuery(noConnection);
-
       const cacheEntry: TransformCacheEntry = {
-        document: transformed,
-        // TODO These two calls (hasClientExports and shouldForceResolvers)
-        // could probably be merged into a single traversal.
-        hasClientExports: hasClientExports(transformed),
-        hasForcedResolvers: this.localState.shouldForceResolvers(transformed),
-        clientQuery,
-        serverQuery,
+        // TODO These three calls (hasClientExports, shouldForceResolvers, and
+        // usesNonreactiveDirective) are performing independent full traversals
+        // of the transformed document. We should consider merging these
+        // traversals into a single pass in the future, though the work is
+        // cached after the first time.
+        hasClientExports: hasClientExports(document),
+        hasForcedResolvers: this.localState.shouldForceResolvers(document),
+        hasNonreactiveDirective: hasDirectives(['nonreactive'], document),
+        clientQuery: this.localState.clientQuery(document),
+        serverQuery: removeDirectivesFromDocument([
+          { name: 'client', remove: true },
+          { name: 'connection' },
+          { name: 'nonreactive' },
+        ], document),
         defaultVars: getDefaultValues(
-          getOperationDefinition(transformed)
+          getOperationDefinition(document)
         ) as OperationVariables,
         // Transform any mutation or subscription operations to query operations
         // so we can read/write them from/to the cache.
         asQuery: {
-          ...transformed,
-          definitions: transformed.definitions.map(def => {
+          ...document,
+          definitions: document.definitions.map(def => {
             if (def.kind === "OperationDefinition" &&
                 def.operation !== "query") {
               return { ...def, operation: "query" as OperationTypeNode };
@@ -643,18 +675,7 @@ export class QueryManager<TStore> {
         }
       };
 
-      const add = (doc: DocumentNode | null) => {
-        if (doc && !transformCache.has(doc)) {
-          transformCache.set(doc, cacheEntry);
-        }
-      }
-      // Add cacheEntry to the transformCache using several different keys,
-      // since any one of these documents could end up getting passed to the
-      // transform method again in the future.
-      add(document);
-      add(transformed);
-      add(clientQuery);
-      add(serverQuery);
+      transformCache.set(document, cacheEntry);
     }
 
     return transformCache.get(document)!;
@@ -665,7 +686,7 @@ export class QueryManager<TStore> {
     variables?: TVariables,
   ): OperationVariables {
     return {
-      ...this.transform(document).defaultVars,
+      ...this.getDocumentInfo(document).defaultVars,
       ...variables,
     };
   }
@@ -673,11 +694,15 @@ export class QueryManager<TStore> {
   public watchQuery<T, TVariables extends OperationVariables = OperationVariables>(
     options: WatchQueryOptions<TVariables, T>,
   ): ObservableQuery<T, TVariables> {
+    const query = this.transform(options.query);
+
     // assign variable default values if supplied
+    // NOTE: We don't modify options.query here with the transformed query to
+    // ensure observable.options.query is set to the raw untransformed query.
     options = {
       ...options,
       variables: this.getVariables(
-        options.query,
+        query,
         options.variables,
       ) as TVariables,
     };
@@ -692,11 +717,14 @@ export class QueryManager<TStore> {
       queryInfo,
       options,
     });
+    observable['lastQuery'] = query;
 
     this.queries.set(observable.queryId, queryInfo);
 
+    // We give queryInfo the transformed query to ensure the first cache diff
+    // uses the transformed query instead of the raw query
     queryInfo.init({
-      document: observable.query,
+      document: query,
       observableQuery: observable,
       variables: observable.variables,
     });
@@ -731,7 +759,7 @@ export class QueryManager<TStore> {
 
     return this.fetchQuery<TData, TVars>(
       queryId,
-      options,
+      { ...options, query: this.transform(options.query) },
     ).finally(() => this.stopQuery(queryId));
   }
 
@@ -768,7 +796,7 @@ export class QueryManager<TStore> {
     // depend on values that previously existed in the data portion of the
     // store. So, we cancel the promises and observers that we have issued
     // so far and not yet resolved (in the case of queries).
-    this.cancelPendingFetches(new InvariantError(
+    this.cancelPendingFetches(newInvariantError(
       'Store reset while query was in flight (not completed in link chain)',
     ));
 
@@ -802,7 +830,7 @@ export class QueryManager<TStore> {
         if (typeof desc === "string") {
           queryNamesAndDocs.set(desc, false);
         } else if (isDocumentNode(desc)) {
-          queryNamesAndDocs.set(this.transform(desc).document, false);
+          queryNamesAndDocs.set(this.transform(desc), false);
         } else if (isNonNullObject(desc) && desc.query) {
           legacyQueryOptions.add(desc);
         }
@@ -867,11 +895,7 @@ export class QueryManager<TStore> {
     if (__DEV__ && queryNamesAndDocs.size) {
       queryNamesAndDocs.forEach((included, nameOrDoc) => {
         if (!included) {
-          invariant.warn(`Unknown query ${
-            typeof nameOrDoc === "string" ? "named " : ""
-          }${
-            JSON.stringify(nameOrDoc, null, 2)
-          } requested in refetchQueries options.include array`);
+          invariant.warn(typeof nameOrDoc === "string" ? `Unknown query named "%s" requested in refetchQueries options.include array` : `Unknown query %s requested in refetchQueries options.include array`, nameOrDoc);
         }
       });
     }
@@ -913,7 +937,7 @@ export class QueryManager<TStore> {
     variables,
     context = {},
   }: SubscriptionOptions): Observable<FetchResult<T>> {
-    query = this.transform(query).document;
+    query = this.transform(query);
     variables = this.getVariables(query, variables);
 
     const makeObservable = (variables: OperationVariables) =>
@@ -953,7 +977,7 @@ export class QueryManager<TStore> {
         return result;
       });
 
-    if (this.transform(query).hasClientExports) {
+    if (this.getDocumentInfo(query).hasClientExports) {
       const observablePromise = this.localState.addExportedVariables(
         query,
         variables,
@@ -1005,8 +1029,10 @@ export class QueryManager<TStore> {
     return this.localState;
   }
 
-  private inFlightLinkObservables = new Map<
-    DocumentNode,
+  // Use protected instead of private field so
+  // @apollo/experimental-nextjs-app-support can access type info.
+  protected inFlightLinkObservables = new Map<
+    string,
     Map<string, Observable<FetchResult>>
   >();
 
@@ -1021,7 +1047,7 @@ export class QueryManager<TStore> {
   ): Observable<FetchResult<T>> {
     let observable: Observable<FetchResult<T>>;
 
-    const { serverQuery } = this.transform(query);
+    const { serverQuery, clientQuery } = this.getDocumentInfo(query);
     if (serverQuery) {
       const { inFlightLinkObservables, link } = this;
 
@@ -1038,8 +1064,9 @@ export class QueryManager<TStore> {
       context = operation.context;
 
       if (deduplication) {
-        const byVariables = inFlightLinkObservables.get(serverQuery) || new Map();
-        inFlightLinkObservables.set(serverQuery, byVariables);
+        const printedServerQuery = print(serverQuery);
+        const byVariables = inFlightLinkObservables.get(printedServerQuery) || new Map();
+        inFlightLinkObservables.set(printedServerQuery, byVariables);
 
         const varJson = canonicalStringify(variables);
         observable = byVariables.get(varJson);
@@ -1054,7 +1081,7 @@ export class QueryManager<TStore> {
           concast.beforeNext(() => {
             if (byVariables.delete(varJson) &&
                 byVariables.size < 1) {
-              inFlightLinkObservables.delete(serverQuery);
+              inFlightLinkObservables.delete(printedServerQuery);
             }
           });
         }
@@ -1071,7 +1098,6 @@ export class QueryManager<TStore> {
       context = this.prepareContext(context);
     }
 
-    const { clientQuery } = this.transform(query);
     if (clientQuery) {
       observable = asyncMap(observable, result => {
         return this.localState.runResolvers({
@@ -1090,6 +1116,7 @@ export class QueryManager<TStore> {
     queryInfo: QueryInfo,
     cacheWriteBehavior: CacheWriteBehavior,
     options: Pick<WatchQueryOptions<TVars, TData>,
+      | "query"
       | "variables"
       | "context"
       | "fetchPolicy"
@@ -1100,10 +1127,7 @@ export class QueryManager<TStore> {
     // Performing transformForLink here gives this.cache a chance to fill in
     // missing fragment definitions (for example) before sending this document
     // through the link chain.
-    const linkDocument = this.cache.transformForLink(
-      // Use same document originally produced by this.cache.transformDocument.
-      this.transform(queryInfo.document!).document
-    );
+    const linkDocument = this.cache.transformForLink(options.query);
 
     return asyncMap(
       this.getObservableFromLink(
@@ -1161,21 +1185,6 @@ export class QueryManager<TStore> {
     );
   }
 
-  public fetchQueryObservable<TData, TVars extends OperationVariables>(
-    queryId: string,
-    options: WatchQueryOptions<TVars, TData>,
-    // The initial networkStatus for this fetch, most often
-    // NetworkStatus.loading, but also possibly fetchMore, poll, refetch,
-    // or setVariables.
-    networkStatus?: NetworkStatus,
-  ): Concast<ApolloQueryResult<TData>> {
-    return this.fetchConcastWithInfo(
-      queryId,
-      options,
-      networkStatus,
-    ).concast;
-  }
-
   private fetchConcastWithInfo<TData, TVars extends OperationVariables>(
     queryId: string,
     options: WatchQueryOptions<TVars, TData>,
@@ -1184,7 +1193,7 @@ export class QueryManager<TStore> {
     // or setVariables.
     networkStatus = NetworkStatus.loading
   ): ConcastAndInfo<TData> {
-    const query = this.transform(options.query).document;
+    const { query } = options;
     const variables = this.getVariables(query, options.variables) as TVars;
     const queryInfo = this.getQuery(queryId);
 
@@ -1252,7 +1261,7 @@ export class QueryManager<TStore> {
     // since the timing of result delivery is (unfortunately) important
     // for backwards compatibility. TODO This code could be simpler if
     // we deprecated and removed LocalState.
-    if (this.transform(normalized.query).hasClientExports) {
+    if (this.getDocumentInfo(normalized.query).hasClientExports) {
       concast = new Concast(
         this.localState
           .addExportedVariables(normalized.query, normalized.variables, normalized.context)
@@ -1453,7 +1462,7 @@ export class QueryManager<TStore> {
     const oldNetworkStatus = queryInfo.networkStatus;
 
     queryInfo.init({
-      document: this.transform(query).document,
+      document: query,
       variables,
       networkStatus,
     });
@@ -1479,7 +1488,7 @@ export class QueryManager<TStore> {
         ...(diff.complete ? null : { partial: true }),
       } as ApolloQueryResult<TData>);
 
-      if (data && this.transform(query).hasForcedResolvers) {
+      if (data && this.getDocumentInfo(query).hasForcedResolvers) {
         return this.localState.runResolvers({
           document: query,
           remoteResult: { data },
@@ -1517,6 +1526,7 @@ export class QueryManager<TStore> {
       queryInfo,
       cacheWriteBehavior,
       {
+        query,
         variables,
         context,
         fetchPolicy,
diff --git a/src/core/equalByQuery.ts b/src/core/equalByQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/core/equalByQuery.ts
@@ -0,0 +1,156 @@
+import equal from "@wry/equality";
+
+import type {
+  DirectiveNode,
+  DocumentNode,
+  FieldNode,
+  FragmentDefinitionNode,
+  FragmentSpreadNode,
+  InlineFragmentNode,
+  SelectionNode,
+  SelectionSetNode,
+} from "graphql";
+
+import type { ApolloQueryResult, OperationVariables } from "./types.js";
+
+import type { FragmentMap } from "../utilities/index.js";
+import {
+  createFragmentMap,
+  getFragmentDefinitions,
+  getFragmentFromSelection,
+  getMainDefinition,
+  isField,
+  resultKeyNameFromField,
+  shouldInclude,
+} from "../utilities/index.js";
+
+// Returns true if aResult and bResult are deeply equal according to the fields
+// selected by the given query, ignoring any fields marked as @nonreactive.
+export function equalByQuery(
+  query: DocumentNode,
+  { data: aData, ...aRest }: Partial<ApolloQueryResult<unknown>>,
+  { data: bData, ...bRest }: Partial<ApolloQueryResult<unknown>>,
+  variables?: OperationVariables
+): boolean {
+  return (
+    equal(aRest, bRest) &&
+    equalBySelectionSet(getMainDefinition(query).selectionSet, aData, bData, {
+      fragmentMap: createFragmentMap(getFragmentDefinitions(query)),
+      variables,
+    })
+  );
+}
+
+// Encapsulates the information used by equalBySelectionSet that does not change
+// during the recursion.
+interface CompareContext<TVariables> {
+  fragmentMap: FragmentMap;
+  variables: TVariables | undefined;
+}
+
+function equalBySelectionSet(
+  selectionSet: SelectionSetNode,
+  aResult: any,
+  bResult: any,
+  context: CompareContext<OperationVariables>
+): boolean {
+  if (aResult === bResult) {
+    return true;
+  }
+
+  const seenSelections = new Set<SelectionNode>();
+
+  // Returning true from this Array.prototype.every callback function skips the
+  // current field/subtree. Returning false aborts the entire traversal
+  // immediately, causing equalBySelectionSet to return false.
+  return selectionSet.selections.every((selection) => {
+    // Avoid re-processing the same selection at the same level of recursion, in
+    // case the same field gets included via multiple indirect fragment spreads.
+    if (seenSelections.has(selection)) return true;
+    seenSelections.add(selection);
+
+    // Ignore @skip(if: true) and @include(if: false) fields.
+    if (!shouldInclude(selection, context.variables)) return true;
+
+    // If the field or (named) fragment spread has a @nonreactive directive on
+    // it, we don't care if it's different, so we pretend it's the same.
+    if (selectionHasNonreactiveDirective(selection)) return true;
+
+    if (isField(selection)) {
+      const resultKey = resultKeyNameFromField(selection);
+      const aResultChild = aResult && aResult[resultKey];
+      const bResultChild = bResult && bResult[resultKey];
+      const childSelectionSet = selection.selectionSet;
+
+      if (!childSelectionSet) {
+        // These are scalar values, so we can compare them with deep equal
+        // without redoing the main recursive work.
+        return equal(aResultChild, bResultChild);
+      }
+
+      const aChildIsArray = Array.isArray(aResultChild);
+      const bChildIsArray = Array.isArray(bResultChild);
+      if (aChildIsArray !== bChildIsArray) return false;
+      if (aChildIsArray && bChildIsArray) {
+        const length = aResultChild.length;
+        if (bResultChild.length !== length) {
+          return false;
+        }
+        for (let i = 0; i < length; ++i) {
+          if (
+            !equalBySelectionSet(
+              childSelectionSet,
+              aResultChild[i],
+              bResultChild[i],
+              context
+            )
+          ) {
+            return false;
+          }
+        }
+        return true;
+      }
+
+      return equalBySelectionSet(
+        childSelectionSet,
+        aResultChild,
+        bResultChild,
+        context
+      );
+    } else {
+      const fragment = getFragmentFromSelection(selection, context.fragmentMap);
+      if (fragment) {
+        // The fragment might === selection if it's an inline fragment, but
+        // could be !== if it's a named fragment ...spread.
+        if (selectionHasNonreactiveDirective(fragment)) return true;
+
+        return equalBySelectionSet(
+          fragment.selectionSet,
+          // Notice that we reuse the same aResult and bResult values here,
+          // since the fragment ...spread does not specify a field name, but
+          // consists of multiple fields (within the fragment's selection set)
+          // that should be applied to the current result value(s).
+          aResult,
+          bResult,
+          context
+        );
+      }
+    }
+  });
+}
+
+function selectionHasNonreactiveDirective(
+  selection:
+    | FieldNode
+    | InlineFragmentNode
+    | FragmentSpreadNode
+    | FragmentDefinitionNode
+): boolean {
+  return (
+    !!selection.directives && selection.directives.some(directiveIsNonreactive)
+  );
+}
+
+function directiveIsNonreactive(dir: DirectiveNode): boolean {
+  return dir.name.value === "nonreactive";
+}
diff --git a/src/core/index.ts b/src/core/index.ts
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,19 +1,13 @@
 /* Core */
 
-import { DEV } from '../utilities/globals';
-
-export {
-  ApolloClient,
-  ApolloClientOptions,
-  DefaultOptions,
-  mergeOptions,
-} from './ApolloClient';
-export {
-  ObservableQuery,
+export type { ApolloClientOptions, DefaultOptions } from "./ApolloClient.js";
+export { ApolloClient, mergeOptions } from "./ApolloClient.js";
+export type {
   FetchMoreOptions,
   UpdateQueryOptions,
-} from './ObservableQuery';
-export {
+} from "./ObservableQuery.js";
+export { ObservableQuery } from "./ObservableQuery.js";
+export type {
   QueryOptions,
   WatchQueryOptions,
   MutationOptions,
@@ -23,30 +17,20 @@ export {
   ErrorPolicy,
   FetchMoreQueryOptions,
   SubscribeToMoreOptions,
-} from './watchQueryOptions';
-export { NetworkStatus } from './networkStatus';
-export * from './types';
-export {
-  Resolver,
-  FragmentMatcher,
-} from './LocalState';
-export { isApolloError, ApolloError } from '../errors';
-
+} from "./watchQueryOptions.js";
+export { NetworkStatus, isNetworkRequestSettled } from "./networkStatus.js";
+export * from "./types.js";
+export type { Resolver, FragmentMatcher } from "./LocalState.js";
+export { isApolloError, ApolloError } from "../errors/index.js";
 /* Cache */
 
-export {
-  // All the exports (types and values) from ../cache, minus cacheSlot,
+export type {
+  // All the exports (types) from ../cache, minus cacheSlot,
   // which we want to keep semi-private.
-  Cache,
-  ApolloCache,
   Transaction,
   DataProxy,
-  InMemoryCache,
   InMemoryCacheConfig,
-  MissingFieldError,
-  defaultDataIdFromObject,
   ReactiveVar,
-  makeVar,
   TypePolicies,
   TypePolicy,
   FieldPolicy,
@@ -54,33 +38,45 @@ export {
   FieldMergeFunction,
   FieldFunctionOptions,
   PossibleTypesMap,
-} from '../cache';
+} from "../cache/index.js";
+export {
+  Cache,
+  ApolloCache,
+  InMemoryCache,
+  MissingFieldError,
+  defaultDataIdFromObject,
+  makeVar,
+} from "../cache/index.js";
 
-export * from '../cache/inmemory/types';
+export * from "../cache/inmemory/types.js";
 
 /* Link */
 
-export * from '../link/core';
-export * from '../link/http';
+export * from "../link/core/index.js";
+export * from "../link/http/index.js";
+export type { ServerError } from "../link/utils/index.js";
 export {
   fromError,
   toPromise,
   fromPromise,
-  ServerError,
   throwServerError,
-} from '../link/utils';
+} from "../link/utils/index.js";
 
 /* Utilities */
 
-export {
-  Observable,
+export type {
+  DocumentTransformCacheKey,
   Observer,
   ObservableSubscription,
   Reference,
+  StoreObject,
+} from "../utilities/index.js";
+export {
+  DocumentTransform,
+  Observable,
   isReference,
   makeReference,
-  StoreObject,
-} from '../utilities';
+} from "../utilities/index.js";
 
 /* Supporting */
 
@@ -89,8 +85,8 @@ export {
 // "warn", "error", or "silent" to setVerbosity ("log" is the default).
 // Note that all invariant.* logging is hidden in production.
 import { setVerbosity } from "ts-invariant";
-export { setVerbosity as setLogVerbosity }
-setVerbosity(DEV ? "log" : "silent");
+export { setVerbosity as setLogVerbosity };
+setVerbosity(__DEV__ ? "log" : "silent");
 
 // Note that importing `gql` by itself, then destructuring
 // additional properties separately before exporting, is intentional.
@@ -110,4 +106,4 @@ export {
   disableFragmentWarnings,
   enableExperimentalFragmentVariables,
   disableExperimentalFragmentVariables,
-} from 'graphql-tag';
+} from "graphql-tag";
diff --git a/src/core/networkStatus.ts b/src/core/networkStatus.ts
--- a/src/core/networkStatus.ts
+++ b/src/core/networkStatus.ts
@@ -54,3 +54,13 @@ export function isNetworkRequestInFlight(
 ): boolean {
   return networkStatus ? networkStatus < 7 : false;
 }
+
+/**
+ * Returns true if the network request is in ready or error state according to a given network
+ * status.
+ */
+export function isNetworkRequestSettled(
+  networkStatus?: NetworkStatus,
+): boolean {
+  return networkStatus === 7 || networkStatus === 8;
+}
diff --git a/src/core/types.ts b/src/core/types.ts
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,17 +1,21 @@
-import { DocumentNode, GraphQLError } from 'graphql';
-
-import { ApolloCache } from '../cache';
-import { FetchResult } from '../link/core';
-import { ApolloError } from '../errors';
-import { QueryInfo } from './QueryInfo';
-import { NetworkStatus } from './networkStatus';
-import { Resolver } from './LocalState';
-import { ObservableQuery } from './ObservableQuery';
-import { QueryOptions } from './watchQueryOptions';
-import { Cache } from '../cache';
-import { IsStrictlyAny } from '../utilities';
-
-export { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import type { DocumentNode, GraphQLError } from 'graphql';
+
+import type { ApolloCache } from '../cache/index.js';
+import type { FetchResult } from '../link/core/index.js';
+import type { ApolloError } from '../errors/index.js';
+import type { QueryInfo } from './QueryInfo.js';
+import type { NetworkStatus } from './networkStatus.js';
+import type { Resolver } from './LocalState.js';
+import type { ObservableQuery } from './ObservableQuery.js';
+import type { QueryOptions } from './watchQueryOptions.js';
+import type { Cache } from '../cache/index.js';
+import type { IsStrictlyAny } from '../utilities/index.js';
+
+export type { TypedDocumentNode } from '@graphql-typed-document-node/core';
+
+export type MethodKeys<T> = {
+  [P in keyof T]: T[P] extends Function ? P : never
+}[keyof T];
 
 export interface DefaultContext extends Record<string, any> {};
 
diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts
--- a/src/core/watchQueryOptions.ts
+++ b/src/core/watchQueryOptions.ts
@@ -1,17 +1,17 @@
-import { DocumentNode } from 'graphql';
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import type { DocumentNode } from 'graphql';
+import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
 
-import { FetchResult } from '../link/core';
-import {
+import type { FetchResult } from '../link/core/index.js';
+import type {
   DefaultContext,
   MutationQueryReducersMap,
   OperationVariables,
   MutationUpdaterFunction,
   OnQueryUpdated,
   InternalRefetchQueriesInclude,
-} from './types';
-import { ApolloCache } from '../cache';
-import { ObservableQuery } from './ObservableQuery';
+} from './types.js';
+import type { ApolloCache } from '../cache/index.js';
+import type { ObservableQuery } from './ObservableQuery.js';
 
 /**
  * fetchPolicy determines where the client may return a result from. The options are:
diff --git a/src/dev/index.ts b/src/dev/index.ts
new file mode 100644
--- /dev/null
+++ b/src/dev/index.ts
@@ -0,0 +1,3 @@
+export { loadDevMessages } from "./loadDevMessages.js";
+export { loadErrorMessageHandler } from "./loadErrorMessageHandler.js";
+export { loadErrorMessages } from "./loadErrorMessages.js";
diff --git a/src/dev/loadDevMessages.ts b/src/dev/loadDevMessages.ts
new file mode 100644
--- /dev/null
+++ b/src/dev/loadDevMessages.ts
@@ -0,0 +1,6 @@
+import { devDebug, devError, devLog, devWarn } from "../invariantErrorCodes.js";
+import { loadErrorMessageHandler } from "./loadErrorMessageHandler.js";
+
+export function loadDevMessages() {
+  loadErrorMessageHandler(devDebug, devError, devLog, devWarn);
+}
diff --git a/src/dev/loadErrorMessageHandler.ts b/src/dev/loadErrorMessageHandler.ts
new file mode 100644
--- /dev/null
+++ b/src/dev/loadErrorMessageHandler.ts
@@ -0,0 +1,27 @@
+import type { ErrorCodes } from "../invariantErrorCodes.js";
+import { global } from "../utilities/globals/index.js";
+import { ApolloErrorMessageHandler } from "../utilities/globals/invariantWrappers.js";
+
+export function loadErrorMessageHandler(...errorCodes: ErrorCodes[]) {
+  if (!global[ApolloErrorMessageHandler]) {
+    global[ApolloErrorMessageHandler] = handler as typeof handler & ErrorCodes;
+  }
+
+  for (const codes of errorCodes) {
+    Object.assign(global[ApolloErrorMessageHandler], codes);
+  }
+
+  return global[ApolloErrorMessageHandler];
+
+  function handler(message: string | number, args: unknown[]) {
+    if (typeof message === "number") {
+      const definition = global[ApolloErrorMessageHandler]![message];
+      if (!message || !definition.message) return;
+      message = definition.message;
+    }
+    return args.reduce<string>(
+      (msg, arg) => msg.replace("%s", String(arg)),
+      String(message)
+    );
+  }
+}
diff --git a/src/dev/loadErrorMessages.ts b/src/dev/loadErrorMessages.ts
new file mode 100644
--- /dev/null
+++ b/src/dev/loadErrorMessages.ts
@@ -0,0 +1,6 @@
+import { errorCodes } from "../invariantErrorCodes.js";
+import { loadErrorMessageHandler } from "./loadErrorMessageHandler.js";
+
+export function loadErrorMessages() {
+  loadErrorMessageHandler(errorCodes);
+}
diff --git a/src/errors/index.ts b/src/errors/index.ts
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -1,11 +1,11 @@
-import '../utilities/globals';
+import '../utilities/globals/index.js';
 
-import { GraphQLError, GraphQLErrorExtensions } from 'graphql';
+import type { GraphQLError, GraphQLErrorExtensions } from 'graphql';
 
-import { isNonNullObject } from '../utilities';
-import { ServerParseError } from '../link/http';
-import { ServerError } from '../link/utils';
-import { FetchResult } from "../link/core";
+import { isNonNullObject } from '../utilities/index.js';
+import type { ServerParseError } from '../link/http/index.js';
+import type { ServerError } from '../link/utils/index.js';
+import type { FetchResult } from "../link/core/index.js";
 
 // This Symbol allows us to pass transport-specific errors from the link chain
 // into QueryManager/client internals without risking a naming collision within
diff --git a/src/index.ts b/src/index.ts
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,2 @@
-export * from './core';
-export * from './react';
+export * from './core/index.js';
+export * from './react/index.js';
diff --git a/src/invariantErrorCodes.ts b/src/invariantErrorCodes.ts
new file mode 100644
--- /dev/null
+++ b/src/invariantErrorCodes.ts
@@ -0,0 +1,9 @@
+export interface ErrorCodes {
+    [key: number]: { file: string, condition?: string, message?: string }
+}
+
+export const errorCodes: ErrorCodes = {};
+export const devDebug: ErrorCodes = {};
+export const devLog: ErrorCodes = {};
+export const devWarn: ErrorCodes = {};
+export const devError: ErrorCodes = {};
diff --git a/src/link/batch-http/batchHttpLink.ts b/src/link/batch-http/batchHttpLink.ts
--- a/src/link/batch-http/batchHttpLink.ts
+++ b/src/link/batch-http/batchHttpLink.ts
@@ -1,6 +1,13 @@
-import { ApolloLink, Operation, FetchResult } from '../core';
-import { Observable } from '../../utilities';
-import { fromError } from '../utils';
+import type { Operation, FetchResult } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import {
+  Observable,
+  hasDirectives,
+  removeClientSetsFromDocument
+} from '../../utilities/index.js';
+import { fromError } from '../utils/index.js';
+import type {
+  HttpOptions} from '../http/index.js';
 import {
   serializeFetchParameter,
   selectURI,
@@ -9,10 +16,9 @@ import {
   selectHttpOptionsAndBodyInternal,
   defaultPrinter,
   fallbackHttpConfig,
-  HttpOptions,
-  createSignalIfSupported,
-} from '../http';
-import { BatchLink } from '../batch';
+} from '../http/index.js';
+import { BatchLink } from '../batch/index.js';
+import { filterOperationVariables } from "../utils/filterOperationVariables.js";
 
 export namespace BatchHttpLink {
   export type Options = Pick<
@@ -45,6 +51,7 @@ export class BatchHttpLink extends ApolloLink {
       batchDebounce,
       batchMax,
       batchKey,
+      includeUnusedVariables = false,
       ...requestOptions
     } = fetchParams || ({} as BatchHttpLink.Options);
 
@@ -95,16 +102,43 @@ export class BatchHttpLink extends ApolloLink {
         headers: { ...clientAwarenessHeaders, ...context.headers },
       };
 
+      const queries = operations.map(({ query }) => {
+        if (hasDirectives(['client'], query)) {
+          return removeClientSetsFromDocument(query);
+        }
+
+        return query;
+      });
+
+      // If we have a query that returned `null` after removing client-only
+      // fields, it indicates a query that is using all client-only fields.
+      if (queries.some(query => !query)) {
+        return fromError<FetchResult[]>(
+          new Error(
+            'BatchHttpLink: Trying to send a client-only query to the server. To send to the server, ensure a non-client field is added to the query or enable the `transformOptions.removeClientFields` option.'
+          )
+        );
+      }
+
       //uses fallback, link, and then context to build options
-      const optsAndBody = operations.map(operation =>
-        selectHttpOptionsAndBodyInternal(
-          operation,
+      const optsAndBody = operations.map((operation, index) => {
+        const result = selectHttpOptionsAndBodyInternal(
+          { ...operation, query: queries[index]! },
           print,
           fallbackHttpConfig,
           linkConfig,
-          contextConfig,
-        ),
-      );
+          contextConfig
+        );
+
+        if (result.body.variables && !includeUnusedVariables) {
+          result.body.variables = filterOperationVariables(
+            result.body.variables,
+            operation.query
+          );
+        }
+
+        return result;
+      });
 
       const loadedBody = optsAndBody.map(({ body }) => body);
       const options = optsAndBody[0].options;
@@ -122,11 +156,10 @@ export class BatchHttpLink extends ApolloLink {
         return fromError<FetchResult[]>(parseError);
       }
 
-      let controller: any;
-      if (!(options as any).signal) {
-        const { controller: _controller, signal } = createSignalIfSupported();
-        controller = _controller;
-        if (controller) (options as any).signal = signal;
+      let controller: AbortController | undefined;
+      if (!options.signal && typeof AbortController !== 'undefined') {
+        controller = new AbortController();
+        options.signal = controller.signal;
       }
 
       return new Observable<FetchResult[]>(observer => {
@@ -138,14 +171,14 @@ export class BatchHttpLink extends ApolloLink {
           })
           .then(parseAndCheckHttpResponse(operations))
           .then(result => {
+            controller = undefined;
             // we have data and can send it to back up the link chain
             observer.next(result);
             observer.complete();
             return result;
           })
           .catch(err => {
-            // fetch was cancelled so its already been cleaned up in the unsubscribe
-            if (err.name === 'AbortError') return;
+            controller = undefined;
             // if it is a network error, BUT there is graphql result info
             // fire the next observer before calling error
             // this gives apollo-client (and react-apollo) the `graphqlErrors` and `networkErrors`
diff --git a/src/link/batch-http/index.ts b/src/link/batch-http/index.ts
--- a/src/link/batch-http/index.ts
+++ b/src/link/batch-http/index.ts
@@ -1 +1 @@
-export * from './batchHttpLink';
+export * from './batchHttpLink.js';
diff --git a/src/link/batch/batchLink.ts b/src/link/batch/batchLink.ts
--- a/src/link/batch/batchLink.ts
+++ b/src/link/batch/batchLink.ts
@@ -1,7 +1,10 @@
-import { ApolloLink, Operation, FetchResult, NextLink } from '../core';
-import { Observable } from '../../utilities';
-import { OperationBatcher, BatchHandler } from './batching';
-export { OperationBatcher, BatchableRequest, BatchHandler } from './batching';
+import type { Operation, FetchResult, NextLink } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import type { Observable } from '../../utilities/index.js';
+import type { BatchHandler } from './batching.js';
+import { OperationBatcher } from './batching.js';
+export type { BatchableRequest, BatchHandler } from './batching.js';
+export { OperationBatcher } from './batching.js';
 
 
 export namespace BatchLink {
diff --git a/src/link/batch/batching.ts b/src/link/batch/batching.ts
--- a/src/link/batch/batching.ts
+++ b/src/link/batch/batching.ts
@@ -1,5 +1,6 @@
-import { FetchResult, NextLink, Operation } from '../core';
-import { Observable, ObservableSubscription } from '../../utilities';
+import type { FetchResult, NextLink, Operation } from '../core/index.js';
+import type { ObservableSubscription } from '../../utilities/index.js';
+import { Observable } from '../../utilities/index.js';
 
 export type BatchHandler = (
   operations: Operation[],
diff --git a/src/link/batch/index.ts b/src/link/batch/index.ts
--- a/src/link/batch/index.ts
+++ b/src/link/batch/index.ts
@@ -1 +1 @@
-export * from './batchLink';
+export * from './batchLink.js';
diff --git a/src/link/context/index.ts b/src/link/context/index.ts
--- a/src/link/context/index.ts
+++ b/src/link/context/index.ts
@@ -1,6 +1,8 @@
-import { ApolloLink, Operation, GraphQLRequest, NextLink } from '../core';
-import { Observable, ObservableSubscription } from '../../utilities';
-import { DefaultContext } from '../../core';
+import type { Operation, GraphQLRequest, NextLink } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import type { ObservableSubscription } from '../../utilities/index.js';
+import { Observable } from '../../utilities/index.js';
+import type { DefaultContext } from '../../core/index.js';
 
 export type ContextSetter = (
   operation: GraphQLRequest,
diff --git a/src/link/core/ApolloLink.ts b/src/link/core/ApolloLink.ts
--- a/src/link/core/ApolloLink.ts
+++ b/src/link/core/ApolloLink.ts
@@ -1,18 +1,19 @@
-import { InvariantError, invariant } from '../../utilities/globals';
+import { newInvariantError, invariant } from '../../utilities/globals/index.js';
 
-import { Observable, Observer } from '../../utilities';
-import {
+import type { Observer } from '../../utilities/index.js';
+import { Observable } from '../../utilities/index.js';
+import type {
   NextLink,
   Operation,
   RequestHandler,
   FetchResult,
   GraphQLRequest
-} from './types';
+} from './types.js';
 import {
   validateOperation,
   createOperation,
   transformOperation,
-} from '../utils';
+} from '../utils/index.js';
 
 function passthrough(op: Operation, forward: NextLink) {
   return (forward ? forward(op) : Observable.of()) as Observable<FetchResult>;
@@ -26,14 +27,6 @@ function isTerminating(link: ApolloLink): boolean {
   return link.request.length <= 1;
 }
 
-class LinkError extends Error {
-  public link?: ApolloLink;
-  constructor(message?: string, link?: ApolloLink) {
-    super(message);
-    this.link = link;
-  }
-}
-
 export class ApolloLink {
   public static empty(): ApolloLink {
     return new ApolloLink(() => Observable.of());
@@ -88,10 +81,8 @@ export class ApolloLink {
     const firstLink = toLink(first);
     if (isTerminating(firstLink)) {
       invariant.warn(
-        new LinkError(
-          `You are calling concat on a terminating link, which will have no effect`,
+          `You are calling concat on a terminating link, which will have no effect %o`,
           firstLink,
-        ),
       );
       return firstLink;
     }
@@ -138,7 +129,7 @@ export class ApolloLink {
     operation: Operation,
     forward?: NextLink,
   ): Observable<FetchResult> | null {
-    throw new InvariantError('request is not implemented');
+    throw newInvariantError('request is not implemented');
   }
 
   protected onError(
diff --git a/src/link/core/concat.ts b/src/link/core/concat.ts
--- a/src/link/core/concat.ts
+++ b/src/link/core/concat.ts
@@ -1,3 +1,3 @@
-import { ApolloLink } from './ApolloLink';
+import { ApolloLink } from './ApolloLink.js';
 
 export const concat = ApolloLink.concat;
diff --git a/src/link/core/empty.ts b/src/link/core/empty.ts
--- a/src/link/core/empty.ts
+++ b/src/link/core/empty.ts
@@ -1,3 +1,3 @@
-import { ApolloLink } from './ApolloLink';
+import { ApolloLink } from './ApolloLink.js';
 
 export const empty = ApolloLink.empty;
diff --git a/src/link/core/execute.ts b/src/link/core/execute.ts
--- a/src/link/core/execute.ts
+++ b/src/link/core/execute.ts
@@ -1,3 +1,3 @@
-import { ApolloLink } from './ApolloLink';
+import { ApolloLink } from './ApolloLink.js';
 
 export const execute = ApolloLink.execute;
diff --git a/src/link/core/from.ts b/src/link/core/from.ts
--- a/src/link/core/from.ts
+++ b/src/link/core/from.ts
@@ -1,3 +1,3 @@
-import { ApolloLink } from './ApolloLink';
+import { ApolloLink } from './ApolloLink.js';
 
 export const from = ApolloLink.from;
diff --git a/src/link/core/index.ts b/src/link/core/index.ts
--- a/src/link/core/index.ts
+++ b/src/link/core/index.ts
@@ -1,10 +1,10 @@
-import '../../utilities/globals';
+import '../../utilities/globals/index.js';
 
-export { empty } from './empty';
-export { from } from './from';
-export { split } from './split';
-export { concat } from './concat';
-export { execute } from './execute';
-export { ApolloLink } from './ApolloLink';
+export { empty } from './empty.js';
+export { from } from './from.js';
+export { split } from './split.js';
+export { concat } from './concat.js';
+export { execute } from './execute.js';
+export { ApolloLink } from './ApolloLink.js';
 
-export * from './types';
+export * from './types.js';
diff --git a/src/link/core/split.ts b/src/link/core/split.ts
--- a/src/link/core/split.ts
+++ b/src/link/core/split.ts
@@ -1,3 +1,3 @@
-import { ApolloLink } from './ApolloLink';
+import { ApolloLink } from './ApolloLink.js';
 
 export const split = ApolloLink.split;
diff --git a/src/link/core/types.ts b/src/link/core/types.ts
--- a/src/link/core/types.ts
+++ b/src/link/core/types.ts
@@ -1,8 +1,9 @@
-import { DocumentNode, ExecutionResult, GraphQLError } from "graphql";
-import { DefaultContext } from "../../core";
-export { DocumentNode };
+import type { ExecutionResult, GraphQLError } from "graphql";
+import type { DocumentNode } from "graphql";
+import type { DefaultContext } from "../../core/index.js";
+export type { DocumentNode };
 
-import { Observable } from "../../utilities";
+import type { Observable } from "../../utilities/index.js";
 
 export type Path = ReadonlyArray<string | number>;
 
diff --git a/src/link/error/index.ts b/src/link/error/index.ts
--- a/src/link/error/index.ts
+++ b/src/link/error/index.ts
@@ -1,8 +1,9 @@
-import { ExecutionResult } from 'graphql';
+import type { ExecutionResult } from 'graphql';
 
-import { NetworkError, GraphQLErrors } from '../../errors';
-import { Observable } from '../../utilities';
-import { ApolloLink, Operation, FetchResult, NextLink } from '../core';
+import type { NetworkError, GraphQLErrors } from '../../errors/index.js';
+import { Observable } from '../../utilities/index.js';
+import type { Operation, FetchResult, NextLink } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
 
 export interface ErrorResponse {
   graphQLErrors?: GraphQLErrors;
diff --git a/src/link/http/HttpLink.ts b/src/link/http/HttpLink.ts
--- a/src/link/http/HttpLink.ts
+++ b/src/link/http/HttpLink.ts
@@ -1,6 +1,7 @@
-import { ApolloLink, RequestHandler } from '../core';
-import { HttpOptions } from './selectHttpOptionsAndBody';
-import { createHttpLink } from './createHttpLink';
+import type { RequestHandler } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import type { HttpOptions } from './selectHttpOptionsAndBody.js';
+import { createHttpLink } from './createHttpLink.js';
 
 export class HttpLink extends ApolloLink {
   public requester: RequestHandler;
diff --git a/src/link/http/checkFetcher.ts b/src/link/http/checkFetcher.ts
--- a/src/link/http/checkFetcher.ts
+++ b/src/link/http/checkFetcher.ts
@@ -1,8 +1,8 @@
-import { InvariantError } from '../../utilities/globals';
+import { newInvariantError } from '../../utilities/globals/index.js';
 
 export const checkFetcher = (fetcher: WindowOrWorkerGlobalScope['fetch'] | undefined) => {
   if (!fetcher && typeof fetch === 'undefined') {
-    throw new InvariantError(`
+    throw newInvariantError(`
 "fetch" has not been found globally and no fetcher has been \
 configured. To fix this, install a fetch package (like \
 https://www.npmjs.com/package/cross-fetch), instantiate the \
diff --git a/src/link/http/createHttpLink.ts b/src/link/http/createHttpLink.ts
--- a/src/link/http/createHttpLink.ts
+++ b/src/link/http/createHttpLink.ts
@@ -1,28 +1,32 @@
-import '../../utilities/globals';
-import { invariant } from '../../utilities/globals';
+import { invariant } from '../../utilities/globals/index.js';
 
-import { visit, DefinitionNode, VariableDefinitionNode } from 'graphql';
+import type { DefinitionNode } from 'graphql';
 
-import { ApolloLink } from '../core';
-import { Observable, hasDirectives } from '../../utilities';
-import { serializeFetchParameter } from './serializeFetchParameter';
-import { selectURI } from './selectURI';
+import { ApolloLink } from '../core/index.js';
+import { Observable, hasDirectives } from '../../utilities/index.js';
+import { serializeFetchParameter } from './serializeFetchParameter.js';
+import { selectURI } from './selectURI.js';
 import {
   handleError,
   readMultipartBody,
-  readJsonBody
-} from './parseAndCheckHttpResponse';
-import { checkFetcher } from './checkFetcher';
+  parseAndCheckHttpResponse
+} from './parseAndCheckHttpResponse.js';
+import { checkFetcher } from './checkFetcher.js';
+import type {
+  HttpOptions
+} from './selectHttpOptionsAndBody.js';
 import {
   selectHttpOptionsAndBodyInternal,
   defaultPrinter,
-  fallbackHttpConfig,
-  HttpOptions
-} from './selectHttpOptionsAndBody';
-import { createSignalIfSupported } from './createSignalIfSupported';
-import { rewriteURIForGET } from './rewriteURIForGET';
-import { fromError } from '../utils';
-import { maybe, getMainDefinition } from '../../utilities';
+  fallbackHttpConfig
+} from './selectHttpOptionsAndBody.js';
+import { rewriteURIForGET } from './rewriteURIForGET.js';
+import { fromError, filterOperationVariables } from '../utils/index.js';
+import {
+  maybe,
+  getMainDefinition,
+  removeClientSetsFromDocument
+} from '../../utilities/index.js';
 
 const backupFetch = maybe(() => fetch);
 
@@ -87,6 +91,20 @@ export const createHttpLink = (linkOptions: HttpOptions = {}) => {
       headers: contextHeaders,
     };
 
+    if (hasDirectives(['client'], operation.query)) {
+      const transformedQuery = removeClientSetsFromDocument(operation.query);
+
+      if (!transformedQuery) {
+        return fromError(
+          new Error(
+            'HttpLink: Trying to send a client-only query to the server. To send to the server, ensure a non-client field is added to the query or set the `transformOptions.removeClientFields` option to `true`.'
+          )
+        );
+      }
+
+      operation.query = transformedQuery;
+    }
+
     //uses fallback, link, and then context to build options
     const { options, body } = selectHttpOptionsAndBodyInternal(
       operation,
@@ -97,33 +115,13 @@ export const createHttpLink = (linkOptions: HttpOptions = {}) => {
     );
 
     if (body.variables && !includeUnusedVariables) {
-      const unusedNames = new Set(Object.keys(body.variables));
-      visit(operation.query, {
-        Variable(node, _key, parent) {
-          // A variable type definition at the top level of a query is not
-          // enough to silence server-side errors about the variable being
-          // unused, so variable definitions do not count as usage.
-          // https://spec.graphql.org/draft/#sec-All-Variables-Used
-          if (parent && (parent as VariableDefinitionNode).kind !== 'VariableDefinition') {
-            unusedNames.delete(node.name.value);
-          }
-        },
-      });
-      if (unusedNames.size) {
-        // Make a shallow copy of body.variables (with keys in the same
-        // order) and then delete unused variables from the copy.
-        body.variables = { ...body.variables };
-        unusedNames.forEach(name => {
-          delete body.variables![name];
-        });
-      }
+      body.variables = filterOperationVariables(body.variables, operation.query);
     }
 
-    let controller: any;
-    if (!(options as any).signal) {
-      const { controller: _controller, signal } = createSignalIfSupported();
-      controller = _controller;
-      if (controller) (options as any).signal = signal;
+    let controller: AbortController | undefined;
+    if (!options.signal && typeof AbortController !== 'undefined') {
+      controller = new AbortController();
+      options.signal = controller.signal;
     }
 
     // If requested, set method to GET if there are no mutations.
@@ -182,18 +180,26 @@ export const createHttpLink = (linkOptions: HttpOptions = {}) => {
       // removal of window.fetch, which is unlikely but not impossible.
       const currentFetch = preferredFetch || maybe(() => fetch) || backupFetch;
 
+      const observerNext = observer.next.bind(observer);
       currentFetch!(chosenURI, options)
         .then(response => {
           operation.setContext({ response });
           const ctype = response.headers?.get('content-type');
 
           if (ctype !== null && /^multipart\/mixed/i.test(ctype)) {
-            return readMultipartBody(response, observer);
+            return readMultipartBody(response, observerNext);
           } else {
-            return readJsonBody(response, operation, observer);
+            return parseAndCheckHttpResponse(operation)(response).then(observerNext);
           }
         })
-        .catch(err => handleError(err, observer));
+        .then(() => {
+          controller = undefined;
+          observer.complete();
+        })
+        .catch(err => {
+          controller = undefined;
+          handleError(err, observer)
+        });
 
       return () => {
         // XXX support canceling this request
diff --git a/src/link/http/createSignalIfSupported.ts b/src/link/http/createSignalIfSupported.ts
--- a/src/link/http/createSignalIfSupported.ts
+++ b/src/link/http/createSignalIfSupported.ts
@@ -1,3 +1,8 @@
+/**
+ * @deprecated 
+ * This is not used internally any more and will be removed in
+ * the next major version of Apollo Client.
+ */
 export const createSignalIfSupported = () => {
   if (typeof AbortController === 'undefined')
     return { controller: false, signal: false };
diff --git a/src/link/http/index.ts b/src/link/http/index.ts
--- a/src/link/http/index.ts
+++ b/src/link/http/index.ts
@@ -1,24 +1,19 @@
-import '../../utilities/globals';
+import '../../utilities/globals/index.js';
 
+export type { ServerParseError } from './parseAndCheckHttpResponse.js';
+export { parseAndCheckHttpResponse } from './parseAndCheckHttpResponse.js';
+export type { ClientParseError } from './serializeFetchParameter.js';
+export { serializeFetchParameter } from './serializeFetchParameter.js';
+export type { HttpOptions, UriFunction } from './selectHttpOptionsAndBody.js';
 export {
-  parseAndCheckHttpResponse,
-  ServerParseError
-} from './parseAndCheckHttpResponse';
-export {
-  serializeFetchParameter,
-  ClientParseError
-} from './serializeFetchParameter';
-export {
-  HttpOptions,
   fallbackHttpConfig,
   defaultPrinter,
   selectHttpOptionsAndBody,
   selectHttpOptionsAndBodyInternal, // needed by ../batch-http but not public
-  UriFunction
-} from './selectHttpOptionsAndBody';
-export { checkFetcher } from './checkFetcher';
-export { createSignalIfSupported } from './createSignalIfSupported';
-export { selectURI } from './selectURI';
-export { createHttpLink } from './createHttpLink';
-export { HttpLink } from './HttpLink';
-export { rewriteURIForGET } from './rewriteURIForGET';
+} from './selectHttpOptionsAndBody.js';
+export { checkFetcher } from './checkFetcher.js';
+export { createSignalIfSupported } from './createSignalIfSupported.js';
+export { selectURI } from './selectURI.js';
+export { createHttpLink } from './createHttpLink.js';
+export { HttpLink } from './HttpLink.js';
+export { rewriteURIForGET } from './rewriteURIForGET.js';
diff --git a/src/link/http/iterators/nodeStream.ts b/src/link/http/iterators/nodeStream.ts
--- a/src/link/http/iterators/nodeStream.ts
+++ b/src/link/http/iterators/nodeStream.ts
@@ -3,8 +3,8 @@
  * https://github.com/kmalakoff/response-iterator/blob/master/src/iterators/nodeStream.ts
  */
 
-import { Readable as NodeReadableStream } from "stream";
-import { canUseAsyncIteratorSymbol } from "../../../utilities";
+import type { Readable as NodeReadableStream } from "stream";
+import { canUseAsyncIteratorSymbol } from "../../../utilities/index.js";
 
 interface NodeStreamIterator<T> {
   next(): Promise<IteratorResult<T, boolean | undefined>>;
diff --git a/src/link/http/iterators/promise.ts b/src/link/http/iterators/promise.ts
--- a/src/link/http/iterators/promise.ts
+++ b/src/link/http/iterators/promise.ts
@@ -3,7 +3,7 @@
  * https://github.com/kmalakoff/response-iterator/blob/master/src/iterators/promise.ts
  */
 
-import { canUseAsyncIteratorSymbol } from "../../../utilities";
+import { canUseAsyncIteratorSymbol } from "../../../utilities/index.js";
 
 interface PromiseIterator<T> {
   next(): Promise<IteratorResult<T, ArrayBuffer | undefined>>;
diff --git a/src/link/http/iterators/reader.ts b/src/link/http/iterators/reader.ts
--- a/src/link/http/iterators/reader.ts
+++ b/src/link/http/iterators/reader.ts
@@ -3,7 +3,7 @@
  * https://github.com/kmalakoff/response-iterator/blob/master/src/iterators/reader.ts
  */
 
-import { canUseAsyncIteratorSymbol } from "../../../utilities";
+import { canUseAsyncIteratorSymbol } from "../../../utilities/index.js";
 
 interface ReaderIterator<T> {
   next(): Promise<ReadableStreamReadResult<T>>;
diff --git a/src/link/http/parseAndCheckHttpResponse.ts b/src/link/http/parseAndCheckHttpResponse.ts
--- a/src/link/http/parseAndCheckHttpResponse.ts
+++ b/src/link/http/parseAndCheckHttpResponse.ts
@@ -1,11 +1,11 @@
-import { responseIterator } from "./responseIterator";
-import { Operation } from "../core";
-import { throwServerError } from "../utils";
-import { PROTOCOL_ERRORS_SYMBOL } from '../../errors';
-import { Observer } from "../../utilities";
+import { responseIterator } from "./responseIterator.js";
+import type { Operation } from "../core/index.js";
+import { throwServerError } from "../utils/index.js";
+import { PROTOCOL_ERRORS_SYMBOL } from '../../errors/index.js';
 import {
   isApolloPayloadResult
-} from '../../utilities/common/incrementalResult';
+} from '../../utilities/common/incrementalResult.js';
+import type { SubscriptionObserver } from "zen-observable-ts";
 
 const { hasOwnProperty } = Object.prototype;
 
@@ -17,7 +17,7 @@ export type ServerParseError = Error & {
 
 export async function readMultipartBody<
   T extends object = Record<string, unknown>
->(response: Response, observer: Observer<T>) {
+>(response: Response, nextValue: (value: T) => void) {
   if (TextDecoder === undefined) {
     throw new Error(
       "TextDecoder must be defined in the environment: please import a polyfill."
@@ -74,52 +74,47 @@ export async function readMultipartBody<
       const body = message.slice(i);
 
       if (body) {
-        try {
-          const result = parseJsonBody<T>(response, body);
-          if (
-            Object.keys(result).length > 1 ||
-            "data" in result ||
-            "incremental" in result ||
-            "errors" in result ||
-            "payload" in result
-          ) {
-            if (isApolloPayloadResult(result)) {
-              let next = {};
-              if ("payload" in result) {
-                next = { ...result.payload };
-              }
-              if ("errors" in result) {
-                next = {
-                  ...next,
-                  extensions: {
-                    ...("extensions" in next ? next.extensions : null as any),
-                    [PROTOCOL_ERRORS_SYMBOL]: result.errors
-                  },
-                };
-              }
-              observer.next?.(next as T);
-            } else {
-              // for the last chunk with only `hasNext: false`
-              // we don't need to call observer.next as there is no data/errors
-              observer.next?.(result);
+        const result = parseJsonBody<T>(response, body);
+        if (
+          Object.keys(result).length > 1 ||
+          "data" in result ||
+          "incremental" in result ||
+          "errors" in result ||
+          "payload" in result
+        ) {
+          if (isApolloPayloadResult(result)) {
+            let next = {};
+            if ("payload" in result) {
+              next = { ...result.payload };
+            }
+            if ("errors" in result) {
+              next = {
+                ...next,
+                extensions: {
+                  ...("extensions" in next ? next.extensions : null as any),
+                  [PROTOCOL_ERRORS_SYMBOL]: result.errors
+                },
+              };
             }
-          } else if (
-            // If the chunk contains only a "hasNext: false", we can call
-            // observer.complete() immediately.
-            Object.keys(result).length === 1 &&
-            "hasNext" in result &&
-            !result.hasNext
-          ) {
-            observer.complete?.();
+            nextValue(next as T);
+          } else {
+            // for the last chunk with only `hasNext: false`
+            // we don't need to call observer.next as there is no data/errors
+            nextValue(result);
           }
-        } catch (err) {
-          handleError(err, observer);
+        } else if (
+          // If the chunk contains only a "hasNext: false", we can call
+          // observer.complete() immediately.
+          Object.keys(result).length === 1 &&
+          "hasNext" in result &&
+          !result.hasNext
+        ) {
+          return;
         }
       }
       bi = buffer.indexOf(boundary);
     }
   }
-  observer.complete?.();
 }
 
 export function parseHeaders(headerText: string): Record<string, string> {
@@ -165,8 +160,7 @@ export function parseJsonBody<T>(response: Response, bodyText: string): T {
   }
 }
 
-export function handleError(err: any, observer: Observer<any>) {
-  if (err.name === "AbortError") return;
+export function handleError(err: any, observer: SubscriptionObserver<any>) {
   // if it is a network error, BUT there is graphql result info fire
   // the next observer before calling error this gives apollo-client
   // (and react-apollo) the `graphqlErrors` and `networkErrors` to
@@ -200,23 +194,10 @@ export function handleError(err: any, observer: Observer<any>) {
     // status code of above would be a 401
     // in the UI you want to show data where you can, errors as data where you can
     // and use correct http status codes
-    observer.next?.(err.result);
+    observer.next(err.result);
   }
 
-  observer.error?.(err);
-}
-
-export function readJsonBody<T = Record<string, unknown>>(
-  response: Response,
-  operation: Operation,
-  observer: Observer<T>
-) {
-  parseAndCheckHttpResponse(operation)(response)
-    .then((result) => {
-      observer.next?.(result);
-      observer.complete?.();
-    })
-    .catch((err) => handleError(err, observer));
+  observer.error(err);
 }
 
 export function parseAndCheckHttpResponse(operations: Operation | Operation[]) {
diff --git a/src/link/http/responseIterator.ts b/src/link/http/responseIterator.ts
--- a/src/link/http/responseIterator.ts
+++ b/src/link/http/responseIterator.ts
@@ -3,7 +3,7 @@
  * https://github.com/kmalakoff/response-iterator/blob/master/src/index.ts
  */
 
-import { Response as NodeResponse } from "node-fetch";
+import type { Response as NodeResponse } from "node-fetch";
 import {
   isAsyncIterableIterator,
   isBlob,
@@ -11,12 +11,12 @@ import {
   isNodeReadableStream,
   isReadableStream,
   isStreamableBlob,
-} from "../../utilities/common/responseIterator";
+} from "../../utilities/index.js";
 
-import asyncIterator from "./iterators/async";
-import nodeStreamIterator from "./iterators/nodeStream";
-import promiseIterator from "./iterators/promise";
-import readerIterator from "./iterators/reader";
+import asyncIterator from "./iterators/async.js";
+import nodeStreamIterator from "./iterators/nodeStream.js";
+import promiseIterator from "./iterators/promise.js";
+import readerIterator from "./iterators/reader.js";
 
 export function responseIterator<T>(
   response: Response | NodeResponse
diff --git a/src/link/http/rewriteURIForGET.ts b/src/link/http/rewriteURIForGET.ts
--- a/src/link/http/rewriteURIForGET.ts
+++ b/src/link/http/rewriteURIForGET.ts
@@ -1,5 +1,5 @@
-import { serializeFetchParameter } from './serializeFetchParameter';
-import { Body } from './selectHttpOptionsAndBody';
+import { serializeFetchParameter } from './serializeFetchParameter.js';
+import type { Body } from './selectHttpOptionsAndBody.js';
 
 // For GET operations, returns the given URI rewritten with parameters, or a
 // parse error.
diff --git a/src/link/http/selectHttpOptionsAndBody.ts b/src/link/http/selectHttpOptionsAndBody.ts
--- a/src/link/http/selectHttpOptionsAndBody.ts
+++ b/src/link/http/selectHttpOptionsAndBody.ts
@@ -1,6 +1,7 @@
-import { ASTNode, print } from 'graphql';
+import type { ASTNode} from 'graphql';
+import { print } from '../../utilities/index.js';
 
-import { Operation } from '../core';
+import type { Operation } from '../core/index.js';
 
 export interface Printer {
   (node: ASTNode, originalPrint: typeof print): string
diff --git a/src/link/http/selectURI.ts b/src/link/http/selectURI.ts
--- a/src/link/http/selectURI.ts
+++ b/src/link/http/selectURI.ts
@@ -1,4 +1,4 @@
-import { Operation } from '../core';
+import type { Operation } from '../core/index.js';
 
 export const selectURI = (
   operation: Operation,
diff --git a/src/link/http/serializeFetchParameter.ts b/src/link/http/serializeFetchParameter.ts
--- a/src/link/http/serializeFetchParameter.ts
+++ b/src/link/http/serializeFetchParameter.ts
@@ -1,4 +1,5 @@
-import { InvariantError } from '../../utilities/globals';
+import { newInvariantError } from '../../utilities/globals/index.js';
+import type { InvariantError } from '../../utilities/globals/index.js';
 
 export type ClientParseError = InvariantError & {
   parseError: Error;
@@ -9,8 +10,10 @@ export const serializeFetchParameter = (p: any, label: string) => {
   try {
     serialized = JSON.stringify(p);
   } catch (e) {
-    const parseError = new InvariantError(
-      `Network request failed. ${label} is not serializable: ${e.message}`,
+    const parseError = newInvariantError(
+      `Network request failed. %s is not serializable: %s`,
+      label,
+      e.message
     ) as ClientParseError;
     parseError.parseError = e;
     throw parseError;
diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts
--- a/src/link/persisted-queries/index.ts
+++ b/src/link/persisted-queries/index.ts
@@ -1,22 +1,24 @@
-import { invariant } from '../../utilities/globals';
+import { invariant } from '../../utilities/globals/index.js';
 
-import { print } from 'graphql';
-import {
+import { print } from '../../utilities/index.js';
+import type {
   DocumentNode,
   ExecutionResult,
   GraphQLError,
 } from 'graphql';
 
-import { ApolloLink, Operation } from '../core';
+import type { Operation } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import type {
+  Observer,
+  ObservableSubscription} from '../../utilities/index.js';
 import {
   Observable,
-  Observer,
-  ObservableSubscription,
   compact,
   isNonEmptyArray,
-} from '../../utilities';
-import { NetworkError } from '../../errors';
-import { ServerError } from '../utils';
+} from '../../utilities/index.js';
+import type { NetworkError } from '../../errors/index.js';
+import type { ServerError } from '../utils/index.js';
 
 export const VERSION = 1;
 
diff --git a/src/link/remove-typename/index.ts b/src/link/remove-typename/index.ts
new file mode 100644
--- /dev/null
+++ b/src/link/remove-typename/index.ts
@@ -0,0 +1,5 @@
+export type { RemoveTypenameFromVariablesOptions } from "./removeTypenameFromVariables.js";
+export {
+  removeTypenameFromVariables,
+  KEEP,
+} from "./removeTypenameFromVariables.js";
diff --git a/src/link/remove-typename/removeTypenameFromVariables.ts b/src/link/remove-typename/removeTypenameFromVariables.ts
new file mode 100644
--- /dev/null
+++ b/src/link/remove-typename/removeTypenameFromVariables.ts
@@ -0,0 +1,116 @@
+import { wrap } from "optimism";
+import type { DocumentNode, TypeNode } from "graphql";
+import { Kind, visit } from "graphql";
+import { ApolloLink } from "../core/index.js";
+import { stripTypename, isPlainObject } from "../../utilities/index.js";
+import type { OperationVariables } from "../../core/index.js";
+
+export const KEEP = "__KEEP";
+
+interface KeepTypenameConfig {
+  [key: string]: typeof KEEP | KeepTypenameConfig;
+}
+
+export interface RemoveTypenameFromVariablesOptions {
+  except?: KeepTypenameConfig;
+}
+
+export function removeTypenameFromVariables(
+  options: RemoveTypenameFromVariablesOptions = Object.create(null)
+) {
+  return new ApolloLink((operation, forward) => {
+    const { except } = options;
+    const { query, variables } = operation;
+
+    if (variables) {
+      operation.variables = except
+        ? maybeStripTypenameUsingConfig(query, variables, except)
+        : stripTypename(variables);
+    }
+
+    return forward(operation);
+  });
+}
+
+function maybeStripTypenameUsingConfig(
+  query: DocumentNode,
+  variables: OperationVariables,
+  config: KeepTypenameConfig
+) {
+  const variableDefinitions = getVariableDefinitions(query);
+
+  return Object.fromEntries(
+    Object.entries(variables).map((keyVal) => {
+      const [key, value] = keyVal;
+      const typename = variableDefinitions[key];
+      const typenameConfig = config[typename];
+
+      keyVal[1] = typenameConfig
+        ? maybeStripTypename(value, typenameConfig)
+        : stripTypename(value);
+
+      return keyVal;
+    })
+  );
+}
+
+type JSONPrimitive = string | number | null | boolean;
+type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue };
+
+function maybeStripTypename(
+  value: JSONValue,
+  config: KeepTypenameConfig[string]
+): JSONValue {
+  if (config === KEEP) {
+    return value;
+  }
+
+  if (Array.isArray(value)) {
+    return value.map((item) => maybeStripTypename(item, config));
+  }
+
+  if (isPlainObject(value)) {
+    const modified: Record<string, any> = {};
+
+    Object.keys(value).forEach((key) => {
+      const child = value[key];
+
+      if (key === "__typename") {
+        return;
+      }
+
+      const fieldConfig = config[key];
+
+      modified[key] = fieldConfig
+        ? maybeStripTypename(child, fieldConfig)
+        : stripTypename(child);
+    });
+
+    return modified;
+  }
+
+  return value;
+}
+
+const getVariableDefinitions = wrap((document: DocumentNode) => {
+  const definitions: Record<string, string> = {};
+
+  visit(document, {
+    VariableDefinition(node) {
+      definitions[node.variable.name.value] = unwrapType(node.type);
+    },
+  });
+
+  return definitions;
+});
+
+function unwrapType(node: TypeNode): string {
+  switch (node.kind) {
+    case Kind.NON_NULL_TYPE:
+      return unwrapType(node.type);
+    case Kind.LIST_TYPE:
+      return unwrapType(node.type);
+    case Kind.NAMED_TYPE:
+      return node.name.value;
+  }
+}
diff --git a/src/link/retry/delayFunction.ts b/src/link/retry/delayFunction.ts
--- a/src/link/retry/delayFunction.ts
+++ b/src/link/retry/delayFunction.ts
@@ -1,4 +1,4 @@
-import { Operation } from '../core';
+import type { Operation } from '../core/index.js';
 
 /**
  * Advanced mode: a function that implements the strategy for calculating delays
diff --git a/src/link/retry/index.ts b/src/link/retry/index.ts
--- a/src/link/retry/index.ts
+++ b/src/link/retry/index.ts
@@ -1 +1 @@
-export * from './retryLink';
+export * from './retryLink.js';
diff --git a/src/link/retry/retryFunction.ts b/src/link/retry/retryFunction.ts
--- a/src/link/retry/retryFunction.ts
+++ b/src/link/retry/retryFunction.ts
@@ -1,4 +1,4 @@
-import { Operation } from '../core';
+import type { Operation } from '../core/index.js';
 
 /**
  * Advanced mode: a function that determines both whether a particular
diff --git a/src/link/retry/retryLink.ts b/src/link/retry/retryLink.ts
--- a/src/link/retry/retryLink.ts
+++ b/src/link/retry/retryLink.ts
@@ -1,15 +1,19 @@
-import { ApolloLink, Operation, FetchResult, NextLink } from '../core';
-import { Observable, Observer, ObservableSubscription } from '../../utilities';
-import {
+import type { Operation, FetchResult, NextLink } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import type { Observer, ObservableSubscription } from '../../utilities/index.js';
+import { Observable } from '../../utilities/index.js';
+import type {
   DelayFunction,
-  DelayFunctionOptions,
-  buildDelayFunction,
-} from './delayFunction';
+  DelayFunctionOptions} from './delayFunction.js';
 import {
+  buildDelayFunction,
+} from './delayFunction.js';
+import type {
   RetryFunction,
-  RetryFunctionOptions,
+  RetryFunctionOptions} from './retryFunction.js';
+import {
   buildRetryFunction,
-} from './retryFunction';
+} from './retryFunction.js';
 
 export namespace RetryLink {
   export interface Options {
diff --git a/src/link/schema/index.ts b/src/link/schema/index.ts
--- a/src/link/schema/index.ts
+++ b/src/link/schema/index.ts
@@ -1,7 +1,9 @@
-import { validate, execute, GraphQLSchema } from 'graphql';
+import type { GraphQLSchema } from 'graphql';
+import { validate, execute } from 'graphql';
 
-import { ApolloLink, Operation, FetchResult } from '../core';
-import { Observable } from '../../utilities';
+import type { Operation, FetchResult } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import { Observable } from '../../utilities/index.js';
 
 export namespace SchemaLink {
   export type ResolverContext = Record<string, any>;
diff --git a/src/link/subscriptions/index.ts b/src/link/subscriptions/index.ts
--- a/src/link/subscriptions/index.ts
+++ b/src/link/subscriptions/index.ts
@@ -28,12 +28,13 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
-import { print } from "graphql";
+import { print } from '../../utilities/index.js';
 import type { Client } from "graphql-ws";
 
-import { ApolloLink, Operation, FetchResult } from "../core";
-import { isNonNullObject, Observable } from "../../utilities";
-import { ApolloError } from "../../errors";
+import type { Operation, FetchResult } from "../core/index.js";
+import { ApolloLink } from "../core/index.js";
+import { isNonNullObject, Observable } from "../../utilities/index.js";
+import { ApolloError } from "../../errors/index.js";
 
 // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event
 function isLikeCloseEvent(val: unknown): val is CloseEvent {
diff --git a/src/link/utils/createOperation.ts b/src/link/utils/createOperation.ts
--- a/src/link/utils/createOperation.ts
+++ b/src/link/utils/createOperation.ts
@@ -1,4 +1,4 @@
-import { GraphQLRequest, Operation } from '../core';
+import type { GraphQLRequest, Operation } from '../core/index.js';
 
 export function createOperation(
   starting: any,
diff --git a/src/link/utils/filterOperationVariables.ts b/src/link/utils/filterOperationVariables.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/filterOperationVariables.ts
@@ -0,0 +1,28 @@
+import type { VariableDefinitionNode, DocumentNode } from 'graphql';
+import { visit } from 'graphql';
+
+export function filterOperationVariables(
+  variables: Record<string, any>,
+  query: DocumentNode
+) {
+  const result = { ...variables };
+  const unusedNames = new Set(Object.keys(variables));
+  visit(query, {
+    Variable(node, _key, parent) {
+      // A variable type definition at the top level of a query is not
+      // enough to silence server-side errors about the variable being
+      // unused, so variable definitions do not count as usage.
+      // https://spec.graphql.org/draft/#sec-All-Variables-Used
+      if (
+        parent &&
+        (parent as VariableDefinitionNode).kind !== 'VariableDefinition'
+      ) {
+        unusedNames.delete(node.name.value);
+      }
+    },
+  });
+  unusedNames.forEach((name) => {
+    delete result![name];
+  });
+  return result;
+}
diff --git a/src/link/utils/fromError.ts b/src/link/utils/fromError.ts
--- a/src/link/utils/fromError.ts
+++ b/src/link/utils/fromError.ts
@@ -1,4 +1,4 @@
-import { Observable } from '../../utilities';
+import { Observable } from '../../utilities/index.js';
 
 export function fromError<T>(errorValue: any): Observable<T> {
   return new Observable<T>(observer => {
diff --git a/src/link/utils/fromPromise.ts b/src/link/utils/fromPromise.ts
--- a/src/link/utils/fromPromise.ts
+++ b/src/link/utils/fromPromise.ts
@@ -1,4 +1,4 @@
-import { Observable } from '../../utilities';
+import { Observable } from '../../utilities/index.js';
 
 export function fromPromise<T>(promise: Promise<T>): Observable<T> {
   return new Observable<T>(observer => {
diff --git a/src/link/utils/index.ts b/src/link/utils/index.ts
--- a/src/link/utils/index.ts
+++ b/src/link/utils/index.ts
@@ -1,9 +1,11 @@
-import '../../utilities/globals';
+import '../../utilities/globals/index.js';
 
-export { fromError } from './fromError';
-export { toPromise } from './toPromise';
-export { fromPromise } from './fromPromise';
-export { ServerError, throwServerError } from './throwServerError';
-export { validateOperation } from './validateOperation';
-export { createOperation } from './createOperation';
-export { transformOperation } from './transformOperation';
+export { fromError } from './fromError.js';
+export { toPromise } from './toPromise.js';
+export { fromPromise } from './fromPromise.js';
+export type { ServerError } from './throwServerError.js';
+export { throwServerError } from './throwServerError.js';
+export { validateOperation } from './validateOperation.js';
+export { createOperation } from './createOperation.js';
+export { transformOperation } from './transformOperation.js';
+export { filterOperationVariables } from './filterOperationVariables.js';
diff --git a/src/link/utils/toPromise.ts b/src/link/utils/toPromise.ts
--- a/src/link/utils/toPromise.ts
+++ b/src/link/utils/toPromise.ts
@@ -1,5 +1,5 @@
-import { invariant } from '../../utilities/globals';
-import { Observable } from '../../utilities';
+import { invariant } from '../../utilities/globals/index.js';
+import type { Observable } from '../../utilities/index.js';
 
 export function toPromise<R>(observable: Observable<R>): Promise<R> {
   let completed = false;
diff --git a/src/link/utils/transformOperation.ts b/src/link/utils/transformOperation.ts
--- a/src/link/utils/transformOperation.ts
+++ b/src/link/utils/transformOperation.ts
@@ -1,5 +1,5 @@
-import { GraphQLRequest, Operation } from '../core';
-import { getOperationName } from '../../utilities';
+import type { GraphQLRequest, Operation } from '../core/index.js';
+import { getOperationName } from '../../utilities/index.js';
 
 export function transformOperation(operation: GraphQLRequest): GraphQLRequest {
   const transformedOperation: GraphQLRequest = {
diff --git a/src/link/utils/validateOperation.ts b/src/link/utils/validateOperation.ts
--- a/src/link/utils/validateOperation.ts
+++ b/src/link/utils/validateOperation.ts
@@ -1,5 +1,5 @@
-import { InvariantError } from '../../utilities/globals'
-import { GraphQLRequest } from '../core';
+import { newInvariantError } from '../../utilities/globals/index.js'
+import type { GraphQLRequest } from '../core/index.js';
 
 export function validateOperation(operation: GraphQLRequest): GraphQLRequest {
   const OPERATION_FIELDS = [
@@ -11,7 +11,7 @@ export function validateOperation(operation: GraphQLRequest): GraphQLRequest {
   ];
   for (let key of Object.keys(operation)) {
     if (OPERATION_FIELDS.indexOf(key) < 0) {
-      throw new InvariantError(`illegal argument: ${key}`);
+      throw newInvariantError(`illegal argument: %s`, key);
     }
   }
 
diff --git a/src/link/ws/index.ts b/src/link/ws/index.ts
--- a/src/link/ws/index.ts
+++ b/src/link/ws/index.ts
@@ -1,7 +1,9 @@
-import { SubscriptionClient, ClientOptions } from 'subscriptions-transport-ws';
+import type { ClientOptions } from 'subscriptions-transport-ws';
+import { SubscriptionClient } from 'subscriptions-transport-ws';
 
-import { ApolloLink, Operation, FetchResult } from '../core';
-import { Observable } from '../../utilities';
+import type { Operation, FetchResult } from '../core/index.js';
+import { ApolloLink } from '../core/index.js';
+import type { Observable } from '../../utilities/index.js';
 
 export namespace WebSocketLink {
   /**
diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts
new file mode 100644
--- /dev/null
+++ b/src/react/cache/QueryReference.ts
@@ -0,0 +1,295 @@
+import { equal } from "@wry/equality";
+import type {
+  ApolloError,
+  ApolloQueryResult,
+  ObservableQuery,
+  OperationVariables,
+  WatchQueryOptions,
+} from "../../core/index.js";
+import { isNetworkRequestSettled } from "../../core/index.js";
+import type { ObservableSubscription } from "../../utilities/index.js";
+import {
+  createFulfilledPromise,
+  createRejectedPromise,
+} from "../../utilities/index.js";
+import type { CacheKey } from "./types.js";
+import type { useBackgroundQuery, useReadQuery } from "../hooks/index.js";
+
+type Listener<TData> = (promise: Promise<ApolloQueryResult<TData>>) => void;
+
+type FetchMoreOptions<TData> = Parameters<
+  ObservableQuery<TData>["fetchMore"]
+>[0];
+
+const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol();
+/**
+ * A `QueryReference` is an opaque object returned by {@link useBackgroundQuery}.
+ * A child component reading the `QueryReference` via {@link useReadQuery} will
+ * suspend until the promise resolves.
+ */
+export interface QueryReference<TData = unknown> {
+  [QUERY_REFERENCE_SYMBOL]: InternalQueryReference<TData>;
+}
+
+interface InternalQueryReferenceOptions {
+  key: CacheKey;
+  onDispose?: () => void;
+  autoDisposeTimeoutMs?: number;
+}
+
+export function wrapQueryRef<TData>(
+  internalQueryRef: InternalQueryReference<TData>
+): QueryReference<TData> {
+  return { [QUERY_REFERENCE_SYMBOL]: internalQueryRef };
+}
+
+export function unwrapQueryRef<TData>(
+  queryRef: QueryReference<TData>
+): InternalQueryReference<TData> {
+  return queryRef[QUERY_REFERENCE_SYMBOL];
+}
+
+const OBSERVED_CHANGED_OPTIONS = [
+  "canonizeResults",
+  "context",
+  "errorPolicy",
+  "fetchPolicy",
+  "refetchWritePolicy",
+  "returnPartialData",
+] as const;
+
+type ObservedOptions = Pick<
+  WatchQueryOptions,
+  typeof OBSERVED_CHANGED_OPTIONS[number]
+>;
+
+export class InternalQueryReference<TData = unknown> {
+  public result: ApolloQueryResult<TData>;
+  public readonly key: CacheKey;
+  public readonly observable: ObservableQuery<TData>;
+
+  public promiseCache?: Map<CacheKey, Promise<ApolloQueryResult<TData>>>;
+  public promise: Promise<ApolloQueryResult<TData>>;
+
+  private subscription: ObservableSubscription;
+  private listeners = new Set<Listener<TData>>();
+  private autoDisposeTimeoutId: NodeJS.Timeout;
+  private status: "idle" | "loading" = "loading";
+
+  private resolve: ((result: ApolloQueryResult<TData>) => void) | undefined;
+  private reject: ((error: unknown) => void) | undefined;
+
+  private references = 0;
+
+  constructor(
+    observable: ObservableQuery<TData>,
+    options: InternalQueryReferenceOptions
+  ) {
+    this.handleNext = this.handleNext.bind(this);
+    this.handleError = this.handleError.bind(this);
+    this.dispose = this.dispose.bind(this);
+    this.observable = observable;
+    // Don't save this result as last result to prevent delivery of last result
+    // when first subscribing
+    this.result = observable.getCurrentResult(false);
+    this.key = options.key;
+
+    if (options.onDispose) {
+      this.onDispose = options.onDispose;
+    }
+
+    if (
+      isNetworkRequestSettled(this.result.networkStatus) ||
+      (this.result.data &&
+        (!this.result.partial || this.watchQueryOptions.returnPartialData))
+    ) {
+      this.promise = createFulfilledPromise(this.result);
+      this.status = "idle";
+    } else {
+      this.promise = new Promise((resolve, reject) => {
+        this.resolve = resolve;
+        this.reject = reject;
+      });
+    }
+
+    this.subscription = observable
+      .filter(({ data }) => !equal(data, {}))
+      .subscribe({
+        next: this.handleNext,
+        error: this.handleError,
+      });
+
+    // Start a timer that will automatically dispose of the query if the
+    // suspended resource does not use this queryRef in the given time. This
+    // helps prevent memory leaks when a component has unmounted before the
+    // query has finished loading.
+    this.autoDisposeTimeoutId = setTimeout(
+      this.dispose,
+      options.autoDisposeTimeoutMs ?? 30_000
+    );
+  }
+
+  get watchQueryOptions() {
+    return this.observable.options;
+  }
+
+  retain() {
+    this.references++;
+    clearTimeout(this.autoDisposeTimeoutId);
+    let disposed = false;
+
+    return () => {
+      if (disposed) {
+        return;
+      }
+
+      disposed = true;
+      this.references--;
+
+      // Wait before fully disposing in case the app is running in strict mode.
+      setTimeout(() => {
+        if (!this.references) {
+          this.dispose();
+        }
+      });
+    };
+  }
+
+  didChangeOptions(watchQueryOptions: ObservedOptions) {
+    return OBSERVED_CHANGED_OPTIONS.some(
+      (option) =>
+        !equal(this.watchQueryOptions[option], watchQueryOptions[option])
+    );
+  }
+
+  applyOptions(watchQueryOptions: ObservedOptions) {
+    const {
+      fetchPolicy: currentFetchPolicy,
+      canonizeResults: currentCanonizeResults,
+    } = this.watchQueryOptions;
+
+    // "standby" is used when `skip` is set to `true`. Detect when we've
+    // enabled the query (i.e. `skip` is `false`) to execute a network request.
+    if (
+      currentFetchPolicy === "standby" &&
+      currentFetchPolicy !== watchQueryOptions.fetchPolicy
+    ) {
+      this.initiateFetch(this.observable.reobserve(watchQueryOptions));
+    } else {
+      this.observable.silentSetOptions(watchQueryOptions);
+
+      if (currentCanonizeResults !== watchQueryOptions.canonizeResults) {
+        this.result = { ...this.result, ...this.observable.getCurrentResult() };
+        this.promise = createFulfilledPromise(this.result);
+      }
+    }
+
+    return this.promise;
+  }
+
+  listen(listener: Listener<TData>) {
+    this.listeners.add(listener);
+
+    return () => {
+      this.listeners.delete(listener);
+    };
+  }
+
+  refetch(variables: OperationVariables | undefined) {
+    return this.initiateFetch(this.observable.refetch(variables));
+  }
+
+  fetchMore(options: FetchMoreOptions<TData>) {
+    return this.initiateFetch(this.observable.fetchMore<TData>(options));
+  }
+
+  private dispose() {
+    this.subscription.unsubscribe();
+    this.onDispose();
+  }
+
+  private onDispose() {
+    // noop. overridable by options
+  }
+
+  private handleNext(result: ApolloQueryResult<TData>) {
+    switch (this.status) {
+      case "loading": {
+        // Maintain the last successful `data` value if the next result does not
+        // have one.
+        if (result.data === void 0) {
+          result.data = this.result.data;
+        }
+        this.status = "idle";
+        this.result = result;
+        this.resolve?.(result);
+        break;
+      }
+      case "idle": {
+        // This occurs when switching to a result that is fully cached when this
+        // class is instantiated. ObservableQuery will run reobserve when
+        // subscribing, which delivers a result from the cache.
+        if (result.data === this.result.data) {
+          return;
+        }
+
+        // Maintain the last successful `data` value if the next result does not
+        // have one.
+        if (result.data === void 0) {
+          result.data = this.result.data;
+        }
+
+        this.result = result;
+        this.promise = createFulfilledPromise(result);
+        this.deliver(this.promise);
+        break;
+      }
+    }
+  }
+
+  private handleError(error: ApolloError) {
+    switch (this.status) {
+      case "loading": {
+        this.status = "idle";
+        this.reject?.(error);
+        break;
+      }
+      case "idle": {
+        this.promise = createRejectedPromise(error);
+        this.deliver(this.promise);
+      }
+    }
+  }
+
+  private deliver(promise: Promise<ApolloQueryResult<TData>>) {
+    this.listeners.forEach((listener) => listener(promise));
+  }
+
+  private initiateFetch(returnedPromise: Promise<ApolloQueryResult<TData>>) {
+    this.status = "loading";
+
+    this.promise = new Promise((resolve, reject) => {
+      this.resolve = resolve;
+      this.reject = reject;
+    });
+
+    this.promise.catch(() => {});
+
+    // If the data returned from the fetch is deeply equal to the data already
+    // in the cache, `handleNext` will not be triggered leaving the promise we
+    // created in a pending state forever. To avoid this situtation, we attempt
+    // to resolve the promise if `handleNext` hasn't been run to ensure the
+    // promise is resolved correctly.
+    returnedPromise
+      .then((result) => {
+        if (this.status === "loading") {
+          this.status = "idle";
+          this.result = result;
+          this.resolve?.(result);
+        }
+      })
+      .catch(() => {});
+
+    return returnedPromise;
+  }
+}
diff --git a/src/react/cache/SuspenseCache.ts b/src/react/cache/SuspenseCache.ts
new file mode 100644
--- /dev/null
+++ b/src/react/cache/SuspenseCache.ts
@@ -0,0 +1,49 @@
+import { Trie } from "@wry/trie";
+import type { ObservableQuery } from "../../core/index.js";
+import { canUseWeakMap } from "../../utilities/index.js";
+import { InternalQueryReference } from "./QueryReference.js";
+import type { CacheKey } from "./types.js";
+
+export interface SuspenseCacheOptions {
+  /**
+   * Specifies the amount of time, in milliseconds, the suspense cache will wait
+   * for a suspended component to read from the suspense cache before it
+   * automatically disposes of the query. This prevents memory leaks when a
+   * component unmounts before a suspended resource finishes loading. Increase
+   * the timeout if your queries take longer than than the specified time to
+   * prevent your queries from suspending over and over.
+   *
+   * Defaults to 30 seconds.
+   */
+  autoDisposeTimeoutMs?: number;
+}
+
+export class SuspenseCache {
+  private queryRefs = new Trie<{ current?: InternalQueryReference }>(
+    canUseWeakMap
+  );
+  private options: SuspenseCacheOptions;
+
+  constructor(options: SuspenseCacheOptions = Object.create(null)) {
+    this.options = options;
+  }
+
+  getQueryRef<TData = any>(
+    cacheKey: CacheKey,
+    createObservable: () => ObservableQuery<TData>
+  ) {
+    const ref = this.queryRefs.lookupArray(cacheKey);
+
+    if (!ref.current) {
+      ref.current = new InternalQueryReference(createObservable(), {
+        key: cacheKey,
+        autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs,
+        onDispose: () => {
+          delete ref.current;
+        },
+      });
+    }
+
+    return ref.current as InternalQueryReference<TData>;
+  }
+}
diff --git a/src/react/cache/getSuspenseCache.ts b/src/react/cache/getSuspenseCache.ts
new file mode 100644
--- /dev/null
+++ b/src/react/cache/getSuspenseCache.ts
@@ -0,0 +1,27 @@
+import type { SuspenseCacheOptions } from "./index.js";
+import { SuspenseCache } from "./SuspenseCache.js";
+import type { ApolloClient } from "../../core/ApolloClient.js";
+
+declare module "../../core/ApolloClient.js" {
+  interface DefaultOptions {
+    react?: {
+      suspense?: Readonly<SuspenseCacheOptions>;
+    };
+  }
+}
+
+const suspenseCacheSymbol = Symbol.for("apollo.suspenseCache");
+
+export function getSuspenseCache(
+  client: ApolloClient<object> & {
+    [suspenseCacheSymbol]?: SuspenseCache;
+  }
+) {
+  if (!client[suspenseCacheSymbol]) {
+    client[suspenseCacheSymbol] = new SuspenseCache(
+      client.defaultOptions.react?.suspense
+    );
+  }
+
+  return client[suspenseCacheSymbol];
+}
diff --git a/src/react/cache/index.ts b/src/react/cache/index.ts
new file mode 100644
--- /dev/null
+++ b/src/react/cache/index.ts
@@ -0,0 +1,27 @@
+export type { SuspenseCacheOptions } from "./SuspenseCache.js";
+export { getSuspenseCache } from "./getSuspenseCache.js";
+
+import { SuspenseCache as RealSuspenseCache } from "./SuspenseCache.js";
+
+// TODO: remove export with release 3.8
+// replace with
+// export type { SuspenseCache } from './SuspenseCache.js';
+/**
+ * @deprecated
+ * It is no longer necessary to create a `SuspenseCache` instance and pass it into the `ApolloProvider`.
+ * Please remove this code from your application.
+ *
+ * This export will be removed with the final 3.8 release.
+ */
+export class SuspenseCache extends RealSuspenseCache {
+  constructor() {
+    super();
+    // throwing an error here instead of using invariant - we do not want this error
+    // message to be link-ified, but to directly show up as bold as possible
+    throw new Error(
+      "It is no longer necessary to create a `SuspenseCache` instance and pass it into the `ApolloProvider`.\n" +
+        "Please remove this code from your application. \n\n" +
+        "This export will be removed with the final 3.8 release."
+    );
+  }
+}
diff --git a/src/react/cache/types.ts b/src/react/cache/types.ts
new file mode 100644
--- /dev/null
+++ b/src/react/cache/types.ts
@@ -0,0 +1,7 @@
+import type { DocumentNode } from "graphql";
+
+export type CacheKey = [
+  query: DocumentNode,
+  stringifiedVariables: string,
+  ...queryKey: any[],
+];
diff --git a/src/react/components/index.ts b/src/react/components/index.ts
--- a/src/react/components/index.ts
+++ b/src/react/components/index.ts
@@ -1,5 +1,5 @@
-export { Query } from './Query';
-export { Mutation } from './Mutation';
-export { Subscription } from './Subscription';
+export { Query } from "./Query.js";
+export { Mutation } from "./Mutation.js";
+export { Subscription } from "./Subscription.js";
 
-export * from './types';
+export * from "./types.js";
diff --git a/src/react/components/types.ts b/src/react/components/types.ts
--- a/src/react/components/types.ts
+++ b/src/react/components/types.ts
@@ -1,20 +1,24 @@
-import { DocumentNode } from 'graphql';
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import type { DocumentNode } from "graphql";
+import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
 
-import { OperationVariables, DefaultContext, ApolloCache } from '../../core';
-import {
+import type {
+  OperationVariables,
+  DefaultContext,
+  ApolloCache,
+} from "../../core/index.js";
+import type {
   QueryFunctionOptions,
   QueryResult,
   BaseMutationOptions,
   MutationFunction,
   MutationResult,
   BaseSubscriptionOptions,
-  SubscriptionResult
-} from '../types/types';
+  SubscriptionResult,
+} from "../types/types.js";
 
 export interface QueryComponentOptions<
   TData = any,
-  TVariables extends OperationVariables = OperationVariables
+  TVariables extends OperationVariables = OperationVariables,
 > extends QueryFunctionOptions<TData, TVariables> {
   children: (result: QueryResult<TData, TVariables>) => JSX.Element | null;
   query: DocumentNode | TypedDocumentNode<TData, TVariables>;
@@ -24,7 +28,7 @@ export interface MutationComponentOptions<
   TData = any,
   TVariables = OperationVariables,
   TContext = DefaultContext,
-  TCache extends ApolloCache<any> = ApolloCache<any>
+  TCache extends ApolloCache<any> = ApolloCache<any>,
 > extends BaseMutationOptions<TData, TVariables, TContext, TCache> {
   mutation: DocumentNode | TypedDocumentNode<TData, TVariables>;
   children: (
@@ -35,7 +39,7 @@ export interface MutationComponentOptions<
 
 export interface SubscriptionComponentOptions<
   TData = any,
-  TVariables extends OperationVariables = OperationVariables
+  TVariables extends OperationVariables = OperationVariables,
 > extends BaseSubscriptionOptions<TData, TVariables> {
   subscription: DocumentNode | TypedDocumentNode<TData, TVariables>;
   children?: null | ((result: SubscriptionResult<TData>) => JSX.Element | null);
diff --git a/src/react/context/ApolloContext.ts b/src/react/context/ApolloContext.ts
--- a/src/react/context/ApolloContext.ts
+++ b/src/react/context/ApolloContext.ts
@@ -1,7 +1,8 @@
-import * as React from 'react';
-import { ApolloClient } from '../../core';
-import { canUseSymbol } from '../../utilities';
-import type { RenderPromises } from '../ssr';
+import * as React from "react";
+import type { ApolloClient } from "../../core/index.js";
+import { canUseSymbol } from "../../utilities/index.js";
+import type { RenderPromises } from "../ssr/index.js";
+import { invariant } from "../../utilities/globals/index.js";
 
 export interface ApolloContextValue {
   client?: ApolloClient<object>;
@@ -13,21 +14,37 @@ export interface ApolloContextValue {
 // in one context, then attempting to retrieve it from another different
 // context), a single Apollo context is created and tracked in global state.
 const contextKey = canUseSymbol
-  ? Symbol.for('__APOLLO_CONTEXT__')
-  : '__APOLLO_CONTEXT__';
+  ? Symbol.for("__APOLLO_CONTEXT__")
+  : "__APOLLO_CONTEXT__";
 
 export function getApolloContext(): React.Context<ApolloContextValue> {
-  let context = (React.createContext as any)[contextKey] as React.Context<ApolloContextValue>;
+  invariant(
+    "createContext" in React,
+    "Invoking `getApolloContext` in an environment where `React.createContext` is not available.\n" +
+      "The Apollo Client functionality you are trying to use is only available in React Client Components.\n" +
+      'Please make sure to add "use client" at the top of your file.\n' +
+      // TODO: change to React documentation once React documentation contains information about Client Components
+      "For more information, see https://nextjs.org/docs/getting-started/react-essentials#client-components"
+  );
+
+  let context = (React.createContext as any)[
+    contextKey
+  ] as React.Context<ApolloContextValue>;
   if (!context) {
     Object.defineProperty(React.createContext, contextKey, {
-      value: context = React.createContext<ApolloContextValue>({}),
+      value: (context = React.createContext<ApolloContextValue>({})),
       enumerable: false,
       writable: false,
       configurable: true,
     });
-    context.displayName = 'ApolloContext';
+    context.displayName = "ApolloContext";
   }
   return context;
 }
 
-export { getApolloContext as resetApolloContext }
+/**
+ * @deprecated This function has no "resetting" effect since Apollo Client 3.4.12,
+ * and will be removed in the next major version of Apollo Client.
+ * If you want to get the Apollo Context, use `getApolloContext` instead.
+ */
+export const resetApolloContext = getApolloContext;
diff --git a/src/react/context/index.ts b/src/react/context/index.ts
--- a/src/react/context/index.ts
+++ b/src/react/context/index.ts
@@ -1,9 +1,8 @@
-import '../../utilities/globals';
+import "../../utilities/globals/index.js";
 
-export { ApolloConsumer, ApolloConsumerProps } from './ApolloConsumer';
-export {
-  ApolloContextValue,
-  getApolloContext,
-  getApolloContext as resetApolloContext
-} from './ApolloContext';
-export { ApolloProvider, ApolloProviderProps } from './ApolloProvider';
+export type { ApolloConsumerProps } from "./ApolloConsumer.js";
+export { ApolloConsumer } from "./ApolloConsumer.js";
+export type { ApolloContextValue } from "./ApolloContext.js";
+export { getApolloContext, resetApolloContext } from "./ApolloContext.js";
+export type { ApolloProviderProps } from "./ApolloProvider.js";
+export { ApolloProvider } from "./ApolloProvider.js";
diff --git a/src/react/hoc/index.ts b/src/react/hoc/index.ts
--- a/src/react/hoc/index.ts
+++ b/src/react/hoc/index.ts
@@ -1,10 +1,10 @@
-import '../../utilities/globals';
+import "../../utilities/globals/index.js";
 
-export { graphql } from './graphql';
+export { graphql } from "./graphql.js";
 
-export { withQuery } from './query-hoc';
-export { withMutation } from './mutation-hoc';
-export { withSubscription } from './subscription-hoc';
-export { withApollo } from './withApollo';
+export { withQuery } from "./query-hoc.js";
+export { withMutation } from "./mutation-hoc.js";
+export { withSubscription } from "./subscription-hoc.js";
+export { withApollo } from "./withApollo.js";
 
-export * from './types';
+export * from "./types.js";
diff --git a/src/react/hoc/types.ts b/src/react/hoc/types.ts
--- a/src/react/hoc/types.ts
+++ b/src/react/hoc/types.ts
@@ -1,6 +1,6 @@
-import { ApolloCache, ApolloClient } from '../../core';
-import { ApolloError } from '../../errors';
-import {
+import type { ApolloCache, ApolloClient } from "../../core/index.js";
+import type { ApolloError } from "../../errors/index.js";
+import type {
   ApolloQueryResult,
   OperationVariables,
   FetchMoreOptions,
@@ -8,17 +8,17 @@ import {
   FetchMoreQueryOptions,
   SubscribeToMoreOptions,
   DefaultContext,
-} from '../../core';
-import {
+} from "../../core/index.js";
+import type {
   MutationFunction,
   BaseQueryOptions,
   BaseMutationOptions,
-  MutationResult
-} from '../types/types';
+  MutationResult,
+} from "../types/types.js";
 
 export interface QueryControls<
   TData = any,
-  TGraphQLVariables = OperationVariables
+  TGraphQLVariables = OperationVariables,
 > {
   error?: ApolloError;
   networkStatus: number;
@@ -39,7 +39,7 @@ export interface QueryControls<
 
 export type DataValue<
   TData,
-  TGraphQLVariables = OperationVariables
+  TGraphQLVariables = OperationVariables,
 > = QueryControls<TData, TGraphQLVariables> &
   // data may not yet be loaded
   Partial<TData>;
@@ -50,7 +50,7 @@ export interface DataProps<TData, TGraphQLVariables = OperationVariables> {
 
 export interface MutateProps<
   TData = any,
-  TGraphQLVariables = OperationVariables
+  TGraphQLVariables = OperationVariables,
 > {
   mutate: MutationFunction<TData, TGraphQLVariables>;
   result: MutationResult<TData>;
@@ -59,7 +59,7 @@ export interface MutateProps<
 export type ChildProps<
   TProps = {},
   TData = {},
-  TGraphQLVariables = OperationVariables
+  TGraphQLVariables = OperationVariables,
 > = TProps &
   Partial<DataProps<TData, TGraphQLVariables>> &
   Partial<MutateProps<TData, TGraphQLVariables>>;
@@ -67,21 +67,20 @@ export type ChildProps<
 export type ChildDataProps<
   TProps = {},
   TData = {},
-  TGraphQLVariables = OperationVariables
+  TGraphQLVariables = OperationVariables,
 > = TProps & DataProps<TData, TGraphQLVariables>;
 
 export type ChildMutateProps<
   TProps = {},
   TData = {},
-  TGraphQLVariables = OperationVariables
+  TGraphQLVariables = OperationVariables,
 > = TProps & MutateProps<TData, TGraphQLVariables>;
 
 export interface OptionProps<
   TProps = any,
   TData = any,
-  TGraphQLVariables = OperationVariables
->
-  extends Partial<DataProps<TData, TGraphQLVariables>>,
+  TGraphQLVariables = OperationVariables,
+> extends Partial<DataProps<TData, TGraphQLVariables>>,
     Partial<MutateProps<TData, TGraphQLVariables>> {
   ownProps: TProps;
 }
@@ -101,8 +100,7 @@ export interface OperationOption<
         props: TProps
       ) =>
         | BaseQueryOptions<TGraphQLVariables>
-        | BaseMutationOptions<TData, TGraphQLVariables, TContext, TCache>
-      );
+        | BaseMutationOptions<TData, TGraphQLVariables, TContext, TCache>);
   props?: (
     props: OptionProps<TProps, TData, TGraphQLVariables>,
     lastProps?: TChildProps | void
diff --git a/src/react/hooks/constants.ts b/src/react/hooks/constants.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/constants.ts
@@ -0,0 +1,2 @@
+export const skipToken = Symbol.for('apollo.skipToken');
+export type SkipToken = typeof skipToken;
diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts
--- a/src/react/hooks/index.ts
+++ b/src/react/hooks/index.ts
@@ -1,9 +1,16 @@
-import '../../utilities/globals';
+import "../../utilities/globals/index.js";
 
-export * from './useApolloClient';
-export * from './useLazyQuery';
-export * from './useMutation';
-export { useQuery } from './useQuery';
-export * from './useSubscription';
-export * from './useReactiveVar';
-export * from './useFragment';
+export * from "./useApolloClient.js";
+export * from "./useLazyQuery.js";
+export * from "./useMutation.js";
+export { useQuery } from "./useQuery.js";
+export * from "./useSubscription.js";
+export * from "./useReactiveVar.js";
+export * from "./useFragment.js";
+export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js";
+export { useSuspenseQuery } from "./useSuspenseQuery.js";
+export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js";
+export { useBackgroundQuery } from "./useBackgroundQuery.js";
+export { useReadQuery } from "./useReadQuery.js";
+export { skipToken } from "./constants.js";
+export type { SkipToken } from "./constants.js";
diff --git a/src/react/hooks/internal/__use.ts b/src/react/hooks/internal/__use.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/internal/__use.ts
@@ -0,0 +1,26 @@
+import { wrapPromiseWithState } from "../../../utilities/index.js";
+import * as React from "react";
+
+type Use = <T>(promise: Promise<T>) => T;
+// Prevent webpack from complaining about our feature detection of the
+// use property of the React namespace, which is expected not
+// to exist when using current stable versions, and that's fine.
+const useKey = "use" as keyof typeof React;
+const realHook = React[useKey] as Use | undefined;
+
+// This is named with two underscores to allow this hook to evade typical rules of
+// hooks (i.e. it can be used conditionally)
+export const __use =
+  realHook ||
+  function __use<TValue>(promise: Promise<TValue>) {
+    const statefulPromise = wrapPromiseWithState(promise);
+
+    switch (statefulPromise.status) {
+      case "pending":
+        throw statefulPromise;
+      case "rejected":
+        throw statefulPromise.reason;
+      case "fulfilled":
+        return statefulPromise.value;
+    }
+  };
diff --git a/src/react/hooks/internal/index.ts b/src/react/hooks/internal/index.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/internal/index.ts
@@ -0,0 +1,4 @@
+// These hooks are used internally and are not exported publicly by the library
+export { useDeepMemo } from "./useDeepMemo.js";
+export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
+export { __use } from "./__use.js";
diff --git a/src/react/hooks/internal/useDeepMemo.ts b/src/react/hooks/internal/useDeepMemo.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/internal/useDeepMemo.ts
@@ -0,0 +1,16 @@
+import type { DependencyList } from "react";
+import * as React from "react";
+import { equal } from "@wry/equality";
+
+export function useDeepMemo<TValue>(
+  memoFn: () => TValue,
+  deps: DependencyList
+) {
+  const ref = React.useRef<{ deps: DependencyList; value: TValue }>();
+
+  if (!ref.current || !equal(ref.current.deps, deps)) {
+    ref.current = { value: memoFn(), deps };
+  }
+
+  return ref.current.value;
+}
diff --git a/src/react/hooks/internal/useIsomorphicLayoutEffect.ts b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts
@@ -0,0 +1,11 @@
+import * as React from "react";
+import { canUseDOM } from "../../../utilities/index.js";
+
+// use canUseDOM here instead of canUseLayoutEffect because we want to be able
+// to use useLayoutEffect in our jest tests. useLayoutEffect seems to work fine
+// in useSuspenseQuery tests, but to honor the original comment about the
+// warnings for useSyncExternalStore implementation, canUseLayoutEffect is left
+// alone.
+export const useIsomorphicLayoutEffect = canUseDOM
+  ? React.useLayoutEffect
+  : React.useEffect;
diff --git a/src/react/hooks/useApolloClient.ts b/src/react/hooks/useApolloClient.ts
--- a/src/react/hooks/useApolloClient.ts
+++ b/src/react/hooks/useApolloClient.ts
@@ -1,18 +1,18 @@
-import { invariant } from '../../utilities/globals';
-import { useContext } from 'react';
-import { ApolloClient } from '../../core';
-import { getApolloContext } from '../context';
+import { invariant } from "../../utilities/globals/index.js";
+import * as React from "react";
+import type { ApolloClient } from "../../core/index.js";
+import { getApolloContext } from "../context/index.js";
 
 export function useApolloClient(
-  override?: ApolloClient<object>,
+  override?: ApolloClient<object>
 ): ApolloClient<object> {
-  const context = useContext(getApolloContext());
+  const context = React.useContext(getApolloContext());
   const client = override || context.client;
   invariant(
     !!client,
     'Could not find "client" in the context or passed in as an option. ' +
-    'Wrap the root component in an <ApolloProvider>, or pass an ApolloClient ' +
-    'instance in via options.',
+      "Wrap the root component in an <ApolloProvider>, or pass an ApolloClient " +
+      "instance in via options."
   );
 
   return client;
diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useBackgroundQuery.ts
@@ -0,0 +1,250 @@
+import * as React from "react";
+import type {
+  DocumentNode,
+  OperationVariables,
+  TypedDocumentNode,
+} from "../../core/index.js";
+import { useApolloClient } from "./useApolloClient.js";
+import { wrapQueryRef, type QueryReference } from "../cache/QueryReference.js";
+import type { BackgroundQueryHookOptions, NoInfer } from "../types/types.js";
+import { __use } from "./internal/index.js";
+import { getSuspenseCache } from "../cache/index.js";
+import { useWatchQueryOptions } from "./useSuspenseQuery.js";
+import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js";
+import { canonicalStringify } from "../../cache/index.js";
+import type { DeepPartial } from "../../utilities/index.js";
+import type { CacheKey } from "../cache/types.js";
+import type { SkipToken } from "./constants.js";
+
+export type UseBackgroundQueryResult<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+> = {
+  fetchMore: FetchMoreFunction<TData, TVariables>;
+  refetch: RefetchFunction<TData, TVariables>;
+};
+
+type BackgroundQueryHookOptionsNoInfer<
+  TData,
+  TVariables extends OperationVariables
+> = BackgroundQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>>;
+
+export function useBackgroundQuery<
+  TData,
+  TVariables extends OperationVariables,
+  TOptions extends Omit<BackgroundQueryHookOptions<TData>, "variables">
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options?: BackgroundQueryHookOptionsNoInfer<TData, TVariables> & TOptions
+): [
+  (
+    | QueryReference<
+        TOptions["errorPolicy"] extends "ignore" | "all"
+          ? TOptions["returnPartialData"] extends true
+            ? DeepPartial<TData> | undefined
+            : TData | undefined
+          : TOptions["returnPartialData"] extends true
+          ? DeepPartial<TData>
+          : TData
+      >
+    | (TOptions["skip"] extends boolean ? undefined : never)
+  ),
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: BackgroundQueryHookOptionsNoInfer<TData, TVariables> & {
+    returnPartialData: true;
+    errorPolicy: "ignore" | "all";
+  }
+): [
+  QueryReference<DeepPartial<TData> | undefined>,
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: BackgroundQueryHookOptionsNoInfer<TData, TVariables> & {
+    errorPolicy: "ignore" | "all";
+  }
+): [
+  QueryReference<TData | undefined>,
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: BackgroundQueryHookOptionsNoInfer<TData, TVariables> & {
+    skip: boolean;
+    returnPartialData: true;
+  }
+): [
+  QueryReference<DeepPartial<TData>> | undefined,
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: BackgroundQueryHookOptionsNoInfer<TData, TVariables> & {
+    returnPartialData: true;
+  }
+): [
+  QueryReference<DeepPartial<TData>>,
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: BackgroundQueryHookOptionsNoInfer<TData, TVariables> & {
+    skip: boolean;
+  }
+): [
+  QueryReference<TData> | undefined,
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options?: BackgroundQueryHookOptionsNoInfer<TData, TVariables>
+): [QueryReference<TData>, UseBackgroundQueryResult<TData, TVariables>];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: SkipToken
+): [undefined, UseBackgroundQueryResult<TData, TVariables>];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options:
+    | SkipToken
+    | (BackgroundQueryHookOptionsNoInfer<TData, TVariables> & {
+        returnPartialData: true;
+      })
+): [
+  QueryReference<DeepPartial<TData>> | undefined,
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options?: SkipToken | BackgroundQueryHookOptionsNoInfer<TData, TVariables>
+): [
+  QueryReference<TData> | undefined,
+  UseBackgroundQueryResult<TData, TVariables>
+];
+
+export function useBackgroundQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options:
+    | (SkipToken &
+        Partial<BackgroundQueryHookOptionsNoInfer<TData, TVariables>>)
+    | BackgroundQueryHookOptionsNoInfer<TData, TVariables> = Object.create(null)
+): [
+  QueryReference<TData> | undefined,
+  UseBackgroundQueryResult<TData, TVariables>
+] {
+  const client = useApolloClient(options.client);
+  const suspenseCache = getSuspenseCache(client);
+  const watchQueryOptions = useWatchQueryOptions({ client, query, options });
+  const { fetchPolicy, variables } = watchQueryOptions;
+  const { queryKey = [] } = options;
+
+  // This ref tracks the first time query execution is enabled to determine
+  // whether to return a query ref or `undefined`. When initialized
+  // in a skipped state (either via `skip: true` or `skipToken`) we return
+  // `undefined` for the `queryRef` until the query has been enabled. Once
+  // enabled, a query ref is always returned regardless of whether the query is
+  // skipped again later.
+  const didFetchResult = React.useRef(fetchPolicy !== "standby");
+  didFetchResult.current ||= fetchPolicy !== "standby";
+
+  const cacheKey: CacheKey = [
+    query,
+    canonicalStringify(variables),
+    ...([] as any[]).concat(queryKey),
+  ];
+
+  const queryRef = suspenseCache.getQueryRef(cacheKey, () =>
+    client.watchQuery(watchQueryOptions)
+  );
+
+  const [promiseCache, setPromiseCache] = React.useState(
+    () => new Map([[queryRef.key, queryRef.promise]])
+  );
+
+  if (queryRef.didChangeOptions(watchQueryOptions)) {
+    const promise = queryRef.applyOptions(watchQueryOptions);
+    promiseCache.set(queryRef.key, promise);
+  }
+
+  React.useEffect(() => queryRef.retain(), [queryRef]);
+
+  const fetchMore: FetchMoreFunction<TData, TVariables> = React.useCallback(
+    (options) => {
+      const promise = queryRef.fetchMore(options);
+
+      setPromiseCache((promiseCache) =>
+        new Map(promiseCache).set(queryRef.key, queryRef.promise)
+      );
+
+      return promise;
+    },
+    [queryRef]
+  );
+
+  const refetch: RefetchFunction<TData, TVariables> = React.useCallback(
+    (variables) => {
+      const promise = queryRef.refetch(variables);
+
+      setPromiseCache((promiseCache) =>
+        new Map(promiseCache).set(queryRef.key, queryRef.promise)
+      );
+
+      return promise;
+    },
+    [queryRef]
+  );
+
+  queryRef.promiseCache = promiseCache;
+
+  const wrappedQueryRef = React.useMemo(
+    () => wrapQueryRef(queryRef),
+    [queryRef]
+  );
+
+  return [
+    didFetchResult.current ? wrappedQueryRef : void 0,
+    { fetchMore, refetch },
+  ];
+}
diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts
--- a/src/react/hooks/useFragment.ts
+++ b/src/react/hooks/useFragment.ts
@@ -1,87 +1,62 @@
-import { useRef } from "react";
+import * as React from "react";
 import { equal } from "@wry/equality";
 
-import { mergeDeepArray } from "../../utilities";
-import {
+import type { DeepPartial } from "../../utilities/index.js";
+import { mergeDeepArray } from "../../utilities/index.js";
+import type {
   Cache,
   Reference,
   StoreObject,
   MissingTree,
-} from "../../cache";
+} from "../../cache/index.js";
 
-import { useApolloClient } from "./useApolloClient";
-import { useSyncExternalStore } from "./useSyncExternalStore";
-import { OperationVariables } from "../../core";
+import { useApolloClient } from "./useApolloClient.js";
+import { useSyncExternalStore } from "./useSyncExternalStore.js";
+import type { OperationVariables } from "../../core/index.js";
+import type { NoInfer } from "../types/types.js";
 
 export interface UseFragmentOptions<TData, TVars>
-extends Omit<
-  Cache.DiffOptions<TData, TVars>,
-  | "id"
-  | "query"
-  | "optimistic"
-  | "previousResult"
-  | "returnPartialData"
->, Omit<Cache.ReadFragmentOptions<TData, TVars>,
-  | "id"
-  | "returnPartialData"
-> {
+  extends Omit<
+      Cache.DiffOptions<NoInfer<TData>, NoInfer<TVars>>,
+      "id" | "query" | "optimistic" | "previousResult" | "returnPartialData"
+    >,
+    Omit<
+      Cache.ReadFragmentOptions<TData, TVars>,
+      "id" | "variables" | "returnPartialData"
+    > {
   from: StoreObject | Reference | string;
   // Override this field to make it optional (default: true).
   optimistic?: boolean;
-
-  /**
-   * Whether to return incomplete data rather than null.
-   * Defaults to `true`.
-   * @deprecated This option will be removed in Apollo Client 3.8.
-   * Please check `result.missing` instead.
-   */
-  returnPartialData?: boolean;
 }
 
-// Since the above definition of UseFragmentOptions can be hard to parse without
-// help from TypeScript/VSCode, here are the intended fields and their types.
-// Uncomment this code to check that it's consistent with the definition above.
-//
-// export interface UseFragmentOptions<TData, TVars> {
-//   from: string | StoreObject | Reference;
-//   fragment: DocumentNode | TypedDocumentNode<TData, TVars>;
-//   fragmentName?: string;
-//   optimistic?: boolean;
-//   variables?: TVars;
-//   returnPartialData?: boolean;
-//   canonizeResults?: boolean;
-// }
-
-export interface UseFragmentResult<TData> {
-  data: TData | undefined;
-  complete: boolean;
-  missing?: MissingTree;
-}
+export type UseFragmentResult<TData> =
+  | {
+      data: TData;
+      complete: true;
+      missing?: never;
+    }
+  | {
+      data: DeepPartial<TData>;
+      complete: false;
+      missing?: MissingTree;
+    };
 
-export function useFragment_experimental<
-  TData = any,
-  TVars = OperationVariables
->(
-  options: UseFragmentOptions<TData, TVars>,
+export function useFragment<TData = any, TVars = OperationVariables>(
+  options: UseFragmentOptions<TData, TVars>
 ): UseFragmentResult<TData> {
   const { cache } = useApolloClient();
 
-  const {
-    fragment,
-    fragmentName,
-    from,
-    optimistic = true,
-    ...rest
-  } = options;
+  const { fragment, fragmentName, from, optimistic = true, ...rest } = options;
 
   const diffOptions: Cache.DiffOptions<TData, TVars> = {
     ...rest,
+    returnPartialData: true,
     id: typeof from === "string" ? from : cache.identify(from),
     query: cache["getFragmentDoc"](fragment, fragmentName),
     optimistic,
   };
 
-  const resultRef = useRef<UseFragmentResult<TData>>();
+  const resultRef = React.useRef<UseFragmentResult<TData>>();
   let latestDiff = cache.diff<TData>(diffOptions);
 
   // Used for both getSnapshot and getServerSnapshot
@@ -95,16 +70,21 @@ export function useFragment_experimental<
 
   return useSyncExternalStore(
     (forceUpdate) => {
-      return cache.watch({
+      let lastTimeout = 0;
+      const unsubcribe = cache.watch({
         ...diffOptions,
         immediate: true,
         callback(diff) {
           if (!equal(diff, latestDiff)) {
             resultRef.current = diffToResult((latestDiff = diff));
-            forceUpdate();
+            lastTimeout = setTimeout(forceUpdate) as any;
           }
         },
       });
+      return () => {
+        unsubcribe();
+        clearTimeout(lastTimeout);
+      };
     },
     getSnapshot,
     getSnapshot
@@ -112,17 +92,15 @@ export function useFragment_experimental<
 }
 
 function diffToResult<TData>(
-  diff: Cache.DiffResult<TData>,
+  diff: Cache.DiffResult<TData>
 ): UseFragmentResult<TData> {
-  const result: UseFragmentResult<TData> = {
-    data: diff.result,
+  const result = {
+    data: diff.result!,
     complete: !!diff.complete,
-  };
+  } as UseFragmentResult<TData>;
 
   if (diff.missing) {
-    result.missing = mergeDeepArray(
-      diff.missing.map(error => error.missing),
-    );
+    result.missing = mergeDeepArray(diff.missing.map((error) => error.missing));
   }
 
   return result;
diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts
--- a/src/react/hooks/useLazyQuery.ts
+++ b/src/react/hooks/useLazyQuery.ts
@@ -1,39 +1,47 @@
-import { DocumentNode } from 'graphql';
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { useCallback, useMemo, useRef } from 'react';
-
-import { OperationVariables } from '../../core';
-import { mergeOptions } from '../../utilities';
-import {
+import type { DocumentNode } from "graphql";
+import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
+import * as React from "react";
+
+import type { OperationVariables } from "../../core/index.js";
+import { mergeOptions } from "../../utilities/index.js";
+import type {
+  LazyQueryHookExecOptions,
   LazyQueryHookOptions,
   LazyQueryResultTuple,
+  NoInfer,
   QueryResult,
-} from '../types/types';
-import { useInternalState } from './useQuery';
-import { useApolloClient } from './useApolloClient';
+} from "../types/types.js";
+import { useInternalState } from "./useQuery.js";
+import { useApolloClient } from "./useApolloClient.js";
 
 // The following methods, when called will execute the query, regardless of
 // whether the useLazyQuery execute function was called before.
 const EAGER_METHODS = [
-  'refetch',
-  'reobserve',
-  'fetchMore',
-  'updateQuery',
-  'startPolling',
-  'subscribeToMore',
+  "refetch",
+  "reobserve",
+  "fetchMore",
+  "updateQuery",
+  "startPolling",
+  "subscribeToMore",
 ] as const;
 
-export function useLazyQuery<TData = any, TVariables extends OperationVariables = OperationVariables>(
+export function useLazyQuery<
+  TData = any,
+  TVariables extends OperationVariables = OperationVariables,
+>(
   query: DocumentNode | TypedDocumentNode<TData, TVariables>,
-  options?: LazyQueryHookOptions<TData, TVariables>
+  options?: LazyQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>>
 ): LazyQueryResultTuple<TData, TVariables> {
-  const execOptionsRef = useRef<Partial<LazyQueryHookOptions<TData, TVariables>>>();
-  const optionsRef = useRef<LazyQueryHookOptions<TData, TVariables>>();
-  const queryRef = useRef<DocumentNode | TypedDocumentNode<TData, TVariables>>();
-  const merged = execOptionsRef.current ? mergeOptions(options, execOptionsRef.current) : options;
+  const execOptionsRef =
+    React.useRef<Partial<LazyQueryHookExecOptions<TData, TVariables>>>();
+  const optionsRef = React.useRef<LazyQueryHookOptions<TData, TVariables>>();
+  const queryRef = React.useRef<
+    DocumentNode | TypedDocumentNode<TData, TVariables>
+  >();
+  const merged = mergeOptions(options, execOptionsRef.current || {});
   const document = merged?.query ?? query;
 
-  // Use refs to track options and the used query to ensure the `execute` 
+  // Use refs to track options and the used query to ensure the `execute`
   // function remains referentially stable between renders.
   optionsRef.current = merged;
   queryRef.current = document;
@@ -52,13 +60,12 @@ export function useLazyQuery<TData = any, TVariables extends OperationVariables
     useQueryResult.observable.options.initialFetchPolicy ||
     internalState.getDefaultFetchPolicy();
 
-  const result: QueryResult<TData, TVariables> =
-    Object.assign(useQueryResult, {
-      called: !!execOptionsRef.current,
-    });
+  const result: QueryResult<TData, TVariables> = Object.assign(useQueryResult, {
+    called: !!execOptionsRef.current,
+  });
 
   // We use useMemo here to make sure the eager methods have a stable identity.
-  const eagerMethods = useMemo(() => {
+  const eagerMethods = React.useMemo(() => {
     const eagerMethods: Record<string, any> = {};
     for (const key of EAGER_METHODS) {
       const method = result[key];
@@ -66,7 +73,7 @@ export function useLazyQuery<TData = any, TVariables extends OperationVariables
         if (!execOptionsRef.current) {
           execOptionsRef.current = Object.create(null);
           // Only the first time populating execOptionsRef.current matters here.
-          internalState.forceUpdate();
+          internalState.forceUpdateState();
         }
         return method.apply(this, arguments);
       };
@@ -77,31 +84,34 @@ export function useLazyQuery<TData = any, TVariables extends OperationVariables
 
   Object.assign(result, eagerMethods);
 
-  const execute = useCallback<
-    LazyQueryResultTuple<TData, TVariables>[0]
-  >(executeOptions => {
-    execOptionsRef.current = executeOptions ? {
-      ...executeOptions,
-      fetchPolicy: executeOptions.fetchPolicy || initialFetchPolicy,
-    } : {
-      fetchPolicy: initialFetchPolicy,
-    };
-
-    const options = mergeOptions(optionsRef.current, {
-      query: queryRef.current,
-      ...execOptionsRef.current,
-    })
-
-    const promise = internalState
-      .executeQuery({ ...options, skip: false }) 
-      .then((queryResult) => Object.assign(queryResult, eagerMethods));
-
-    // Because the return value of `useLazyQuery` is usually floated, we need
-    // to catch the promise to prevent unhandled rejections.
-    promise.catch(() => {});
-
-    return promise;
-  }, []);
+  const execute = React.useCallback<LazyQueryResultTuple<TData, TVariables>[0]>(
+    (executeOptions) => {
+      execOptionsRef.current = executeOptions
+        ? {
+            ...executeOptions,
+            fetchPolicy: executeOptions.fetchPolicy || initialFetchPolicy,
+          }
+        : {
+            fetchPolicy: initialFetchPolicy,
+          };
+
+      const options = mergeOptions(optionsRef.current, {
+        query: queryRef.current,
+        ...execOptionsRef.current,
+      });
+
+      const promise = internalState
+        .executeQuery({ ...options, skip: false })
+        .then((queryResult) => Object.assign(queryResult, eagerMethods));
+
+      // Because the return value of `useLazyQuery` is usually floated, we need
+      // to catch the promise to prevent unhandled rejections.
+      promise.catch(() => {});
+
+      return promise;
+    },
+    []
+  );
 
   return [execute, result];
 }
diff --git a/src/react/hooks/useMutation.ts b/src/react/hooks/useMutation.ts
--- a/src/react/hooks/useMutation.ts
+++ b/src/react/hooks/useMutation.ts
@@ -1,23 +1,24 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { DocumentNode } from 'graphql';
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import {
+import * as React from "react";
+import type { DocumentNode } from "graphql";
+import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
+import type {
   MutationFunctionOptions,
   MutationHookOptions,
   MutationResult,
   MutationTuple,
-} from '../types/types';
+  NoInfer,
+} from "../types/types.js";
 
-import {
+import type {
   ApolloCache,
   DefaultContext,
-  mergeOptions,
   OperationVariables,
-} from '../../core';
-import { equal } from '@wry/equality';
-import { DocumentType, verifyDocumentType } from '../parser';
-import { ApolloError } from '../../errors';
-import { useApolloClient } from './useApolloClient';
+} from "../../core/index.js";
+import { mergeOptions } from "../../utilities/index.js";
+import { equal } from "@wry/equality";
+import { DocumentType, verifyDocumentType } from "../parser/index.js";
+import { ApolloError } from "../../errors/index.js";
+import { useApolloClient } from "./useApolloClient.js";
 
 export function useMutation<
   TData = any,
@@ -26,17 +27,22 @@ export function useMutation<
   TCache extends ApolloCache<any> = ApolloCache<any>,
 >(
   mutation: DocumentNode | TypedDocumentNode<TData, TVariables>,
-  options?: MutationHookOptions<TData, TVariables, TContext, TCache>,
+  options?: MutationHookOptions<
+    NoInfer<TData>,
+    NoInfer<TVariables>,
+    TContext,
+    TCache
+  >
 ): MutationTuple<TData, TVariables, TContext, TCache> {
   const client = useApolloClient(options?.client);
   verifyDocumentType(mutation, DocumentType.Mutation);
-  const [result, setResult] = useState<Omit<MutationResult, 'reset'>>({
+  const [result, setResult] = React.useState<Omit<MutationResult, "reset">>({
     called: false,
     loading: false,
     client,
   });
 
-  const ref = useRef({
+  const ref = React.useRef({
     result,
     mutationId: 0,
     isMounted: true,
@@ -51,100 +57,118 @@ export function useMutation<
     Object.assign(ref.current, { client, options, mutation });
   }
 
-  const execute = useCallback((
-    executeOptions: MutationFunctionOptions<
-      TData,
-      TVariables,
-      TContext,
-      TCache
-    > = {}
-  ) => {
-    const {options, mutation} = ref.current;
-    const baseOptions = { ...options, mutation };
-    const client = executeOptions.client || ref.current.client;
-
-    if (!ref.current.result.loading && !baseOptions.ignoreResults && ref.current.isMounted) {
-      setResult(ref.current.result = {
-        loading: true,
-        error: void 0,
-        data: void 0,
-        called: true,
-        client,
-      });
-    }
-
-    const mutationId = ++ref.current.mutationId;
-    const clientOptions = mergeOptions(
-      baseOptions,
-      executeOptions as any,
-    );
-
-    return client.mutate(clientOptions).then((response) => {
-      const { data, errors } = response;
-      const error =
-        errors && errors.length > 0
-          ? new ApolloError({ graphQLErrors: errors })
-          : void 0;
-
-      if (
-        mutationId === ref.current.mutationId &&
-        !clientOptions.ignoreResults
-      ) {
-        const result = {
-          called: true,
-          loading: false,
-          data,
-          error,
-          client,
-        };
-
-        if (ref.current.isMounted && !equal(ref.current.result, result)) {
-          setResult(ref.current.result = result);
-        }
-      }
+  const execute = React.useCallback(
+    (
+      executeOptions: MutationFunctionOptions<
+        TData,
+        TVariables,
+        TContext,
+        TCache
+      > = {}
+    ) => {
+      const { options, mutation } = ref.current;
+      const baseOptions = { ...options, mutation };
+      const client = executeOptions.client || ref.current.client;
 
-      const onCompleted = executeOptions.onCompleted || ref.current.options?.onCompleted
-      onCompleted?.(response.data!, clientOptions);
-
-      return response;
-    }).catch((error) => {
       if (
-        mutationId === ref.current.mutationId &&
+        !ref.current.result.loading &&
+        !baseOptions.ignoreResults &&
         ref.current.isMounted
       ) {
-        const result = {
-          loading: false,
-          error,
-          data: void 0,
-          called: true,
-          client,
-        };
-
-        if (!equal(ref.current.result, result)) {
-          setResult(ref.current.result = result);
-        }
+        setResult(
+          (ref.current.result = {
+            loading: true,
+            error: void 0,
+            data: void 0,
+            called: true,
+            client,
+          })
+        );
       }
 
-      const onError = executeOptions.onError || ref.current.options?.onError
-
-      if (onError) {
-        onError(error, clientOptions);
-
-        // TODO(brian): why are we returning this here???
-        return { data: void 0, errors: error };
-      }
-
-      throw error;
-    });
-  }, []);
-
-  const reset = useCallback(() => {
+      const mutationId = ++ref.current.mutationId;
+      const clientOptions = mergeOptions(baseOptions, executeOptions as any);
+
+      return client
+        .mutate(clientOptions)
+        .then((response) => {
+          const { data, errors } = response;
+          const error =
+            errors && errors.length > 0
+              ? new ApolloError({ graphQLErrors: errors })
+              : void 0;
+
+          const onError =
+            executeOptions.onError || ref.current.options?.onError;
+
+          if (error && onError) {
+            onError(error, clientOptions);
+          }
+
+          if (
+            mutationId === ref.current.mutationId &&
+            !clientOptions.ignoreResults
+          ) {
+            const result = {
+              called: true,
+              loading: false,
+              data,
+              error,
+              client,
+            };
+
+            if (ref.current.isMounted && !equal(ref.current.result, result)) {
+              setResult((ref.current.result = result));
+            }
+          }
+
+          const onCompleted =
+            executeOptions.onCompleted || ref.current.options?.onCompleted;
+
+          if (!error) {
+            onCompleted?.(response.data!, clientOptions);
+          }
+
+          return response;
+        })
+        .catch((error) => {
+          if (mutationId === ref.current.mutationId && ref.current.isMounted) {
+            const result = {
+              loading: false,
+              error,
+              data: void 0,
+              called: true,
+              client,
+            };
+
+            if (!equal(ref.current.result, result)) {
+              setResult((ref.current.result = result));
+            }
+          }
+
+          const onError =
+            executeOptions.onError || ref.current.options?.onError;
+
+          if (onError) {
+            onError(error, clientOptions);
+
+            // TODO(brian): why are we returning this here???
+            return { data: void 0, errors: error };
+          }
+
+          throw error;
+        });
+    },
+    []
+  );
+
+  const reset = React.useCallback(() => {
     if (ref.current.isMounted) {
       setResult({ called: false, loading: false, client });
     }
   }, []);
 
-  useEffect(() => {
+  React.useEffect(() => {
     ref.current.isMounted = true;
 
     return () => {
diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts
--- a/src/react/hooks/useQuery.ts
+++ b/src/react/hooks/useQuery.ts
@@ -1,41 +1,44 @@
-import { invariant } from '../../utilities/globals';
-
-import {
-  useCallback,
-  useContext,
-  useMemo,
-  useRef,
-  useState,
-} from 'react';
-import { useSyncExternalStore } from './useSyncExternalStore';
-import { equal } from '@wry/equality';
-
-import { mergeOptions, OperationVariables, WatchQueryFetchPolicy } from '../../core';
-import { ApolloContextValue, getApolloContext } from '../context';
-import { ApolloError } from '../../errors';
-import {
+import { invariant } from "../../utilities/globals/index.js";
+
+import * as React from "react";
+import { useSyncExternalStore } from "./useSyncExternalStore.js";
+import { equal } from "@wry/equality";
+
+import type {
+  OperationVariables,
+  WatchQueryFetchPolicy,
+} from "../../core/index.js";
+import { mergeOptions } from "../../utilities/index.js";
+import type { ApolloContextValue } from "../context/index.js";
+import { getApolloContext } from "../context/index.js";
+import { ApolloError } from "../../errors/index.js";
+import type {
   ApolloClient,
   ApolloQueryResult,
-  NetworkStatus,
   ObservableQuery,
   DocumentNode,
   TypedDocumentNode,
   WatchQueryOptions,
-} from '../../core';
-import {
+} from "../../core/index.js";
+import { NetworkStatus } from "../../core/index.js";
+import type {
   QueryHookOptions,
   QueryResult,
   ObservableQueryFields,
-} from '../types/types';
+  NoInfer,
+} from "../types/types.js";
 
-import { DocumentType, verifyDocumentType } from '../parser';
-import { useApolloClient } from './useApolloClient';
-import { canUseWeakMap, compact, isNonEmptyArray, maybeDeepFreeze } from '../../utilities';
+import { DocumentType, verifyDocumentType } from "../parser/index.js";
+import { useApolloClient } from "./useApolloClient.js";
+import {
+  canUseWeakMap,
+  compact,
+  isNonEmptyArray,
+  maybeDeepFreeze,
+} from "../../utilities/index.js";
 
 const {
-  prototype: {
-    hasOwnProperty,
-  },
+  prototype: { hasOwnProperty },
 } = Object;
 
 export function useQuery<
@@ -43,19 +46,21 @@ export function useQuery<
   TVariables extends OperationVariables = OperationVariables,
 >(
   query: DocumentNode | TypedDocumentNode<TData, TVariables>,
-  options: QueryHookOptions<TData, TVariables> = Object.create(null),
+  options: QueryHookOptions<
+    NoInfer<TData>,
+    NoInfer<TVariables>
+  > = Object.create(null)
 ): QueryResult<TData, TVariables> {
-  return useInternalState(
-    useApolloClient(options.client),
-    query,
-  ).useQuery(options);
+  return useInternalState(useApolloClient(options.client), query).useQuery(
+    options
+  );
 }
 
 export function useInternalState<TData, TVariables extends OperationVariables>(
   client: ApolloClient<any>,
-  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>
 ): InternalState<TData, TVariables> {
-  const stateRef = useRef<InternalState<TData, TVariables>>();
+  const stateRef = React.useRef<InternalState<TData, TVariables>>();
   if (
     !stateRef.current ||
     client !== stateRef.current.client ||
@@ -71,10 +76,7 @@ export function useInternalState<TData, TVariables extends OperationVariables>(
   // setTick function. Updating this state by calling state.forceUpdate is the
   // only way we trigger React component updates (no other useState calls within
   // the InternalState class).
-  const [_tick, setTick] = useState(0);
-  state.forceUpdate = () => {
-    setTick(tick => tick + 1);
-  };
+  state.forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1];
 
   return state;
 }
@@ -83,7 +85,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
   constructor(
     public readonly client: ReturnType<typeof useApolloClient>,
     public readonly query: DocumentNode | TypedDocumentNode<TData, TVariables>,
-    previous?: InternalState<TData, TVariables>,
+    previous?: InternalState<TData, TVariables>
   ) {
     verifyDocumentType(query, DocumentType.Query);
 
@@ -96,18 +98,37 @@ class InternalState<TData, TVariables extends OperationVariables> {
     }
   }
 
-  forceUpdate() {
+  /**
+   * Forces an update using local component state.
+   * As this is not batched with `useSyncExternalStore` updates,
+   * this is only used as a fallback if the `useSyncExternalStore` "force update"
+   * method is not registered at the moment.
+   * See https://github.com/facebook/react/issues/25191
+   *  */
+  forceUpdateState() {
     // Replaced (in useInternalState) with a method that triggers an update.
-    invariant.warn("Calling default no-op implementation of InternalState#forceUpdate");
+    invariant.warn(
+      "Calling default no-op implementation of InternalState#forceUpdate"
+    );
   }
 
-  executeQuery(options: QueryHookOptions<TData, TVariables>) {
+  /**
+   * Will be overwritten by the `useSyncExternalStore` "force update" method
+   * whenever it is available and reset to `forceUpdateState` when it isn't.
+   */
+  forceUpdate = () => this.forceUpdateState();
+
+  executeQuery(
+    options: QueryHookOptions<TData, TVariables> & {
+      query?: DocumentNode;
+    }
+  ) {
     if (options.query) {
-      Object.assign(this, { query: options.query })
+      Object.assign(this, { query: options.query });
     }
 
     this.watchQueryOptions = this.createWatchQueryOptions(
-      this.queryHookOptions = options,
+      (this.queryHookOptions = options)
     );
 
     const concast = this.observable.reobserveAsConcast(
@@ -124,7 +145,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
     return new Promise<QueryResult<TData, TVariables>>((resolve) => {
       let result: ApolloQueryResult<TData>;
 
-      // Subscribe to the concast independently of the ObservableQuery in case 
+      // Subscribe to the concast independently of the ObservableQuery in case
       // the component gets unmounted before the promise resolves. This prevents
       // the concast from terminating early and resolving with `undefined` when
       // there are no more subscribers for the concast.
@@ -137,7 +158,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
         },
         complete: () => {
           resolve(this.toQueryResult(result));
-        }
+        },
       });
     });
   }
@@ -153,95 +174,103 @@ class InternalState<TData, TVariables extends OperationVariables> {
     // initialization, this.renderPromises is usually undefined (unless SSR is
     // happening), but that's fine as long as it has been initialized that way,
     // rather than left uninitialized.
-    this.renderPromises = useContext(getApolloContext()).renderPromises;
+    this.renderPromises = React.useContext(getApolloContext()).renderPromises;
 
     this.useOptions(options);
 
     const obsQuery = this.useObservableQuery();
 
     const result = useSyncExternalStore(
-      useCallback(() => {
-        if (this.renderPromises) {
-          return () => {};
-        }
-
-        const onNext = () => {
-          const previousResult = this.result;
-          // We use `getCurrentResult()` instead of the onNext argument because
-          // the values differ slightly. Specifically, loading results will have
-          // an empty object for data instead of `undefined` for some reason.
-          const result = obsQuery.getCurrentResult();
-          // Make sure we're not attempting to re-render similar results
-          if (
-            previousResult &&
-            previousResult.loading === result.loading &&
-            previousResult.networkStatus === result.networkStatus &&
-            equal(previousResult.data, result.data)
-          ) {
-            return;
-          }
-
-          this.setResult(result);
-        };
-
-        const onError = (error: Error) => {
-          const last = obsQuery["last"];
-          subscription.unsubscribe();
-          // Unfortunately, if `lastError` is set in the current
-          // `observableQuery` when the subscription is re-created,
-          // the subscription will immediately receive the error, which will
-          // cause it to terminate again. To avoid this, we first clear
-          // the last error/result from the `observableQuery` before re-starting
-          // the subscription, and restore it afterwards (so the subscription
-          // has a chance to stay open).
-          try {
-            obsQuery.resetLastResults();
-            subscription = obsQuery.subscribe(onNext, onError);
-          } finally {
-            obsQuery["last"] = last;
-          }
-
-          if (!hasOwnProperty.call(error, 'graphQLErrors')) {
-            // The error is not a GraphQL error
-            throw error;
+      React.useCallback(
+        (handleStoreChange) => {
+          if (this.renderPromises) {
+            return () => {};
           }
 
-          const previousResult = this.result;
-          if (
-            !previousResult ||
-            (previousResult && previousResult.loading) ||
-            !equal(error, previousResult.error)
-          ) {
-            this.setResult({
-              data: (previousResult && previousResult.data) as TData,
-              error: error as ApolloError,
-              loading: false,
-              networkStatus: NetworkStatus.error,
-            });
-          }
-        };
-
-        let subscription = obsQuery.subscribe(onNext, onError);
-
-        // Do the "unsubscribe" with a short delay.
-        // This way, an existing subscription can be reused without an additional
-        // request if "unsubscribe"  and "resubscribe" to the same ObservableQuery 
-        // happen in very fast succession.
-        return () => setTimeout(() => subscription.unsubscribe());
-      }, [
-        // We memoize the subscribe function using useCallback and the following
-        // dependency keys, because the subscribe function reference is all that
-        // useSyncExternalStore uses internally as a dependency key for the
-        // useEffect ultimately responsible for the subscription, so we are
-        // effectively passing this dependency array to that useEffect buried
-        // inside useSyncExternalStore, as desired.
-        obsQuery,
-        this.renderPromises,
-        this.client.disableNetworkFetches,
-      ]),
+          this.forceUpdate = handleStoreChange;
+
+          const onNext = () => {
+            const previousResult = this.result;
+            // We use `getCurrentResult()` instead of the onNext argument because
+            // the values differ slightly. Specifically, loading results will have
+            // an empty object for data instead of `undefined` for some reason.
+            const result = obsQuery.getCurrentResult();
+            // Make sure we're not attempting to re-render similar results
+            if (
+              previousResult &&
+              previousResult.loading === result.loading &&
+              previousResult.networkStatus === result.networkStatus &&
+              equal(previousResult.data, result.data)
+            ) {
+              return;
+            }
+
+            this.setResult(result);
+          };
+
+          const onError = (error: Error) => {
+            const last = obsQuery["last"];
+            subscription.unsubscribe();
+            // Unfortunately, if `lastError` is set in the current
+            // `observableQuery` when the subscription is re-created,
+            // the subscription will immediately receive the error, which will
+            // cause it to terminate again. To avoid this, we first clear
+            // the last error/result from the `observableQuery` before re-starting
+            // the subscription, and restore it afterwards (so the subscription
+            // has a chance to stay open).
+            try {
+              obsQuery.resetLastResults();
+              subscription = obsQuery.subscribe(onNext, onError);
+            } finally {
+              obsQuery["last"] = last;
+            }
+
+            if (!hasOwnProperty.call(error, "graphQLErrors")) {
+              // The error is not a GraphQL error
+              throw error;
+            }
+
+            const previousResult = this.result;
+            if (
+              !previousResult ||
+              (previousResult && previousResult.loading) ||
+              !equal(error, previousResult.error)
+            ) {
+              this.setResult({
+                data: (previousResult && previousResult.data) as TData,
+                error: error as ApolloError,
+                loading: false,
+                networkStatus: NetworkStatus.error,
+              });
+            }
+          };
+
+          let subscription = obsQuery.subscribe(onNext, onError);
+
+          // Do the "unsubscribe" with a short delay.
+          // This way, an existing subscription can be reused without an additional
+          // request if "unsubscribe"  and "resubscribe" to the same ObservableQuery
+          // happen in very fast succession.
+          return () => {
+            setTimeout(() => subscription.unsubscribe());
+            this.forceUpdate = () => this.forceUpdateState();
+          };
+        },
+        [
+          // We memoize the subscribe function using useCallback and the following
+          // dependency keys, because the subscribe function reference is all that
+          // useSyncExternalStore uses internally as a dependency key for the
+          // useEffect ultimately responsible for the subscription, so we are
+          // effectively passing this dependency array to that useEffect buried
+          // inside useSyncExternalStore, as desired.
+          obsQuery,
+          this.renderPromises,
+          this.client.disableNetworkFetches,
+        ]
+      ),
 
       () => this.getCurrentResult(),
-      () => this.getCurrentResult(),
+      () => this.getCurrentResult()
     );
 
     // TODO Remove this method when we remove support for options.partialRefetch.
@@ -258,11 +287,9 @@ class InternalState<TData, TVariables extends OperationVariables> {
   private queryHookOptions: QueryHookOptions<TData, TVariables>;
   private watchQueryOptions: WatchQueryOptions<TVariables, TData>;
 
-  private useOptions(
-    options: QueryHookOptions<TData, TVariables>,
-  ) {
+  private useOptions(options: QueryHookOptions<TData, TVariables>) {
     const watchQueryOptions = this.createWatchQueryOptions(
-      this.queryHookOptions = options,
+      (this.queryHookOptions = options)
     );
 
     // Update this.watchQueryOptions, but only when they have changed, which
@@ -298,7 +325,8 @@ class InternalState<TData, TVariables extends OperationVariables> {
     // Like the forceUpdate method, the versions of these methods inherited from
     // InternalState.prototype are empty no-ops, but we can override them on the
     // base state object (without modifying the prototype).
-    this.onCompleted = options.onCompleted || InternalState.prototype.onCompleted;
+    this.onCompleted =
+      options.onCompleted || InternalState.prototype.onCompleted;
     this.onError = options.onError || InternalState.prototype.onError;
 
     if (
@@ -311,7 +339,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
       this.result = this.ssrDisabledResult;
     } else if (
       this.queryHookOptions.skip ||
-      this.watchQueryOptions.fetchPolicy === 'standby'
+      this.watchQueryOptions.fetchPolicy === "standby"
     ) {
       // When skipping a query (ie. we're not querying for data but still want to
       // render children), make sure the `data` is cleared out and `loading` is
@@ -333,9 +361,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
   }
 
   private getObsQueryOptions(): WatchQueryOptions<TVariables, TData> {
-    const toMerge: Array<
-      Partial<WatchQueryOptions<TVariables, TData>>
-    > = [];
+    const toMerge: Array<Partial<WatchQueryOptions<TVariables, TData>>> = [];
 
     const globalDefaults = this.client.defaultOptions.watchQuery;
     if (globalDefaults) toMerge.push(globalDefaults);
@@ -354,14 +380,14 @@ class InternalState<TData, TVariables extends OperationVariables> {
     // (if provided) should be merged, to ensure individual defaulted
     // variables always have values, if not otherwise defined in
     // observable.options or watchQueryOptions.
-    toMerge.push(compact(
-      this.observable && this.observable.options,
-      this.watchQueryOptions,
-    ));
-
-    return toMerge.reduce(
-      mergeOptions
-    ) as WatchQueryOptions<TVariables, TData>;
+    toMerge.push(
+      compact(
+        this.observable && this.observable.options,
+        this.watchQueryOptions
+      )
+    );
+
+    return toMerge.reduce(mergeOptions) as WatchQueryOptions<TVariables, TData>;
   }
 
   private ssrDisabledResult = maybeDeepFreeze({
@@ -389,7 +415,10 @@ class InternalState<TData, TVariables extends OperationVariables> {
     // makes otherOptions almost a WatchQueryOptions object, except for the
     // query property that we add below.
     ...otherOptions
-  }: QueryHookOptions<TData, TVariables> = {}): WatchQueryOptions<TVariables, TData> {
+  }: QueryHookOptions<TData, TVariables> = {}): WatchQueryOptions<
+    TVariables,
+    TData
+  > {
     // This Object.assign is safe because otherOptions is a fresh ...rest object
     // that did not exist until just now, so modifications are still allowed.
     const watchQueryOptions: WatchQueryOptions<TVariables, TData> =
@@ -397,14 +426,12 @@ class InternalState<TData, TVariables extends OperationVariables> {
 
     if (
       this.renderPromises &&
-      (
-        watchQueryOptions.fetchPolicy === 'network-only' ||
-        watchQueryOptions.fetchPolicy === 'cache-and-network'
-      )
+      (watchQueryOptions.fetchPolicy === "network-only" ||
+        watchQueryOptions.fetchPolicy === "cache-and-network")
     ) {
       // this behavior was added to react-apollo without explanation in this PR
       // https://github.com/apollographql/react-apollo/pull/1579
-      watchQueryOptions.fetchPolicy = 'cache-first';
+      watchQueryOptions.fetchPolicy = "cache-first";
     }
 
     if (!watchQueryOptions.variables) {
@@ -422,7 +449,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
       // fetchPolicy that would have been used if not skipping.
       Object.assign(watchQueryOptions, {
         initialFetchPolicy,
-        fetchPolicy: 'standby',
+        fetchPolicy: "standby",
       });
     } else if (!watchQueryOptions.fetchPolicy) {
       watchQueryOptions.fetchPolicy =
@@ -457,25 +484,27 @@ class InternalState<TData, TVariables extends OperationVariables> {
     // See if there is an existing observable that was used to fetch the same
     // data and if so, use it instead since it will contain the proper queryId
     // to fetch the result set. This is used during SSR.
-    const obsQuery = this.observable =
-      this.renderPromises
-        && this.renderPromises.getSSRObservable(this.watchQueryOptions)
-        || this.observable // Reuse this.observable if possible (and not SSR)
-        || this.client.watchQuery(this.getObsQueryOptions());
-
-    this.obsQueryFields = useMemo(() => ({
-      refetch: obsQuery.refetch.bind(obsQuery),
-      reobserve: obsQuery.reobserve.bind(obsQuery),
-      fetchMore: obsQuery.fetchMore.bind(obsQuery),
-      updateQuery: obsQuery.updateQuery.bind(obsQuery),
-      startPolling: obsQuery.startPolling.bind(obsQuery),
-      stopPolling: obsQuery.stopPolling.bind(obsQuery),
-      subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery),
-    }), [obsQuery]);
+    const obsQuery = (this.observable =
+      (this.renderPromises &&
+        this.renderPromises.getSSRObservable(this.watchQueryOptions)) ||
+      this.observable || // Reuse this.observable if possible (and not SSR)
+      this.client.watchQuery(this.getObsQueryOptions()));
+
+    this.obsQueryFields = React.useMemo(
+      () => ({
+        refetch: obsQuery.refetch.bind(obsQuery),
+        reobserve: obsQuery.reobserve.bind(obsQuery),
+        fetchMore: obsQuery.fetchMore.bind(obsQuery),
+        updateQuery: obsQuery.updateQuery.bind(obsQuery),
+        startPolling: obsQuery.startPolling.bind(obsQuery),
+        stopPolling: obsQuery.stopPolling.bind(obsQuery),
+        subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery),
+      }),
+      [obsQuery]
+    );
 
     const ssrAllowed = !(
-      this.queryHookOptions.ssr === false ||
-      this.queryHookOptions.skip
+      this.queryHookOptions.ssr === false || this.queryHookOptions.skip
     );
 
     if (this.renderPromises && ssrAllowed) {
@@ -504,30 +533,41 @@ class InternalState<TData, TVariables extends OperationVariables> {
     // Calling state.setResult always triggers an update, though some call sites
     // perform additional equality checks before committing to an update.
     this.forceUpdate();
-    this.handleErrorOrCompleted(nextResult);
+    this.handleErrorOrCompleted(nextResult, previousResult);
   }
 
-  private handleErrorOrCompleted(result: ApolloQueryResult<TData>) {
+  private handleErrorOrCompleted(
+    result: ApolloQueryResult<TData>,
+    previousResult?: ApolloQueryResult<TData>
+  ) {
     if (!result.loading) {
       const error = this.toApolloError(result);
 
       // wait a tick in case we are in the middle of rendering a component
-      Promise.resolve().then(() => {
-        if (error) {
-          this.onError(error);
-        } else if (result.data) {
-          this.onCompleted(result.data);
-        }
-      }).catch(error => {
-        invariant.warn(error);
-      });
+      Promise.resolve()
+        .then(() => {
+          if (error) {
+            this.onError(error);
+          } else if (
+            result.data &&
+            previousResult?.networkStatus !== result.networkStatus &&
+            result.networkStatus === NetworkStatus.ready
+          ) {
+            this.onCompleted(result.data);
+          }
+        })
+        .catch((error) => {
+          invariant.warn(error);
+        });
     }
   }
 
-  private toApolloError(result: ApolloQueryResult<TData>): ApolloError | undefined {
+  private toApolloError(
+    result: ApolloQueryResult<TData>
+  ): ApolloError | undefined {
     return isNonEmptyArray(result.errors)
       ? new ApolloError({ graphQLErrors: result.errors })
-      : result.error
+      : result.error;
   }
 
   private getCurrentResult(): ApolloQueryResult<TData> {
@@ -536,7 +576,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
     // we're doing server rendering and therefore override the result below.
     if (!this.result) {
       this.handleErrorOrCompleted(
-        this.result = this.observable.getCurrentResult()
+        (this.result = this.observable.getCurrentResult())
       );
     }
     return this.result;
@@ -551,22 +591,25 @@ class InternalState<TData, TVariables extends OperationVariables> {
   >();
 
   toQueryResult(
-    result: ApolloQueryResult<TData>,
+    result: ApolloQueryResult<TData>
   ): QueryResult<TData, TVariables> {
     let queryResult = this.toQueryResultCache.get(result);
     if (queryResult) return queryResult;
 
     const { data, partial, ...resultWithoutPartial } = result;
-    this.toQueryResultCache.set(result, queryResult = {
-      data, // Ensure always defined, even if result.data is missing.
-      ...resultWithoutPartial,
-      ...this.obsQueryFields,
-      client: this.client,
-      observable: this.observable,
-      variables: this.observable.variables,
-      called: !this.queryHookOptions.skip,
-      previousData: this.previousData,
-    });
+    this.toQueryResultCache.set(
+      result,
+      (queryResult = {
+        data, // Ensure always defined, even if result.data is missing.
+        ...resultWithoutPartial,
+        ...this.obsQueryFields,
+        client: this.client,
+        observable: this.observable,
+        variables: this.observable.variables,
+        called: !this.queryHookOptions.skip,
+        previousData: this.previousData,
+      })
+    );
 
     if (!queryResult.error && isNonEmptyArray(result.errors)) {
       // Until a set naming convention for networkError and graphQLErrors is
@@ -590,7 +633,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
       this.queryHookOptions.partialRefetch &&
       !result.loading &&
       (!result.data || Object.keys(result.data).length === 0) &&
-      this.observable.options.fetchPolicy !== 'cache-only'
+      this.observable.options.fetchPolicy !== "cache-only"
     ) {
       Object.assign(result, {
         loading: true,
diff --git a/src/react/hooks/useReactiveVar.ts b/src/react/hooks/useReactiveVar.ts
--- a/src/react/hooks/useReactiveVar.ts
+++ b/src/react/hooks/useReactiveVar.ts
@@ -1,17 +1,17 @@
-import { useEffect, useState } from 'react';
-import { ReactiveVar } from '../../core';
+import * as React from "react";
+import type { ReactiveVar } from "../../core/index.js";
 
 export function useReactiveVar<T>(rv: ReactiveVar<T>): T {
   const value = rv();
 
   // We don't actually care what useState thinks the value of the variable
   // is, so we take only the update function from the returned array.
-  const setValue = useState(value)[1];
+  const setValue = React.useState(value)[1];
 
   // We subscribe to variable updates on initial mount and when the value has
   // changed. This avoids a subtle bug in React.StrictMode where multiple
   // listeners are added, leading to inconsistent updates.
-  useEffect(() => {
+  React.useEffect(() => {
     const probablySameValue = rv();
     if (value !== probablySameValue) {
       // If the value of rv has already changed, we don't need to listen for the
diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useReadQuery.ts
@@ -0,0 +1,49 @@
+import * as React from "react";
+import {
+  unwrapQueryRef,
+  type QueryReference,
+} from "../cache/QueryReference.js";
+import { __use } from "./internal/index.js";
+import { toApolloError } from "./useSuspenseQuery.js";
+import { invariant } from "../../utilities/globals/index.js";
+import { useSyncExternalStore } from "./useSyncExternalStore.js";
+
+export function useReadQuery<TData>(queryRef: QueryReference<TData>) {
+  const internalQueryRef = unwrapQueryRef(queryRef);
+  invariant(
+    internalQueryRef.promiseCache,
+    "It appears that `useReadQuery` was used outside of `useBackgroundQuery`. " +
+      "`useReadQuery` is only supported for use with `useBackgroundQuery`. " +
+      "Please ensure you are passing the `queryRef` returned from `useBackgroundQuery`."
+  );
+
+  const { promiseCache, key } = internalQueryRef;
+
+  if (!promiseCache.has(key)) {
+    promiseCache.set(key, internalQueryRef.promise);
+  }
+
+  const promise = useSyncExternalStore(
+    React.useCallback(
+      (forceUpdate) => {
+        return internalQueryRef.listen((promise) => {
+          internalQueryRef.promiseCache!.set(internalQueryRef.key, promise);
+          forceUpdate();
+        });
+      },
+      [internalQueryRef]
+    ),
+    () => promiseCache.get(key)!,
+    () => promiseCache.get(key)!
+  );
+
+  const result = __use(promise);
+
+  return React.useMemo(() => {
+    return {
+      data: result.data,
+      networkStatus: result.networkStatus,
+      error: toApolloError(result),
+    };
+  }, [result]);
+}
diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts
--- a/src/react/hooks/useSubscription.ts
+++ b/src/react/hooks/useSubscription.ts
@@ -1,26 +1,31 @@
-import '../../utilities/globals';
-import { useState, useRef, useEffect } from 'react';
-import { DocumentNode } from 'graphql';
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { invariant } from '../../utilities/globals'
-import { equal } from '@wry/equality';
-
-import { DocumentType, verifyDocumentType } from '../parser';
-import {
+import { invariant } from "../../utilities/globals/index.js";
+import * as React from "react";
+import type { DocumentNode } from "graphql";
+import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
+import { equal } from "@wry/equality";
+
+import { DocumentType, verifyDocumentType } from "../parser/index.js";
+import type {
+  NoInfer,
   SubscriptionHookOptions,
-  SubscriptionResult
-} from '../types/types';
-import { OperationVariables } from '../../core';
-import { useApolloClient } from './useApolloClient';
-
-export function useSubscription<TData = any, TVariables extends OperationVariables = OperationVariables>(
+  SubscriptionResult,
+} from "../types/types.js";
+import type { OperationVariables } from "../../core/index.js";
+import { useApolloClient } from "./useApolloClient.js";
+
+export function useSubscription<
+  TData = any,
+  TVariables extends OperationVariables = OperationVariables,
+>(
   subscription: DocumentNode | TypedDocumentNode<TData, TVariables>,
-  options?: SubscriptionHookOptions<TData, TVariables>,
+  options?: SubscriptionHookOptions<NoInfer<TData>, NoInfer<TVariables>>
 ) {
-  const hasIssuedDeprecationWarningRef = useRef(false);
+  const hasIssuedDeprecationWarningRef = React.useRef(false);
   const client = useApolloClient(options?.client);
   verifyDocumentType(subscription, DocumentType.Subscription);
-  const [result, setResult] = useState<SubscriptionResult<TData>>({
+  const [result, setResult] = React.useState<
+    SubscriptionResult<TData, TVariables>
+  >({
     loading: !options?.skip,
     error: void 0,
     data: void 0,
@@ -47,7 +52,7 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
     }
   }
 
-  const [observable, setObservable] = useState(() => {
+  const [observable, setObservable] = React.useState(() => {
     if (options?.skip) {
       return null;
     }
@@ -60,22 +65,25 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
     });
   });
 
-  const canResetObservableRef = useRef(false);
-  useEffect(() => {
+  const canResetObservableRef = React.useRef(false);
+  React.useEffect(() => {
     return () => {
       canResetObservableRef.current = true;
     };
   }, []);
 
-  const ref = useRef({ client, subscription, options });
-  useEffect(() => {
+  const ref = React.useRef({ client, subscription, options });
+  React.useEffect(() => {
     let shouldResubscribe = options?.shouldResubscribe;
-    if (typeof shouldResubscribe === 'function') {
+    if (typeof shouldResubscribe === "function") {
       shouldResubscribe = !!shouldResubscribe(options!);
     }
 
     if (options?.skip) {
-      if (!options?.skip !== !ref.current.options?.skip || canResetObservableRef.current) {
+      if (
+        !options?.skip !== !ref.current.options?.skip ||
+        canResetObservableRef.current
+      ) {
         setResult({
           loading: false,
           data: void 0,
@@ -100,19 +108,21 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
         error: void 0,
         variables: options?.variables,
       });
-      setObservable(client.subscribe({
-        query: subscription,
-        variables: options?.variables,
-        fetchPolicy: options?.fetchPolicy,
-        context: options?.context,
-      }));
+      setObservable(
+        client.subscribe({
+          query: subscription,
+          variables: options?.variables,
+          fetchPolicy: options?.fetchPolicy,
+          context: options?.context,
+        })
+      );
       canResetObservableRef.current = false;
     }
 
     Object.assign(ref.current, { client, subscription, options });
   }, [client, subscription, options, canResetObservableRef.current]);
 
-  useEffect(() => {
+  React.useEffect(() => {
     if (!observable) {
       return;
     }
@@ -137,12 +147,12 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
         if (ref.current.options?.onData) {
           ref.current.options.onData({
             client,
-            data: result
+            data: result,
           });
         } else if (ref.current.options?.onSubscriptionData) {
           ref.current.options.onSubscriptionData({
             client,
-            subscriptionData: result
+            subscriptionData: result,
           });
         }
       },
@@ -155,7 +165,7 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
             variables: options?.variables,
           });
           ref.current.options?.onError?.(error);
-        };
+        }
       },
       complete() {
         if (!subscriptionStopped) {
diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useSuspenseQuery.ts
@@ -0,0 +1,375 @@
+import * as React from "react";
+import { invariant } from "../../utilities/globals/index.js";
+import type {
+  ApolloClient,
+  ApolloQueryResult,
+  DocumentNode,
+  OperationVariables,
+  TypedDocumentNode,
+  WatchQueryFetchPolicy,
+  FetchMoreQueryOptions,
+  WatchQueryOptions,
+} from "../../core/index.js";
+import { ApolloError, NetworkStatus } from "../../core/index.js";
+import type { DeepPartial } from "../../utilities/index.js";
+import { isNonEmptyArray } from "../../utilities/index.js";
+import { useApolloClient } from "./useApolloClient.js";
+import { DocumentType, verifyDocumentType } from "../parser/index.js";
+import type {
+  SuspenseQueryHookOptions,
+  ObservableQueryFields,
+  NoInfer,
+} from "../types/types.js";
+import { __use, useDeepMemo } from "./internal/index.js";
+import { getSuspenseCache } from "../cache/index.js";
+import { canonicalStringify } from "../../cache/index.js";
+import { skipToken, type SkipToken } from "./constants.js";
+import type { CacheKey } from "../cache/types.js";
+
+export interface UseSuspenseQueryResult<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+> {
+  client: ApolloClient<any>;
+  data: TData;
+  error: ApolloError | undefined;
+  fetchMore: FetchMoreFunction<TData, TVariables>;
+  networkStatus: NetworkStatus;
+  refetch: RefetchFunction<TData, TVariables>;
+  subscribeToMore: SubscribeToMoreFunction<TData, TVariables>;
+}
+
+export type FetchMoreFunction<TData, TVariables extends OperationVariables> = (
+  fetchMoreOptions: FetchMoreQueryOptions<TVariables, TData> & {
+    updateQuery?: (
+      previousQueryResult: TData,
+      options: {
+        fetchMoreResult: TData;
+        variables: TVariables;
+      }
+    ) => TData;
+  }
+) => Promise<ApolloQueryResult<TData>>;
+
+export type RefetchFunction<
+  TData,
+  TVariables extends OperationVariables
+> = ObservableQueryFields<TData, TVariables>["refetch"];
+
+export type SubscribeToMoreFunction<
+  TData,
+  TVariables extends OperationVariables
+> = ObservableQueryFields<TData, TVariables>["subscribeToMore"];
+
+export function useSuspenseQuery<
+  TData,
+  TVariables extends OperationVariables,
+  TOptions extends Omit<SuspenseQueryHookOptions<TData>, "variables">
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options?: SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> &
+    TOptions
+): UseSuspenseQueryResult<
+  TOptions["errorPolicy"] extends "ignore" | "all"
+    ? TOptions["returnPartialData"] extends true
+      ? DeepPartial<TData> | undefined
+      : TData | undefined
+    : TOptions["returnPartialData"] extends true
+    ? TOptions["skip"] extends boolean
+      ? DeepPartial<TData> | undefined
+      : DeepPartial<TData>
+    : TOptions["skip"] extends boolean
+    ? TData | undefined
+    : TData,
+  TVariables
+>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> & {
+    returnPartialData: true;
+    errorPolicy: "ignore" | "all";
+  }
+): UseSuspenseQueryResult<DeepPartial<TData> | undefined, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> & {
+    errorPolicy: "ignore" | "all";
+  }
+): UseSuspenseQueryResult<TData | undefined, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> & {
+    skip: boolean;
+    returnPartialData: true;
+  }
+): UseSuspenseQueryResult<DeepPartial<TData> | undefined, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> & {
+    returnPartialData: true;
+  }
+): UseSuspenseQueryResult<DeepPartial<TData>, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> & {
+    skip: boolean;
+  }
+): UseSuspenseQueryResult<TData | undefined, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options?: SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>>
+): UseSuspenseQueryResult<TData, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options:
+    | SkipToken
+    | (SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> & {
+        returnPartialData: true;
+      })
+): UseSuspenseQueryResult<DeepPartial<TData> | undefined, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options?:
+    | SkipToken
+    | SuspenseQueryHookOptions<NoInfer<TData>, NoInfer<TVariables>>
+): UseSuspenseQueryResult<TData | undefined, TVariables>;
+
+export function useSuspenseQuery<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options:
+    | (SkipToken & Partial<SuspenseQueryHookOptions<TData, TVariables>>)
+    | SuspenseQueryHookOptions<TData, TVariables> = Object.create(null)
+): UseSuspenseQueryResult<TData | undefined, TVariables> {
+  const client = useApolloClient(options.client);
+  const suspenseCache = getSuspenseCache(client);
+  const watchQueryOptions = useWatchQueryOptions({ client, query, options });
+  const { fetchPolicy, variables } = watchQueryOptions;
+  const { queryKey = [] } = options;
+
+  const cacheKey: CacheKey = [
+    query,
+    canonicalStringify(variables),
+    ...([] as any[]).concat(queryKey),
+  ];
+
+  const queryRef = suspenseCache.getQueryRef(cacheKey, () =>
+    client.watchQuery(watchQueryOptions)
+  );
+
+  const [promiseCache, setPromiseCache] = React.useState(
+    () => new Map([[queryRef.key, queryRef.promise]])
+  );
+
+  let promise = promiseCache.get(queryRef.key);
+
+  if (queryRef.didChangeOptions(watchQueryOptions)) {
+    promise = queryRef.applyOptions(watchQueryOptions);
+    promiseCache.set(queryRef.key, promise);
+  }
+
+  if (!promise) {
+    promise = queryRef.promise;
+    promiseCache.set(queryRef.key, promise);
+  }
+
+  React.useEffect(() => {
+    const dispose = queryRef.retain();
+
+    const removeListener = queryRef.listen((promise) => {
+      setPromiseCache((promiseCache) =>
+        new Map(promiseCache).set(queryRef.key, promise)
+      );
+    });
+
+    return () => {
+      removeListener();
+      dispose();
+    };
+  }, [queryRef]);
+
+  const skipResult = React.useMemo(() => {
+    const error = toApolloError(queryRef.result);
+
+    return {
+      loading: false,
+      data: queryRef.result.data,
+      networkStatus: error ? NetworkStatus.error : NetworkStatus.ready,
+      error,
+    };
+  }, [queryRef.result]);
+
+  const result = fetchPolicy === "standby" ? skipResult : __use(promise);
+
+  const fetchMore: FetchMoreFunction<TData, TVariables> = React.useCallback(
+    (options) => {
+      const promise = queryRef.fetchMore(options);
+
+      setPromiseCache((previousPromiseCache) =>
+        new Map(previousPromiseCache).set(queryRef.key, queryRef.promise)
+      );
+
+      return promise;
+    },
+    [queryRef]
+  );
+
+  const refetch: RefetchFunction<TData, TVariables> = React.useCallback(
+    (variables) => {
+      const promise = queryRef.refetch(variables);
+
+      setPromiseCache((previousPromiseCache) =>
+        new Map(previousPromiseCache).set(queryRef.key, queryRef.promise)
+      );
+
+      return promise;
+    },
+    [queryRef]
+  );
+
+  const subscribeToMore: SubscribeToMoreFunction<TData, TVariables> =
+    React.useCallback(
+      (options) => queryRef.observable.subscribeToMore(options),
+      [queryRef]
+    );
+
+  return React.useMemo(() => {
+    return {
+      client,
+      data: result.data,
+      error: toApolloError(result),
+      networkStatus: result.networkStatus,
+      fetchMore,
+      refetch,
+      subscribeToMore,
+    };
+  }, [client, fetchMore, refetch, result, subscribeToMore]);
+}
+
+function validateOptions(options: WatchQueryOptions) {
+  const { query, fetchPolicy, returnPartialData } = options;
+
+  verifyDocumentType(query, DocumentType.Query);
+  validateFetchPolicy(fetchPolicy);
+  validatePartialDataReturn(fetchPolicy, returnPartialData);
+}
+
+function validateFetchPolicy(
+  fetchPolicy: WatchQueryFetchPolicy = "cache-first"
+) {
+  const supportedFetchPolicies: WatchQueryFetchPolicy[] = [
+    "cache-first",
+    "network-only",
+    "no-cache",
+    "cache-and-network",
+  ];
+
+  invariant(
+    supportedFetchPolicies.includes(fetchPolicy),
+    `The fetch policy \`%s\` is not supported with suspense.`,
+    fetchPolicy
+  );
+}
+
+function validatePartialDataReturn(
+  fetchPolicy: WatchQueryFetchPolicy | undefined,
+  returnPartialData: boolean | undefined
+) {
+  if (fetchPolicy === "no-cache" && returnPartialData) {
+    invariant.warn(
+      "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy."
+    );
+  }
+}
+
+export function toApolloError(result: ApolloQueryResult<any>) {
+  return isNonEmptyArray(result.errors)
+    ? new ApolloError({ graphQLErrors: result.errors })
+    : result.error;
+}
+
+interface UseWatchQueryOptionsHookOptions<
+  TData,
+  TVariables extends OperationVariables
+> {
+  client: ApolloClient<unknown>;
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>;
+  options: SkipToken | SuspenseQueryHookOptions<TData, TVariables>;
+}
+
+export function useWatchQueryOptions<
+  TData,
+  TVariables extends OperationVariables
+>({
+  client,
+  query,
+  options,
+}: UseWatchQueryOptionsHookOptions<TData, TVariables>): WatchQueryOptions<
+  TVariables,
+  TData
+> {
+  return useDeepMemo<WatchQueryOptions<TVariables, TData>>(() => {
+    if (options === skipToken) {
+      return { query, fetchPolicy: "standby" };
+    }
+
+    const fetchPolicy =
+      options.fetchPolicy ||
+      client.defaultOptions.watchQuery?.fetchPolicy ||
+      "cache-first";
+
+    const watchQueryOptions = {
+      ...options,
+      fetchPolicy,
+      query,
+      notifyOnNetworkStatusChange: false,
+      nextFetchPolicy: void 0,
+    };
+
+    if (__DEV__) {
+      validateOptions(watchQueryOptions);
+    }
+
+    // Assign the updated fetch policy after our validation since `standby` is
+    // not a supported fetch policy on its own without the use of `skip`.
+    if (options.skip) {
+      watchQueryOptions.fetchPolicy = "standby";
+    }
+
+    return watchQueryOptions;
+  }, [client, options, query]);
+}
diff --git a/src/react/hooks/useSyncExternalStore.ts b/src/react/hooks/useSyncExternalStore.ts
--- a/src/react/hooks/useSyncExternalStore.ts
+++ b/src/react/hooks/useSyncExternalStore.ts
@@ -1,7 +1,7 @@
-import { invariant } from '../../utilities/globals';
-import * as React from 'react';
+import { invariant } from "../../utilities/globals/index.js";
+import * as React from "react";
 
-import { canUseLayoutEffect } from '../../utilities';
+import { canUseLayoutEffect } from "../../utilities/index.js";
 
 let didWarnUncachedGetSnapshot = false;
 
@@ -22,96 +22,96 @@ const realHook = React[uSESKey] as RealUseSESHookType | undefined;
 // Apollo Client deviations called out by "// DEVIATION ..." comments.
 
 // When/if React.useSyncExternalStore is defined, delegate fully to it.
-export const useSyncExternalStore: RealUseSESHookType = realHook || ((
-  subscribe,
-  getSnapshot,
-  getServerSnapshot,
-) => {
-  // Read the current snapshot from the store on every render. Again, this
-  // breaks the rules of React, and only works here because of specific
-  // implementation details, most importantly that updates are
-  // always synchronous.
-  const value = getSnapshot();
-  if (
-    // DEVIATION: Using our own __DEV__ polyfill (from ../../utilities/globals).
-    __DEV__ &&
-    !didWarnUncachedGetSnapshot &&
-    // DEVIATION: Not using Object.is because we know our snapshots will never
-    // be exotic primitive values like NaN, which is !== itself.
-    value !== getSnapshot()
-  ) {
-    didWarnUncachedGetSnapshot = true;
-    // DEVIATION: Using invariant.error instead of console.error directly.
-    invariant.error(
-      'The result of getSnapshot should be cached to avoid an infinite loop',
-    );
-  }
+export const useSyncExternalStore: RealUseSESHookType =
+  realHook ||
+  ((subscribe, getSnapshot, getServerSnapshot) => {
+    // Read the current snapshot from the store on every render. Again, this
+    // breaks the rules of React, and only works here because of specific
+    // implementation details, most importantly that updates are
+    // always synchronous.
+    const value = getSnapshot();
+    if (
+      // DEVIATION: Using __DEV__
+      __DEV__ &&
+      !didWarnUncachedGetSnapshot &&
+      // DEVIATION: Not using Object.is because we know our snapshots will never
+      // be exotic primitive values like NaN, which is !== itself.
+      value !== getSnapshot()
+    ) {
+      didWarnUncachedGetSnapshot = true;
+      // DEVIATION: Using invariant.error instead of console.error directly.
+      invariant.error(
+        "The result of getSnapshot should be cached to avoid an infinite loop"
+      );
+    }
 
-  // Because updates are synchronous, we don't queue them. Instead we force a
-  // re-render whenever the subscribed state changes by updating an some
-  // arbitrary useState hook. Then, during render, we call getSnapshot to read
-  // the current value.
-  //
-  // Because we don't actually use the state returned by the useState hook, we
-  // can save a bit of memory by storing other stuff in that slot.
-  //
-  // To implement the early bailout, we need to track some things on a mutable
-  // object. Usually, we would put that in a useRef hook, but we can stash it in
-  // our useState hook instead.
-  //
-  // To force a re-render, we call forceUpdate({inst}). That works because the
-  // new object always fails an equality check.
-  const [{inst}, forceUpdate] = React.useState({inst: {value, getSnapshot}});
+    // Because updates are synchronous, we don't queue them. Instead we force a
+    // re-render whenever the subscribed state changes by updating an some
+    // arbitrary useState hook. Then, during render, we call getSnapshot to read
+    // the current value.
+    //
+    // Because we don't actually use the state returned by the useState hook, we
+    // can save a bit of memory by storing other stuff in that slot.
+    //
+    // To implement the early bailout, we need to track some things on a mutable
+    // object. Usually, we would put that in a useRef hook, but we can stash it in
+    // our useState hook instead.
+    //
+    // To force a re-render, we call forceUpdate({inst}). That works because the
+    // new object always fails an equality check.
+    const [{ inst }, forceUpdate] = React.useState({
+      inst: { value, getSnapshot },
+    });
 
-  // Track the latest getSnapshot function with a ref. This needs to be updated
-  // in the layout phase so we can access it during the tearing check that
-  // happens on subscribe.
-  if (canUseLayoutEffect) {
-    // DEVIATION: We avoid calling useLayoutEffect when !canUseLayoutEffect,
-    // which may seem like a conditional hook, but this code ends up behaving
-    // unconditionally (one way or the other) because canUseLayoutEffect is
-    // constant.
-    React.useLayoutEffect(() => {
+    // Track the latest getSnapshot function with a ref. This needs to be updated
+    // in the layout phase so we can access it during the tearing check that
+    // happens on subscribe.
+    if (canUseLayoutEffect) {
+      // DEVIATION: We avoid calling useLayoutEffect when !canUseLayoutEffect,
+      // which may seem like a conditional hook, but this code ends up behaving
+      // unconditionally (one way or the other) because canUseLayoutEffect is
+      // constant.
+      React.useLayoutEffect(() => {
+        Object.assign(inst, { value, getSnapshot });
+        // Whenever getSnapshot or subscribe changes, we need to check in the
+        // commit phase if there was an interleaved mutation. In concurrent mode
+        // this can happen all the time, but even in synchronous mode, an earlier
+        // effect may have mutated the store.
+        if (checkIfSnapshotChanged(inst)) {
+          // Force a re-render.
+          forceUpdate({ inst });
+        }
+      }, [subscribe, value, getSnapshot]);
+    } else {
       Object.assign(inst, { value, getSnapshot });
-      // Whenever getSnapshot or subscribe changes, we need to check in the
-      // commit phase if there was an interleaved mutation. In concurrent mode
-      // this can happen all the time, but even in synchronous mode, an earlier
-      // effect may have mutated the store.
-      if (checkIfSnapshotChanged(inst)) {
-        // Force a re-render.
-        forceUpdate({inst});
-      }
-    }, [subscribe, value, getSnapshot]);
-  } else {
-    Object.assign(inst, { value, getSnapshot });
-  }
-
-  React.useEffect(() => {
-    // Check for changes right before subscribing. Subsequent changes will be
-    // detected in the subscription handler.
-    if (checkIfSnapshotChanged(inst)) {
-      // Force a re-render.
-      forceUpdate({inst});
     }
 
-    // Subscribe to the store and return a clean-up function.
-    return subscribe(function handleStoreChange() {
-      // TODO: Because there is no cross-renderer API for batching updates, it's
-      // up to the consumer of this library to wrap their subscription event
-      // with unstable_batchedUpdates. Should we try to detect when this isn't
-      // the case and print a warning in development?
-
-      // The store changed. Check if the snapshot changed since the last time we
-      // read from the store.
+    React.useEffect(() => {
+      // Check for changes right before subscribing. Subsequent changes will be
+      // detected in the subscription handler.
       if (checkIfSnapshotChanged(inst)) {
         // Force a re-render.
-        forceUpdate({inst});
+        forceUpdate({ inst });
       }
-    });
-  }, [subscribe]);
 
-  return value;
-});
+      // Subscribe to the store and return a clean-up function.
+      return subscribe(function handleStoreChange() {
+        // TODO: Because there is no cross-renderer API for batching updates, it's
+        // up to the consumer of this library to wrap their subscription event
+        // with unstable_batchedUpdates. Should we try to detect when this isn't
+        // the case and print a warning in development?
+
+        // The store changed. Check if the snapshot changed since the last time we
+        // read from the store.
+        if (checkIfSnapshotChanged(inst)) {
+          // Force a re-render.
+          forceUpdate({ inst });
+        }
+      });
+    }, [subscribe]);
+
+    return value;
+  });
 
 function checkIfSnapshotChanged<Snapshot>({
   value,
diff --git a/src/react/index.ts b/src/react/index.ts
--- a/src/react/index.ts
+++ b/src/react/index.ts
@@ -1,20 +1,18 @@
-import '../utilities/globals';
+import "../utilities/globals/index.js";
 
+export type { ApolloContextValue } from "./context/index.js";
 export {
   ApolloProvider,
   ApolloConsumer,
   getApolloContext,
   resetApolloContext,
-  ApolloContextValue
-} from './context';
+} from "./context/index.js";
 
-export * from './hooks';
+export * from "./hooks/index.js";
+// TODO: remove export with release 3.8
+export { SuspenseCache } from "./cache/index.js";
 
-export {
-  DocumentType,
-  IDocumentDefinition,
-  operationName,
-  parser
-} from './parser';
+export type { IDocumentDefinition } from "./parser/index.js";
+export { DocumentType, operationName, parser } from "./parser/index.js";
 
-export * from './types/types';
+export * from "./types/types.js";
diff --git a/src/react/parser/index.ts b/src/react/parser/index.ts
--- a/src/react/parser/index.ts
+++ b/src/react/parser/index.ts
@@ -1,16 +1,16 @@
-import { invariant } from '../../utilities/globals';
+import { invariant } from "../../utilities/globals/index.js";
 
-import {
+import type {
   DocumentNode,
   DefinitionNode,
   VariableDefinitionNode,
-  OperationDefinitionNode
-} from 'graphql';
+  OperationDefinitionNode,
+} from "graphql";
 
 export enum DocumentType {
   Query,
   Mutation,
-  Subscription
+  Subscription,
 }
 
 export interface IDocumentDefinition {
@@ -25,13 +25,13 @@ export function operationName(type: DocumentType) {
   let name;
   switch (type) {
     case DocumentType.Query:
-      name = 'Query';
+      name = "Query";
       break;
     case DocumentType.Mutation:
-      name = 'Mutation';
+      name = "Mutation";
       break;
     case DocumentType.Subscription:
-      name = 'Subscription';
+      name = "Subscription";
       break;
   }
   return name;
@@ -46,31 +46,32 @@ export function parser(document: DocumentNode): IDocumentDefinition {
 
   invariant(
     !!document && !!document.kind,
-    `Argument of ${document} passed to parser was not a valid GraphQL ` +
+    `Argument of %s passed to parser was not a valid GraphQL ` +
       `DocumentNode. You may need to use 'graphql-tag' or another method ` +
-      `to convert your operation into a document`
+      `to convert your operation into a document`,
+    document
   );
 
-  const fragments: DefinitionNode[] = []
-  const queries: DefinitionNode[] = []
-  const mutations: DefinitionNode[] = []
-  const subscriptions: DefinitionNode[] = []
+  const fragments: DefinitionNode[] = [];
+  const queries: DefinitionNode[] = [];
+  const mutations: DefinitionNode[] = [];
+  const subscriptions: DefinitionNode[] = [];
 
   for (const x of document.definitions) {
-    if (x.kind === 'FragmentDefinition') {
+    if (x.kind === "FragmentDefinition") {
       fragments.push(x);
-      continue
+      continue;
     }
 
-    if (x.kind === 'OperationDefinition') {
+    if (x.kind === "OperationDefinition") {
       switch (x.operation) {
-        case 'query':
+        case "query":
           queries.push(x);
           break;
-        case 'mutation':
+        case "mutation":
           mutations.push(x);
           break;
-        case 'subscription':
+        case "subscription":
           subscriptions.push(x);
           break;
       }
@@ -79,7 +80,9 @@ export function parser(document: DocumentNode): IDocumentDefinition {
 
   invariant(
     !fragments.length ||
-      (queries.length || mutations.length || subscriptions.length),
+      queries.length ||
+      mutations.length ||
+      subscriptions.length,
     `Passing only a fragment to 'graphql' is not yet supported. ` +
       `You must include a query, subscription or mutation as well`
   );
@@ -87,9 +90,13 @@ export function parser(document: DocumentNode): IDocumentDefinition {
   invariant(
     queries.length + mutations.length + subscriptions.length <= 1,
     `react-apollo only supports a query, subscription, or a mutation per HOC. ` +
-      `${document} had ${queries.length} queries, ${subscriptions.length} ` +
-      `subscriptions and ${mutations.length} mutations. ` +
-      `You can use 'compose' to join multiple operation types to a component`
+      `%s had %s queries, %s ` +
+      `subscriptions and %s mutations. ` +
+      `You can use 'compose' to join multiple operation types to a component`,
+    document,
+    queries.length,
+    subscriptions.length,
+    mutations.length
   );
 
   type = queries.length ? DocumentType.Query : DocumentType.Mutation;
@@ -103,18 +110,20 @@ export function parser(document: DocumentNode): IDocumentDefinition {
 
   invariant(
     definitions.length === 1,
-    `react-apollo only supports one definition per HOC. ${document} had ` +
-      `${definitions.length} definitions. ` +
-      `You can use 'compose' to join multiple operation types to a component`
+    `react-apollo only supports one definition per HOC. %s had ` +
+      `%s definitions. ` +
+      `You can use 'compose' to join multiple operation types to a component`,
+    document,
+    definitions.length
   );
 
   const definition = definitions[0] as OperationDefinitionNode;
   variables = definition.variableDefinitions || [];
 
-  if (definition.name && definition.name.kind === 'Name') {
+  if (definition.name && definition.name.kind === "Name") {
     name = definition.name.value;
   } else {
-    name = 'data'; // fallback to using data if no name
+    name = "data"; // fallback to using data if no name
   }
 
   const payload = { name, type, variables };
@@ -128,8 +137,9 @@ export function verifyDocumentType(document: DocumentNode, type: DocumentType) {
   const usedOperationName = operationName(operation.type);
   invariant(
     operation.type === type,
-    `Running a ${requiredOperationName} requires a graphql ` +
-      `${requiredOperationName}, but a ${usedOperationName} was used instead.`
+    `Running a %s requires a graphql ` + `%s, but a %s was used instead.`,
+    requiredOperationName,
+    requiredOperationName,
+    usedOperationName
   );
 }
-
diff --git a/src/react/ssr/RenderPromises.ts b/src/react/ssr/RenderPromises.ts
--- a/src/react/ssr/RenderPromises.ts
+++ b/src/react/ssr/RenderPromises.ts
@@ -1,7 +1,7 @@
-import { DocumentNode } from 'graphql';
+import type { DocumentNode } from "graphql";
 
-import { ObservableQuery, OperationVariables } from '../../core';
-import { QueryDataOptions } from '../types/types';
+import type { ObservableQuery, OperationVariables } from "../../core/index.js";
+import type { QueryDataOptions } from "../types/types.js";
 
 // TODO: A vestigial interface from when hooks were implemented with utility
 // classes, which should be deleted in the future.
@@ -18,7 +18,7 @@ type QueryInfo = {
 function makeDefaultQueryInfo(): QueryInfo {
   return {
     seen: false,
-    observable: null
+    observable: null,
   };
 }
 
@@ -43,7 +43,7 @@ export class RenderPromises {
 
   // Registers the server side rendered observable.
   public registerSSRObservable<TData, TVariables extends OperationVariables>(
-    observable: ObservableQuery<any, TVariables>,
+    observable: ObservableQuery<any, TVariables>
   ) {
     if (this.stopped) return;
     this.lookupQueryInfo(observable.options).observable = observable;
@@ -58,14 +58,14 @@ export class RenderPromises {
 
   public addQueryPromise(
     queryInstance: QueryData,
-    finish?: () => React.ReactNode,
+    finish?: () => React.ReactNode
   ): React.ReactNode {
     if (!this.stopped) {
       const info = this.lookupQueryInfo(queryInstance.getOptions());
       if (!info.seen) {
         this.queryPromises.set(
           queryInstance.getOptions(),
-          new Promise(resolve => {
+          new Promise((resolve) => {
             resolve(queryInstance.fetchData());
           })
         );
@@ -77,30 +77,32 @@ export class RenderPromises {
     return finish ? finish() : null;
   }
 
-  public addObservableQueryPromise<TData, TVariables extends OperationVariables>(
-    obsQuery: ObservableQuery<TData, TVariables>,
-  ) {
+  public addObservableQueryPromise<
+    TData,
+    TVariables extends OperationVariables,
+  >(obsQuery: ObservableQuery<TData, TVariables>) {
     return this.addQueryPromise({
       // The only options which seem to actually be used by the
       // RenderPromises class are query and variables.
       getOptions: () => obsQuery.options,
-      fetchData: () => new Promise<void>((resolve) => {
-        const sub = obsQuery.subscribe({
-          next(result) {
-            if (!result.loading) {
-              resolve()
+      fetchData: () =>
+        new Promise<void>((resolve) => {
+          const sub = obsQuery.subscribe({
+            next(result) {
+              if (!result.loading) {
+                resolve();
+                sub.unsubscribe();
+              }
+            },
+            error() {
+              resolve();
               sub.unsubscribe();
-            }
-          },
-          error() {
-            resolve();
-            sub.unsubscribe();
-          },
-          complete() {
-            resolve();
-          },
-        });
-      }),
+            },
+            complete() {
+              resolve();
+            },
+          });
+        }),
     });
   }
 
diff --git a/src/react/ssr/getDataFromTree.ts b/src/react/ssr/getDataFromTree.ts
--- a/src/react/ssr/getDataFromTree.ts
+++ b/src/react/ssr/getDataFromTree.ts
@@ -1,7 +1,7 @@
-import * as React from 'react';
-import { getApolloContext } from '../context';
-import { RenderPromises } from './RenderPromises';
-import { renderToStaticMarkup } from 'react-dom/server';
+import * as React from "react";
+import { getApolloContext } from "../context/index.js";
+import { RenderPromises } from "./RenderPromises.js";
+import { renderToStaticMarkup } from "react-dom/server";
 
 export function getDataFromTree(
   tree: React.ReactNode,
@@ -12,7 +12,7 @@ export function getDataFromTree(
     context,
     // If you need to configure this renderFunction, call getMarkupFromTree
     // directly instead of getDataFromTree.
-    renderFunction: renderToStaticMarkup
+    renderFunction: renderToStaticMarkup,
   });
 }
 
@@ -20,7 +20,7 @@ export type GetMarkupFromTreeOptions = {
   tree: React.ReactNode;
   context?: { [key: string]: any };
   renderFunction?: (
-    tree: React.ReactElement<any>,
+    tree: React.ReactElement<any>
   ) => string | PromiseLike<string>;
 };
 
@@ -30,7 +30,7 @@ export function getMarkupFromTree({
   // The rendering function is configurable! We use renderToStaticMarkup as
   // the default, because it's a little less expensive than renderToString,
   // and legacy usage of getDataFromTree ignores the return value anyway.
-  renderFunction = renderToStaticMarkup
+  renderFunction = renderToStaticMarkup,
 }: GetMarkupFromTreeOptions): Promise<string> {
   const renderPromises = new RenderPromises();
 
@@ -42,20 +42,22 @@ export function getMarkupFromTree({
     // elements) for a subtree of the original component tree.
     const ApolloContext = getApolloContext();
 
-    return new Promise<string>(resolve => {
+    return new Promise<string>((resolve) => {
       const element = React.createElement(
         ApolloContext.Provider,
-        { value: { ...context, renderPromises }},
-        tree,
+        { value: { ...context, renderPromises } },
+        tree
       );
       resolve(renderFunction(element));
-    }).then(html => {
-      return renderPromises.hasPromises()
-        ? renderPromises.consumeAndAwaitPromises().then(process)
-        : html;
-    }).finally(() => {
-      renderPromises.stop();
-    });
+    })
+      .then((html) => {
+        return renderPromises.hasPromises()
+          ? renderPromises.consumeAndAwaitPromises().then(process)
+          : html;
+      })
+      .finally(() => {
+        renderPromises.stop();
+      });
   }
 
   return Promise.resolve().then(process);
diff --git a/src/react/ssr/index.ts b/src/react/ssr/index.ts
--- a/src/react/ssr/index.ts
+++ b/src/react/ssr/index.ts
@@ -1,3 +1,3 @@
-export { getMarkupFromTree, getDataFromTree } from './getDataFromTree';
-export { renderToStringWithData } from './renderToStringWithData';
-export { RenderPromises } from './RenderPromises';
+export { getMarkupFromTree, getDataFromTree } from "./getDataFromTree.js";
+export { renderToStringWithData } from "./renderToStringWithData.js";
+export { RenderPromises } from "./RenderPromises.js";
diff --git a/src/react/ssr/renderToStringWithData.ts b/src/react/ssr/renderToStringWithData.ts
--- a/src/react/ssr/renderToStringWithData.ts
+++ b/src/react/ssr/renderToStringWithData.ts
@@ -1,12 +1,12 @@
-import { ReactElement } from 'react';
-import { getMarkupFromTree } from './getDataFromTree';
-import { renderToString } from 'react-dom/server';
+import type { ReactElement } from "react";
+import { getMarkupFromTree } from "./getDataFromTree.js";
+import { renderToString } from "react-dom/server";
 
 export function renderToStringWithData(
   component: ReactElement<any>
 ): Promise<string> {
   return getMarkupFromTree({
     tree: component,
-    renderFunction: renderToString
+    renderFunction: renderToString,
   });
 }
diff --git a/src/react/types/types.ts b/src/react/types/types.ts
--- a/src/react/types/types.ts
+++ b/src/react/types/types.ts
@@ -1,11 +1,14 @@
-import { ReactNode } from 'react';
-import { DocumentNode } from 'graphql';
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-
-import { Observable, ObservableSubscription } from '../../utilities';
-import { FetchResult } from '../../link/core';
-import { ApolloError } from '../../errors';
-import {
+import type { ReactNode } from "react";
+import type { DocumentNode } from "graphql";
+import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
+
+import type {
+  Observable,
+  ObservableSubscription,
+} from "../../utilities/index.js";
+import type { FetchResult } from "../../link/core/index.js";
+import type { ApolloError } from "../../errors/index.js";
+import type {
   ApolloCache,
   ApolloClient,
   DefaultContext,
@@ -16,11 +19,17 @@ import {
   OperationVariables,
   InternalRefetchQueriesInclude,
   WatchQueryOptions,
-} from '../../core';
+  WatchQueryFetchPolicy,
+} from "../../core/index.js";
+import type { SuspenseCache } from "../cache/index.js";
+
+/* QueryReference type */
+
+export type { QueryReference } from "../cache/QueryReference.js";
 
 /* Common types */
 
-export type { DefaultContext as Context } from "../../core";
+export type { DefaultContext as Context } from "../../core/index.js";
 
 export type CommonOptions<TOptions> = TOptions & {
   client?: ApolloClient<object>;
@@ -28,8 +37,9 @@ export type CommonOptions<TOptions> = TOptions & {
 
 /* Query types */
 
-export interface BaseQueryOptions<TVariables extends OperationVariables = OperationVariables>
-extends Omit<WatchQueryOptions<TVariables>, "query"> {
+export interface BaseQueryOptions<
+  TVariables extends OperationVariables = OperationVariables
+> extends Omit<WatchQueryOptions<TVariables>, "query"> {
   ssr?: boolean;
   client?: ApolloClient<any>;
   context?: DefaultContext;
@@ -51,20 +61,25 @@ export interface QueryFunctionOptions<
   defaultOptions?: Partial<WatchQueryOptions<TVariables, TData>>;
 }
 
-export type ObservableQueryFields<TData, TVariables extends OperationVariables> = Pick<
+export type ObservableQueryFields<
+  TData,
+  TVariables extends OperationVariables
+> = Pick<
   ObservableQuery<TData, TVariables>,
-  | 'startPolling'
-  | 'stopPolling'
-  | 'subscribeToMore'
-  | 'updateQuery'
-  | 'refetch'
-  | 'reobserve'
-  | 'variables'
-  | 'fetchMore'
+  | "startPolling"
+  | "stopPolling"
+  | "subscribeToMore"
+  | "updateQuery"
+  | "refetch"
+  | "reobserve"
+  | "variables"
+  | "fetchMore"
 >;
 
-export interface QueryResult<TData = any, TVariables extends OperationVariables = OperationVariables>
-  extends ObservableQueryFields<TData, TVariables> {
+export interface QueryResult<
+  TData = any,
+  TVariables extends OperationVariables = OperationVariables
+> extends ObservableQueryFields<TData, TVariables> {
   client: ApolloClient<any>;
   observable: ObservableQuery<TData, TVariables>;
   data: TData | undefined;
@@ -75,21 +90,106 @@ export interface QueryResult<TData = any, TVariables extends OperationVariables
   called: boolean;
 }
 
-export interface QueryDataOptions<TData = any, TVariables extends OperationVariables = OperationVariables>
-  extends QueryFunctionOptions<TData, TVariables> {
+export interface QueryDataOptions<
+  TData = any,
+  TVariables extends OperationVariables = OperationVariables
+> extends QueryFunctionOptions<TData, TVariables> {
   children?: (result: QueryResult<TData, TVariables>) => ReactNode;
   query: DocumentNode | TypedDocumentNode<TData, TVariables>;
 }
 
-export interface QueryHookOptions<TData = any, TVariables extends OperationVariables = OperationVariables>
-  extends QueryFunctionOptions<TData, TVariables> {
-  query?: DocumentNode | TypedDocumentNode<TData, TVariables>;
-}
+export interface QueryHookOptions<
+  TData = any,
+  TVariables extends OperationVariables = OperationVariables
+> extends QueryFunctionOptions<TData, TVariables> {}
 
 export interface LazyQueryHookOptions<
   TData = any,
   TVariables extends OperationVariables = OperationVariables
-> extends Omit<QueryHookOptions<TData, TVariables>, 'skip'> {}
+> extends Omit<QueryHookOptions<TData, TVariables>, "skip"> {}
+
+export interface LazyQueryHookExecOptions<
+  TData = any,
+  TVariables extends OperationVariables = OperationVariables
+> extends LazyQueryHookOptions<TData, TVariables> {
+  query?: DocumentNode | TypedDocumentNode<TData, TVariables>;
+}
+
+export type SuspenseQueryHookFetchPolicy = Extract<
+  WatchQueryFetchPolicy,
+  "cache-first" | "network-only" | "no-cache" | "cache-and-network"
+>;
+
+export interface SuspenseQueryHookOptions<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+> extends Pick<
+    QueryHookOptions<TData, TVariables>,
+    | "client"
+    | "variables"
+    | "errorPolicy"
+    | "context"
+    | "canonizeResults"
+    | "returnPartialData"
+    | "refetchWritePolicy"
+  > {
+  fetchPolicy?: SuspenseQueryHookFetchPolicy;
+  suspenseCache?: SuspenseCache;
+  queryKey?: string | number | any[];
+
+  /**
+   * If `true`, the query is not executed. The default value is `false`.
+   *
+   * @deprecated We recommend using `skipToken` in place of the `skip` option as
+   * it is more type-safe.
+   *
+   * @example Recommended usage of `skipToken`:
+   * ```ts
+   * import { skipToken, useSuspenseQuery } from '@apollo/client';
+   *
+   * const { data } = useSuspenseQuery(query, id ? { variables: { id } } : skipToken);
+   * ```
+   */
+  skip?: boolean;
+}
+
+export type BackgroundQueryHookFetchPolicy = Extract<
+  WatchQueryFetchPolicy,
+  "cache-first" | "network-only" | "no-cache" | "cache-and-network"
+>;
+
+export interface BackgroundQueryHookOptions<
+  TData = unknown,
+  TVariables extends OperationVariables = OperationVariables
+> extends Pick<
+    QueryHookOptions<TData, TVariables>,
+    | "client"
+    | "variables"
+    | "errorPolicy"
+    | "context"
+    | "canonizeResults"
+    | "returnPartialData"
+    | "refetchWritePolicy"
+  > {
+  fetchPolicy?: BackgroundQueryHookFetchPolicy;
+  suspenseCache?: SuspenseCache;
+  queryKey?: string | number | any[];
+
+  /**
+   * If `true`, the query is not executed. The default value is `false`.
+   *
+   * @deprecated We recommend using `skipToken` in place of the `skip` option as
+   * it is more type-safe.
+   *
+   * @example Recommended usage of `skipToken`:
+   * ```ts
+   * import { skipToken, useBackgroundQuery } from '@apollo/client';
+   *
+   * const [queryRef] = useBackgroundQuery(query, id ? { variables: { id } } : skipToken);
+   * ```
+   */
+  skip?: boolean;
+}
 
 /**
  * @deprecated TODO Delete this unused interface.
@@ -102,22 +202,30 @@ export interface QueryLazyOptions<TVariables> {
 /**
  * @deprecated TODO Delete this unused type alias.
  */
-export type LazyQueryResult<TData, TVariables extends OperationVariables> = QueryResult<TData, TVariables>;
+export type LazyQueryResult<
+  TData,
+  TVariables extends OperationVariables
+> = QueryResult<TData, TVariables>;
 
 /**
  * @deprecated TODO Delete this unused type alias.
  */
-export type QueryTuple<TData, TVariables extends OperationVariables> =
-  LazyQueryResultTuple<TData, TVariables>;
+export type QueryTuple<
+  TData,
+  TVariables extends OperationVariables
+> = LazyQueryResultTuple<TData, TVariables>;
 
-export type LazyQueryExecFunction<TData, TVariables extends OperationVariables> = (
-  options?: Partial<LazyQueryHookOptions<TData, TVariables>>,
+export type LazyQueryExecFunction<
+  TData,
+  TVariables extends OperationVariables
+> = (
+  options?: Partial<LazyQueryHookExecOptions<TData, TVariables>>
 ) => Promise<QueryResult<TData, TVariables>>;
 
-export type LazyQueryResultTuple<TData, TVariables extends OperationVariables> = [
-  LazyQueryExecFunction<TData, TVariables>,
-  QueryResult<TData, TVariables>,
-];
+export type LazyQueryResultTuple<
+  TData,
+  TVariables extends OperationVariables
+> = [LazyQueryExecFunction<TData, TVariables>, QueryResult<TData, TVariables>];
 
 /* Mutation types */
 
@@ -131,9 +239,9 @@ export interface BaseMutationOptions<
   TContext = DefaultContext,
   TCache extends ApolloCache<any> = ApolloCache<any>
 > extends Omit<
-  MutationOptions<TData, TVariables, TContext, TCache>,
-  | "mutation"
-> {
+    MutationOptions<TData, TVariables, TContext, TCache>,
+    "mutation"
+  > {
   client?: ApolloClient<object>;
   notifyOnNetworkStatusChange?: boolean;
   onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void;
@@ -145,7 +253,7 @@ export interface MutationFunctionOptions<
   TData = any,
   TVariables = OperationVariables,
   TContext = DefaultContext,
-  TCache extends ApolloCache<any> = ApolloCache<any>,
+  TCache extends ApolloCache<any> = ApolloCache<any>
 > extends BaseMutationOptions<TData, TVariables, TContext, TCache> {
   mutation?: DocumentNode | TypedDocumentNode<TData, TVariables>;
 }
@@ -163,7 +271,7 @@ export declare type MutationFunction<
   TData = any,
   TVariables = OperationVariables,
   TContext = DefaultContext,
-  TCache extends ApolloCache<any> = ApolloCache<any>,
+  TCache extends ApolloCache<any> = ApolloCache<any>
 > = (
   options?: MutationFunctionOptions<TData, TVariables, TContext, TCache>
 ) => Promise<FetchResult<TData>>;
@@ -172,16 +280,14 @@ export interface MutationHookOptions<
   TData = any,
   TVariables = OperationVariables,
   TContext = DefaultContext,
-  TCache extends ApolloCache<any> = ApolloCache<any>,
-> extends BaseMutationOptions<TData, TVariables, TContext, TCache> {
-  mutation?: DocumentNode | TypedDocumentNode<TData, TVariables>;
-}
+  TCache extends ApolloCache<any> = ApolloCache<any>
+> extends BaseMutationOptions<TData, TVariables, TContext, TCache> {}
 
 export interface MutationDataOptions<
   TData = any,
   TVariables = OperationVariables,
   TContext = DefaultContext,
-  TCache extends ApolloCache<any> = ApolloCache<any>,
+  TCache extends ApolloCache<any> = ApolloCache<any>
 > extends BaseMutationOptions<TData, TVariables, TContext, TCache> {
   mutation: DocumentNode | TypedDocumentNode<TData, TVariables>;
 }
@@ -190,14 +296,14 @@ export type MutationTuple<
   TData,
   TVariables,
   TContext = DefaultContext,
-  TCache extends ApolloCache<any> = ApolloCache<any>,
+  TCache extends ApolloCache<any> = ApolloCache<any>
 > = [
   (
     options?: MutationFunctionOptions<TData, TVariables, TContext, TCache>
     // TODO This FetchResult<TData> seems strange here, as opposed to an
     // ApolloQueryResult<TData>
   ) => Promise<FetchResult<TData>>,
-  MutationResult<TData>,
+  MutationResult<TData>
 ];
 
 /* Subscription types */
@@ -227,13 +333,13 @@ export interface BaseSubscriptionOptions<
   onComplete?: () => void;
   onData?: (options: OnDataOptions<TData>) => any;
   /**
-  * @deprecated Use onData instead
-  */
+   * @deprecated Use onData instead
+   */
   onSubscriptionData?: (options: OnSubscriptionDataOptions<TData>) => any;
   onError?: (error: ApolloError) => void;
   /**
-  * @deprecated Use onComplete instead
-  */
+   * @deprecated Use onComplete instead
+   */
   onSubscriptionComplete?: () => void;
 }
 
@@ -249,9 +355,7 @@ export interface SubscriptionResult<TData = any, TVariables = any> {
 export interface SubscriptionHookOptions<
   TData = any,
   TVariables extends OperationVariables = OperationVariables
-> extends BaseSubscriptionOptions<TData, TVariables> {
-  subscription?: DocumentNode | TypedDocumentNode<TData, TVariables>;
-}
+> extends BaseSubscriptionOptions<TData, TVariables> {}
 
 export interface SubscriptionDataOptions<
   TData = any,
@@ -265,3 +369,30 @@ export interface SubscriptionCurrentObservable {
   query?: Observable<any>;
   subscription?: ObservableSubscription;
 }
+
+/**
+Helper type that allows using a type in a way that cannot be "widened" by inference on the value it is used on.
+
+This type was first suggested [in this Github discussion](https://github.com/microsoft/TypeScript/issues/14829#issuecomment-504042546).
+
+Example usage:
+```ts
+export function useQuery<
+  TData = any,
+  TVariables extends OperationVariables = OperationVariables,
+>(
+  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
+  options: QueryHookOptions<NoInfer<TData>, NoInfer<TVariables>> = Object.create(null),
+)
+```
+In this case, `TData` and `TVariables` should be inferred from `query`, but never widened from something in `options`.
+
+So, in this code example:
+```ts
+declare const typedNode: TypedDocumentNode<{ foo: string}, { bar: number }>
+const { variables } = useQuery(typedNode, { variables: { bar: 4, nonExistingVariable: "string" } });
+```
+Without the use of `NoInfer`, `variables` would now be of the type `{ bar: number, nonExistingVariable: "string" }`.
+With `NoInfer`, it will instead give an error on `nonExistingVariable`.
+ */
+export type NoInfer<T> = [T][T extends any ? 0 : never];
diff --git a/src/utilities/common/canUse.ts b/src/utilities/common/canUse.ts
--- a/src/utilities/common/canUse.ts
+++ b/src/utilities/common/canUse.ts
@@ -1,4 +1,4 @@
-import { maybe } from "../globals";
+import { maybe } from "../globals/index.js";
 
 export const canUseWeakMap =
   typeof WeakMap === 'function' &&
diff --git a/src/utilities/common/compact.ts b/src/utilities/common/compact.ts
--- a/src/utilities/common/compact.ts
+++ b/src/utilities/common/compact.ts
@@ -1,4 +1,4 @@
-import { TupleToIntersection } from './mergeDeep';
+import type { TupleToIntersection } from './mergeDeep.js';
 
 /**
  * Merges the provided objects shallowly and removes
diff --git a/src/utilities/common/errorHandling.ts b/src/utilities/common/errorHandling.ts
--- a/src/utilities/common/errorHandling.ts
+++ b/src/utilities/common/errorHandling.ts
@@ -1,6 +1,6 @@
-import { FetchResult } from "../../link/core";
-import { isNonEmptyArray } from "../../utilities/common/arrays";
-import { isExecutionPatchIncrementalResult } from "../../utilities/common/incrementalResult";
+import type { FetchResult } from "../../link/core/index.js";
+import { isNonEmptyArray } from "./arrays.js";
+import { isExecutionPatchIncrementalResult } from "./incrementalResult.js";
 
 export function graphQLResultHasError<T>(result: FetchResult<T>): boolean {
   const errors = getGraphQLErrorsFromResult(result);
diff --git a/src/utilities/common/incrementalResult.ts b/src/utilities/common/incrementalResult.ts
--- a/src/utilities/common/incrementalResult.ts
+++ b/src/utilities/common/incrementalResult.ts
@@ -1,13 +1,13 @@
-import {
+import type {
   ExecutionPatchIncrementalResult,
   ExecutionPatchInitialResult,
   ExecutionPatchResult,
   ApolloPayloadResult,
   FetchResult,
-} from "../../link/core";
-import { isNonNullObject } from "./objects";
-import { isNonEmptyArray } from "./arrays";
-import { DeepMerger } from "./mergeDeep";
+} from "../../link/core/index.js";
+import { isNonNullObject } from "./objects.js";
+import { isNonEmptyArray } from "./arrays.js";
+import { DeepMerger } from "./mergeDeep.js";
 
 export function isExecutionPatchIncrementalResult<T>(
   value: FetchResult<T>
diff --git a/src/utilities/common/maybeDeepFreeze.ts b/src/utilities/common/maybeDeepFreeze.ts
--- a/src/utilities/common/maybeDeepFreeze.ts
+++ b/src/utilities/common/maybeDeepFreeze.ts
@@ -1,5 +1,4 @@
-import '../globals'; // For __DEV__
-import { isNonNullObject } from './objects';
+import { isNonNullObject } from './objects.js';
 
 function deepFreeze(value: any) {
   const workSet = new Set([value]);
diff --git a/src/utilities/common/mergeDeep.ts b/src/utilities/common/mergeDeep.ts
--- a/src/utilities/common/mergeDeep.ts
+++ b/src/utilities/common/mergeDeep.ts
@@ -1,4 +1,4 @@
-import { isNonNullObject } from "./objects";
+import { isNonNullObject } from "./objects.js";
 
 const { hasOwnProperty } = Object.prototype;
 
diff --git a/src/utilities/common/mergeOptions.ts b/src/utilities/common/mergeOptions.ts
--- a/src/utilities/common/mergeOptions.ts
+++ b/src/utilities/common/mergeOptions.ts
@@ -3,9 +3,9 @@ import type {
   WatchQueryOptions,
   MutationOptions,
   OperationVariables,
-} from "../../core";
+} from "../../core/index.js";
 
-import { compact } from "./compact";
+import { compact } from "./compact.js";
 
 type OptionsUnion<TData, TVariables extends OperationVariables, TContext> =
   | WatchQueryOptions<TVariables, TData>
@@ -13,15 +13,16 @@ type OptionsUnion<TData, TVariables extends OperationVariables, TContext> =
   | MutationOptions<TData, TVariables, TContext>;
 
 export function mergeOptions<
-  TOptions extends Partial<OptionsUnion<any, any, any>>
+  TDefaultOptions extends Partial<OptionsUnion<any, any, any>>,
+  TOptions extends TDefaultOptions
 >(
-  defaults: TOptions | Partial<TOptions> | undefined,
+  defaults: TDefaultOptions | Partial<TDefaultOptions> | undefined,
   options: TOptions | Partial<TOptions>,
-): TOptions {
+): TOptions & TDefaultOptions {
   return compact(defaults, options, options.variables && {
-    variables: {
+    variables: compact({
       ...(defaults && defaults.variables),
       ...options.variables,
-    },
+    }),
   });
 }
diff --git a/src/utilities/common/objects.ts b/src/utilities/common/objects.ts
--- a/src/utilities/common/objects.ts
+++ b/src/utilities/common/objects.ts
@@ -1,3 +1,12 @@
 export function isNonNullObject(obj: any): obj is Record<string | number, any> {
   return obj !== null && typeof obj === 'object';
 }
+
+export function isPlainObject(obj: any): obj is Record<string | number, any> {
+  return (
+    obj !== null &&
+    typeof obj === 'object' &&
+    (Object.getPrototypeOf(obj) === Object.prototype ||
+      Object.getPrototypeOf(obj) === null)
+  );
+}
diff --git a/src/utilities/common/omitDeep.ts b/src/utilities/common/omitDeep.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/common/omitDeep.ts
@@ -0,0 +1,55 @@
+import type { DeepOmit } from "../types/DeepOmit.js";
+import { isPlainObject } from "./objects.js";
+
+export function omitDeep<T, K extends string>(value: T, key: K) {
+  return __omitDeep(value, key);
+}
+
+function __omitDeep<T, K extends string>(
+  value: T,
+  key: K,
+  known = new Map<any, any>()
+): DeepOmit<T, K> {
+  if (known.has(value)) {
+    return known.get(value);
+  }
+
+  let modified = false;
+
+  if (Array.isArray(value)) {
+    const array: any[] = [];
+    known.set(value, array);
+
+    value.forEach((value, index) => {
+      const result = __omitDeep(value, key, known);
+      modified ||= result !== value;
+
+      array[index] = result;
+    });
+
+    if (modified) {
+      return array as DeepOmit<T, K>;
+    }
+  } else if (isPlainObject(value)) {
+    const obj = Object.create(Object.getPrototypeOf(value));
+    known.set(value, obj);
+
+    Object.keys(value).forEach((k) => {
+      if (k === key) {
+        modified = true;
+        return;
+      }
+
+      const result = __omitDeep(value[k], key, known);
+      modified ||= result !== value[k];
+
+      obj[k] = result;
+    });
+
+    if (modified) {
+      return obj;
+    }
+  }
+
+  return value as DeepOmit<T, K>;
+}
diff --git a/src/utilities/common/responseIterator.ts b/src/utilities/common/responseIterator.ts
--- a/src/utilities/common/responseIterator.ts
+++ b/src/utilities/common/responseIterator.ts
@@ -1,6 +1,6 @@
-import { Response as NodeResponse } from "node-fetch";
-import { Readable as NodeReadableStream } from "stream";
-import { canUseAsyncIteratorSymbol } from "./canUse";
+import type { Response as NodeResponse } from "node-fetch";
+import type { Readable as NodeReadableStream } from "stream";
+import { canUseAsyncIteratorSymbol } from "./canUse.js";
 
 export function isNodeResponse(value: any): value is NodeResponse {
   return !!(value as NodeResponse).body;
diff --git a/src/utilities/common/stringifyForDisplay.ts b/src/utilities/common/stringifyForDisplay.ts
--- a/src/utilities/common/stringifyForDisplay.ts
+++ b/src/utilities/common/stringifyForDisplay.ts
@@ -1,8 +1,8 @@
-import { makeUniqueId } from "./makeUniqueId";
+import { makeUniqueId } from "./makeUniqueId.js";
 
-export function stringifyForDisplay(value: any): string {
+export function stringifyForDisplay(value: any, space = 0): string {
   const undefId = makeUniqueId("stringifyForDisplay");
   return JSON.stringify(value, (key, value) => {
     return value === void 0 ? undefId : value;
-  }).split(JSON.stringify(undefId)).join("<undefined>");
+  }, space).split(JSON.stringify(undefId)).join("<undefined>");
 }
diff --git a/src/utilities/common/stripTypename.ts b/src/utilities/common/stripTypename.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/common/stripTypename.ts
@@ -0,0 +1,5 @@
+import { omitDeep } from "./omitDeep.js";
+
+export function stripTypename<T>(value: T) {
+  return omitDeep(value, "__typename");
+}
diff --git a/src/utilities/globals/DEV.ts b/src/utilities/globals/DEV.ts
deleted file mode 100644
--- a/src/utilities/globals/DEV.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import global from "./global";
-import { maybe } from "./maybe";
-
-// To keep string-based find/replace minifiers from messing with __DEV__ inside
-// string literals or properties like global.__DEV__, we construct the "__DEV__"
-// string in a roundabout way that won't be altered by find/replace strategies.
-const __ = "__";
-const GLOBAL_KEY = [__, __].join("DEV");
-
-function getDEV() {
-  try {
-    return Boolean(__DEV__);
-  } catch {
-    Object.defineProperty(global, GLOBAL_KEY, {
-      // In a buildless browser environment, maybe(() => process.env.NODE_ENV)
-      // evaluates as undefined, so __DEV__ becomes true by default, but can be
-      // initialized to false instead by a script/module that runs earlier.
-      value: maybe(() => process.env.NODE_ENV) !== "production",
-      enumerable: false,
-      configurable: true,
-      writable: true,
-    });
-    // Using computed property access rather than global.__DEV__ here prevents
-    // string-based find/replace strategies from munging this to global.false:
-    return (global as any)[GLOBAL_KEY];
-  }
-}
-
-export default getDEV();
diff --git a/src/utilities/globals/fix-graphql.ts b/src/utilities/globals/fix-graphql.ts
deleted file mode 100644
--- a/src/utilities/globals/fix-graphql.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-// The ordering of these imports is important, because it ensures the temporary
-// process.env.NODE_ENV polyfill is defined globally (if necessary) before we
-// import { Source } from 'graphql'. The instanceOf function that we really care
-// about (the one that uses process.env.NODE_ENV) is not exported from the
-// top-level graphql package, but graphql/language/source uses instanceOf, and
-// has relatively few dependencies, so importing it here should not increase
-// bundle sizes as much as other options.
-import { remove } from 'ts-invariant/process';
-import { Source } from 'graphql';
-
-export function removeTemporaryGlobals() {
-  // Using Source here here just to make sure it won't be tree-shaken away.
-  return typeof Source === "function" ? remove() : remove();
-}
diff --git a/src/utilities/globals/global.ts b/src/utilities/globals/global.ts
--- a/src/utilities/globals/global.ts
+++ b/src/utilities/globals/global.ts
@@ -1,23 +1,10 @@
-import { maybe } from "./maybe";
+import { maybe } from "./maybe.js";
 
 declare global {
-  // Despite our attempts to reuse the React Native __DEV__ constant instead of
-  // inventing something new and Apollo-specific, declaring a useful type for
-  // __DEV__ unfortunately conflicts (TS2451) with the global declaration in
-  // @types/react-native/index.d.ts.
-  //
-  // To hide that harmless conflict, we @ts-ignore this line, which should
-  // continue to provide a type for __DEV__ elsewhere in the Apollo Client
-  // codebase, even when @types/react-native is not in use.
-  //
-  // However, because TypeScript drops @ts-ignore comments when generating .d.ts
-  // files (https://github.com/microsoft/TypeScript/issues/38628), we also
-  // sanitize the dist/utilities/globals/global.d.ts file to avoid declaring
-  // __DEV__ globally altogether when @apollo/client is installed in the
-  // node_modules directory of an application.
-  //
-  // @ts-ignore
-  const __DEV__: boolean | undefined;
+  const __DEV__: boolean; // will be removed in `dist` by the `postprocessDist` script
+	interface Window {
+		__DEV__?: boolean;
+	}
 }
 
 export default (
@@ -32,6 +19,4 @@ export default (
   // improve your static analysis to detect this obfuscation, think again. This
   // is an arms race you cannot win, at least not in JavaScript.
   maybe(function() { return maybe.constructor("return this")() })
-) as typeof globalThis & {
-  __DEV__: typeof __DEV__;
-};
+) as typeof globalThis & Window;
diff --git a/src/utilities/globals/index.ts b/src/utilities/globals/index.ts
--- a/src/utilities/globals/index.ts
+++ b/src/utilities/globals/index.ts
@@ -1,25 +1,15 @@
-import { invariant, InvariantError } from "ts-invariant";
-
-// Just in case the graphql package switches from process.env.NODE_ENV to
-// __DEV__, make sure __DEV__ is polyfilled before importing graphql.
-import DEV from "./DEV";
-export { DEV }
-export function checkDEV() {
-  invariant("boolean" === typeof DEV, DEV);
-}
-
-// Import graphql/jsutils/instanceOf safely, working around its unchecked usage
-// of process.env.NODE_ENV and https://github.com/graphql/graphql-js/pull/2894.
-import { removeTemporaryGlobals } from "./fix-graphql";
-
-// Synchronously undo the global process.env.NODE_ENV polyfill that we created
-// temporarily while importing the offending graphql/jsutils/instanceOf module.
-removeTemporaryGlobals();
-
-export { maybe } from "./maybe";
-export { default as global } from "./global";
-export { invariant, InvariantError }
-
-// Ensure __DEV__ was properly initialized, and prevent tree-shaking bundlers
-// from mistakenly pruning the ./DEV module (see issue #8674).
-checkDEV();
+import { invariant, newInvariantError, InvariantError } from "./invariantWrappers.js";
+
+export { maybe } from "./maybe.js";
+export { default as global } from "./global.js";
+export { invariant, newInvariantError, InvariantError }
+
+/** 
+ * @deprecated we do not use this internally anymore, 
+ * it is just exported for backwards compatibility 
+ */
+// this file is extempt from automatic `__DEV__` replacement 
+// so we have to write it out here 
+// @ts-ignore
+export const DEV = globalThis.__DEV__ !== false;
+export { DEV as __DEV__ };
\ No newline at end of file
diff --git a/src/utilities/globals/invariantWrappers.ts b/src/utilities/globals/invariantWrappers.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/globals/invariantWrappers.ts
@@ -0,0 +1,128 @@
+import { invariant as originalInvariant, InvariantError } from "ts-invariant";
+import { version } from "../../version.js";
+import global from "./global.js";
+import type { ErrorCodes } from "../../invariantErrorCodes.js";
+import { stringifyForDisplay } from "../common/stringifyForDisplay.js";
+
+function wrap(fn: (msg?: string, ...args: any[]) => void) {
+  return function (message: string | number, ...args: any[]) {
+    fn(typeof message === "number" ? getErrorMsg(message) : message, ...args);
+  };
+}
+
+type LogFunction = {
+  /**
+   * Logs a `$level` message if the user used `ts-invariant`'s `setVerbosity` to set
+   * a verbosity level of `$level` or lower. (defaults to `"log"`).
+   *
+   * The user will either be presented with a link to the documentation for the message,
+   * or they can use the `loadDevMessages` to add the message strings to the bundle.
+   * The documentation will display the message without argument substitution.
+   * Instead, the arguments will be printed on the console after the link.
+   *
+   * `message` can only be a string, a concatenation of strings, or a ternary statement
+   * that results in a string. This will be enforced on build, where the message will
+   * be replaced with a message number.
+   *
+   * String substitutions like %s, %o, %d or %f are supported.
+   */
+  (message?: any, ...optionalParams: unknown[]): void;
+};
+
+type WrappedInvariant = {
+  /**
+   * Throws and InvariantError with the given message if the condition is false.
+   *
+   * `message` can only be a string, a concatenation of strings, or a ternary statement
+   * that results in a string. This will be enforced on build, where the message will
+   * be replaced with a message number.
+   *
+   * The user will either be presented with a link to the documentation for the message,
+   * or they can use the `loadErrorMessages` to add the message strings to the bundle.
+   * The documentation will display the message with the arguments substituted.
+   *
+   * String substitutions with %s are supported and will also return
+   * pretty-stringified objects.
+   * Excess `optionalParams` will be swallowed.
+   */
+  (
+    condition: any,
+    message?: string | number,
+    ...optionalParams: unknown[]
+  ): asserts condition;
+
+  debug: LogFunction;
+  log: LogFunction;
+  warn: LogFunction;
+  error: LogFunction;
+};
+const invariant: WrappedInvariant = Object.assign(
+  function invariant(
+    condition: any,
+    message?: string | number,
+    ...args: unknown[]
+  ): asserts condition {
+    if (!condition) {
+      originalInvariant(condition, getErrorMsg(message, args));
+    }
+  },
+  {
+    debug: wrap(originalInvariant.debug),
+    log: wrap(originalInvariant.log),
+    warn: wrap(originalInvariant.warn),
+    error: wrap(originalInvariant.error),
+  }
+);
+
+/**
+ * Returns an InvariantError.
+ *
+ * `message` can only be a string, a concatenation of strings, or a ternary statement
+ * that results in a string. This will be enforced on build, where the message will
+ * be replaced with a message number.
+ * String substitutions with %s are supported and will also return
+ * pretty-stringified objects.
+ * Excess `optionalParams` will be swallowed.
+ */
+function newInvariantError(
+  message?: string | number,
+  ...optionalParams: unknown[]
+) {
+  return new InvariantError(getErrorMsg(message, optionalParams));
+}
+
+const ApolloErrorMessageHandler = Symbol.for(
+  "ApolloErrorMessageHandler_" + version
+);
+declare global {
+  interface Window {
+    [ApolloErrorMessageHandler]?: {
+      (message: string | number, args: unknown[]): string | undefined;
+    } & ErrorCodes;
+  }
+}
+
+function getErrorMsg(message?: string | number, messageArgs: unknown[] = []) {
+  if (!message) return;
+  const args = messageArgs.map((arg) =>
+    typeof arg == "string" ? arg : stringifyForDisplay(arg, 2).slice(0, 1000)
+  );
+  return (
+    (global[ApolloErrorMessageHandler] &&
+      global[ApolloErrorMessageHandler](message, args)) ||
+    `An error occured! For more details, see the full error text at https://go.apollo.dev/c/err#${encodeURIComponent(
+      JSON.stringify({
+        version,
+        message,
+        args,
+      })
+    )}`
+  );
+}
+
+export {
+  invariant,
+  InvariantError,
+  newInvariantError,
+  ApolloErrorMessageHandler,
+};
diff --git a/src/utilities/graphql/DocumentTransform.ts b/src/utilities/graphql/DocumentTransform.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/graphql/DocumentTransform.ts
@@ -0,0 +1,132 @@
+import { Trie } from "@wry/trie";
+import { canUseWeakMap, canUseWeakSet } from "../common/canUse.js";
+import { checkDocument } from "./getFromAST.js";
+import { invariant } from "../globals/index.js";
+import type { DocumentNode } from "graphql";
+
+export type DocumentTransformCacheKey = ReadonlyArray<unknown>;
+
+type TransformFn = (document: DocumentNode) => DocumentNode;
+
+interface DocumentTransformOptions {
+  cache?: boolean;
+  getCacheKey?: (
+    document: DocumentNode
+  ) => DocumentTransformCacheKey | undefined;
+}
+
+function identity(document: DocumentNode) {
+  return document;
+}
+
+export class DocumentTransform {
+  private readonly transform: TransformFn;
+
+  private readonly resultCache = canUseWeakSet
+    ? new WeakSet<DocumentNode>()
+    : new Set<DocumentNode>();
+
+  private stableCacheKeys:
+    | Trie<{ key: DocumentTransformCacheKey; value?: DocumentNode }>
+    | undefined;
+
+  // This default implementation of getCacheKey can be overridden by providing
+  // options.getCacheKey to the DocumentTransform constructor. In general, a
+  // getCacheKey function may either return an array of keys (often including
+  // the document) to be used as a cache key, or undefined to indicate the
+  // transform for this document should not be cached.
+  private getCacheKey(
+    document: DocumentNode
+  ): DocumentTransformCacheKey | undefined {
+    return [document];
+  }
+
+  static identity() {
+    // No need to cache this transform since it just returns the document
+    // unchanged. This should save a bit of memory that would otherwise be
+    // needed to populate the `documentCache` of this transform.
+    return new DocumentTransform(identity, { cache: false });
+  }
+
+  static split(
+    predicate: (document: DocumentNode) => boolean,
+    left: DocumentTransform,
+    right: DocumentTransform = DocumentTransform.identity()
+  ) {
+    return new DocumentTransform(
+      (document) => {
+        const documentTransform = predicate(document) ? left : right;
+
+        return documentTransform.transformDocument(document);
+      },
+      // Reasonably assume both `left` and `right` transforms handle their own caching
+      { cache: false }
+    );
+  }
+
+  constructor(
+    transform: TransformFn,
+    options: DocumentTransformOptions = Object.create(null)
+  ) {
+    this.transform = transform;
+
+    if (options.getCacheKey) {
+      // Override default `getCacheKey` function, which returns [document].
+      this.getCacheKey = options.getCacheKey;
+    }
+
+    if (options.cache !== false) {
+      this.stableCacheKeys = new Trie(canUseWeakMap, (key) => ({ key }));
+    }
+  }
+
+  transformDocument(document: DocumentNode) {
+    // If a user passes an already transformed result back to this function,
+    // immediately return it.
+    if (this.resultCache.has(document)) {
+      return document;
+    }
+
+    const cacheEntry = this.getStableCacheEntry(document);
+
+    if (cacheEntry && cacheEntry.value) {
+      return cacheEntry.value;
+    }
+
+    checkDocument(document);
+
+    const transformedDocument = this.transform(document);
+
+    this.resultCache.add(transformedDocument);
+
+    if (cacheEntry) {
+      cacheEntry.value = transformedDocument;
+    }
+
+    return transformedDocument;
+  }
+
+  concat(otherTransform: DocumentTransform) {
+    return new DocumentTransform(
+      (document) => {
+        return otherTransform.transformDocument(
+          this.transformDocument(document)
+        );
+      },
+      // Reasonably assume both transforms handle their own caching
+      { cache: false }
+    );
+  }
+
+  getStableCacheEntry(document: DocumentNode) {
+    if (!this.stableCacheKeys) return;
+    const cacheKeys = this.getCacheKey(document);
+    if (cacheKeys) {
+      invariant(
+        Array.isArray(cacheKeys),
+        "`getCacheKey` must return an array or undefined"
+      );
+      return this.stableCacheKeys.lookupArray(cacheKeys);
+    }
+  }
+}
diff --git a/src/utilities/graphql/directives.ts b/src/utilities/graphql/directives.ts
--- a/src/utilities/graphql/directives.ts
+++ b/src/utilities/graphql/directives.ts
@@ -1,8 +1,8 @@
-import { invariant } from '../globals';
+import { invariant } from '../globals/index.js';
 
 // Provides the methods that allow QueryManager to handle the `skip` and
 // `include` directives within GraphQL.
-import {
+import type {
   SelectionNode,
   VariableNode,
   BooleanValueNode,
@@ -10,7 +10,8 @@ import {
   DocumentNode,
   ArgumentNode,
   ValueNode,
-  ASTNode,
+  ASTNode} from 'graphql';
+import {
   visit,
   BREAK,
 } from 'graphql';
@@ -34,7 +35,8 @@ export function shouldInclude(
       evaledValue = variables && variables[(ifArgument.value as VariableNode).name.value];
       invariant(
         evaledValue !== void 0,
-        `Invalid variable referenced in @${directive.name.value} directive.`,
+        `Invalid variable referenced in @%s directive.`,
+        directive.name.value
       );
     } else {
       evaledValue = (ifArgument.value as BooleanValueNode).value;
@@ -116,13 +118,15 @@ export function getInclusionDirectives(
 
       invariant(
         directiveArguments && directiveArguments.length === 1,
-        `Incorrect number of arguments for the @${directiveName} directive.`,
+        `Incorrect number of arguments for the @%s directive.`,
+        directiveName
       );
 
       const ifArgument = directiveArguments![0];
       invariant(
         ifArgument.name && ifArgument.name.value === 'if',
-        `Invalid argument for the @${directiveName} directive.`,
+        `Invalid argument for the @%s directive.`,
+        directiveName
       );
 
       const ifValue: ValueNode = ifArgument.value;
@@ -131,7 +135,8 @@ export function getInclusionDirectives(
       invariant(
         ifValue &&
           (ifValue.kind === 'Variable' || ifValue.kind === 'BooleanValue'),
-        `Argument for the @${directiveName} directive must be a variable or a boolean value.`,
+        `Argument for the @%s directive must be a variable or a boolean value.`,
+        directiveName
       );
 
       result.push({ directive, ifArgument });
diff --git a/src/utilities/graphql/fragments.ts b/src/utilities/graphql/fragments.ts
--- a/src/utilities/graphql/fragments.ts
+++ b/src/utilities/graphql/fragments.ts
@@ -1,6 +1,6 @@
-import { invariant, InvariantError } from '../globals';
+import { invariant, newInvariantError } from '../globals/index.js';
 
-import {
+import type {
   DocumentNode,
   FragmentDefinitionNode,
   InlineFragmentNode,
@@ -46,11 +46,11 @@ export function getFragmentQueryDocument(
     // Throw an error if we encounter an operation definition because we will
     // define our own operation definition later on.
     if (definition.kind === 'OperationDefinition') {
-      throw new InvariantError(
-        `Found a ${definition.operation} operation${
-          definition.name ? ` named '${definition.name.value}'` : ''
-        }. ` +
+      throw newInvariantError(
+        `Found a %s operation%s. ` +
           'No operations are allowed when using a fragment as a query. Only fragments are allowed.',
+        definition.operation,
+        definition.name ? ` named '${definition.name.value}'` : ''
       );
     }
     // Add our definition to the fragments array if it is a fragment
@@ -65,9 +65,8 @@ export function getFragmentQueryDocument(
   if (typeof actualFragmentName === 'undefined') {
     invariant(
       fragments.length === 1,
-      `Found ${
-        fragments.length
-      } fragments. \`fragmentName\` must be provided when there is not exactly 1 fragment.`,
+      `Found %s fragments. \`fragmentName\` must be provided when there is not exactly 1 fragment.`,
+      fragments.length
     );
     actualFragmentName = fragments[0].name.value;
   }
@@ -136,7 +135,7 @@ export function getFragmentFromSelection(
         return fragmentMap(fragmentName);
       }
       const fragment = fragmentMap && fragmentMap[fragmentName];
-      invariant(fragment, `No fragment named ${fragmentName}`);
+      invariant(fragment, `No fragment named %s`, fragmentName);
       return fragment || null;
     }
     default:
diff --git a/src/utilities/graphql/getFromAST.ts b/src/utilities/graphql/getFromAST.ts
--- a/src/utilities/graphql/getFromAST.ts
+++ b/src/utilities/graphql/getFromAST.ts
@@ -1,13 +1,13 @@
-import { invariant, InvariantError } from '../globals';
+import { invariant, newInvariantError } from '../globals/index.js';
 
-import {
+import type {
   DocumentNode,
   OperationDefinitionNode,
   FragmentDefinitionNode,
   ValueNode,
 } from 'graphql';
 
-import { valueToObjectRepresentation } from './storeUtils';
+import { valueToObjectRepresentation } from './storeUtils.js';
 
 type OperationDefinitionWithName = OperationDefinitionNode & {
   name: NonNullable<OperationDefinitionNode['name']>;
@@ -25,10 +25,9 @@ string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`,
     .filter(d => d.kind !== 'FragmentDefinition')
     .map(definition => {
       if (definition.kind !== 'OperationDefinition') {
-        throw new InvariantError(
-          `Schema type definitions not allowed in queries. Found: "${
-            definition.kind
-          }"`,
+        throw newInvariantError(
+          `Schema type definitions not allowed in queries. Found: "%s"`,
+          definition.kind
         );
       }
       return definition;
@@ -36,7 +35,8 @@ string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`,
 
   invariant(
     operations.length <= 1,
-    `Ambiguous GraphQL document: contains ${operations.length} operations`,
+    `Ambiguous GraphQL document: contains %s operations`,
+    operations.length
   );
 
   return doc;
@@ -142,7 +142,7 @@ export function getMainDefinition(
     return fragmentDefinition;
   }
 
-  throw new InvariantError(
+  throw newInvariantError(
     'Expected a parsed GraphQL query with a query, mutation, subscription, or a fragment.',
   );
 }
diff --git a/src/utilities/graphql/operations.ts b/src/utilities/graphql/operations.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/graphql/operations.ts
@@ -0,0 +1,21 @@
+import type { DocumentNode } from "../../core/index.js";
+import { getOperationDefinition } from "./getFromAST.js";
+
+function isOperation(
+  document: DocumentNode,
+  operation: "query" | "mutation" | "subscription"
+) {
+  return getOperationDefinition(document)?.operation === operation;
+}
+
+export function isMutationOperation(document: DocumentNode) {
+  return isOperation(document, "mutation");
+}
+
+export function isQueryOperation(document: DocumentNode) {
+  return isOperation(document, "query");
+}
+
+export function isSubscriptionOperation(document: DocumentNode) {
+  return isOperation(document, "subscription");
+}
diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/graphql/print.ts
@@ -0,0 +1,14 @@
+import { print as origPrint } from "graphql";
+import { canUseWeakMap } from "../common/canUse.js";
+
+const printCache = canUseWeakMap ? new WeakMap() : undefined;
+export const print: typeof origPrint = (ast) => {
+  let result;
+  result = printCache?.get(ast);
+
+  if (!result) {
+    result = origPrint(ast);
+    printCache?.set(ast, result);
+  }
+  return result;
+};
diff --git a/src/utilities/graphql/storeUtils.ts b/src/utilities/graphql/storeUtils.ts
--- a/src/utilities/graphql/storeUtils.ts
+++ b/src/utilities/graphql/storeUtils.ts
@@ -1,6 +1,6 @@
-import { InvariantError } from '../globals';
+import { newInvariantError } from '../globals/index.js';
 
-import {
+import type {
   DirectiveNode,
   FieldNode,
   IntValueNode,
@@ -21,8 +21,9 @@ import {
   FragmentSpreadNode,
 } from 'graphql';
 
-import { isNonNullObject } from '../common/objects';
-import { FragmentMap, getFragmentFromSelection } from './fragments';
+import { isNonNullObject } from '../common/objects.js';
+import type { FragmentMap} from './fragments.js';
+import { getFragmentFromSelection } from './fragments.js';
 
 export interface Reference {
   readonly __ref: string;
@@ -131,10 +132,11 @@ export function valueToObjectRepresentation(
   } else if (isNullValue(value)) {
     argObj[name.value] = null;
   } else {
-    throw new InvariantError(
-      `The inline argument "${name.value}" of kind "${(value as any).kind}"` +
+    throw newInvariantError(
+      `The inline argument "%s" of kind "%s"` +
         'is not supported. Use variables instead of inline arguments to ' +
         'overcome this limitation.',
+        name.value, (value as any).kind
     );
   }
 }
@@ -186,6 +188,7 @@ const KNOWN_DIRECTIVES: string[] = [
   'client',
   'rest',
   'export',
+  'nonreactive',
 ];
 
 export const getStoreKeyName = Object.assign(function (
diff --git a/src/utilities/graphql/transform.ts b/src/utilities/graphql/transform.ts
--- a/src/utilities/graphql/transform.ts
+++ b/src/utilities/graphql/transform.ts
@@ -1,6 +1,6 @@
-import { invariant } from '../globals';
+import { invariant } from '../globals/index.js';
 
-import {
+import type {
   DocumentNode,
   SelectionNode,
   SelectionSetNode,
@@ -11,11 +11,12 @@ import {
   ArgumentNode,
   FragmentSpreadNode,
   VariableDefinitionNode,
-  visit,
   ASTNode,
-  Kind,
   ASTVisitor,
-  InlineFragmentNode,
+  InlineFragmentNode} from 'graphql';
+import {
+  visit,
+  Kind
 } from 'graphql';
 
 import {
@@ -24,13 +25,14 @@ import {
   getFragmentDefinition,
   getFragmentDefinitions,
   getMainDefinition,
-} from './getFromAST';
-import { isField } from './storeUtils';
+} from './getFromAST.js';
+import { isField } from './storeUtils.js';
+import type {
+  FragmentMap} from './fragments.js';
 import {
-  createFragmentMap,
-  FragmentMap,
-} from './fragments';
-import { isArray } from '../common/arrays';
+  createFragmentMap
+} from './fragments.js';
+import { isArray, isNonEmptyArray } from '../common/arrays.js';
 
 export type RemoveNodeConfig<N> = {
   name?: string;
@@ -83,22 +85,39 @@ function nullIfDocIsEmpty(doc: DocumentNode) {
 }
 
 function getDirectiveMatcher(
-  directives: (RemoveDirectiveConfig | GetDirectiveConfig)[],
+  configs: (RemoveDirectiveConfig | GetDirectiveConfig)[],
 ) {
-  const nameSet = new Set<string>();
-  const tests: Array<(directive: DirectiveNode) => boolean> = [];
-  directives.forEach(directive => {
-    if (directive.name) {
-      nameSet.add(directive.name);
-    } else if (directive.test) {
-      tests.push(directive.test);
+  const names = new Map<
+    string,
+    RemoveDirectiveConfig | GetDirectiveConfig
+  >();
+
+  const tests = new Map<
+    (directive: DirectiveNode) => boolean,
+    RemoveDirectiveConfig | GetDirectiveConfig
+  >();
+
+  configs.forEach(directive => {
+    if (directive) {
+      if (directive.name) {
+        names.set(directive.name, directive);
+      } else if (directive.test) {
+        tests.set(directive.test, directive);
+      }
     }
   });
 
-  return (directive: DirectiveNode) => (
-    nameSet.has(directive.name.value) ||
-    tests.some(test => test(directive))
-  );
+  return (directive: DirectiveNode) => {
+    let config = names.get(directive.name.value);
+    if (!config && tests.size) {
+      tests.forEach((testConfig, test) => {
+        if (test(directive)) {
+          config = testConfig;
+        }
+      });
+    }
+    return config;
+  };
 }
 
 // Helper interface and function used by removeDirectivesFromDocument to keep
@@ -138,6 +157,8 @@ export function removeDirectivesFromDocument(
   directives: RemoveDirectiveConfig[],
   doc: DocumentNode,
 ): DocumentNode | null {
+  checkDocument(doc);
+
   // Passing empty strings to makeInUseGetterFunction means we handle anonymous
   // operations as if their names were "". Anonymous fragment definitions are
   // not supposed to be possible, but the same default naming strategy seems
@@ -173,13 +194,10 @@ export function removeDirectivesFromDocument(
   }
 
   const directiveMatcher = getDirectiveMatcher(directives);
-  const hasRemoveDirective = directives.some(directive => directive.remove);
-  const shouldRemoveField = (
-    nodeDirectives: FieldNode["directives"]
-  ) => (
-    hasRemoveDirective &&
-    nodeDirectives &&
-    nodeDirectives.some(directiveMatcher)
+  const shouldRemoveField = (nodeDirectives: FieldNode["directives"]) => (
+    isNonEmptyArray(nodeDirectives) &&
+    nodeDirectives.map(directiveMatcher).some(
+      (config: RemoveDirectiveConfig | undefined) => config && config.remove)
   );
 
   const originalFragmentDefsByPath = new Map<string, FragmentDefinitionNode>();
diff --git a/src/utilities/index.ts b/src/utilities/index.ts
--- a/src/utilities/index.ts
+++ b/src/utilities/index.ts
@@ -1,8 +1,10 @@
-export { DEV, maybe } from './globals';
+export { DEV, maybe } from "./globals/index.js";
 
-export {
+export type {
   DirectiveInfo,
   InclusionDirectives,
+} from "./graphql/directives.js";
+export {
   shouldInclude,
   hasDirectives,
   hasAnyDirectives,
@@ -10,15 +12,17 @@ export {
   hasClientExports,
   getDirectiveNames,
   getInclusionDirectives,
-} from './graphql/directives';
+} from "./graphql/directives.js";
 
+export type { DocumentTransformCacheKey } from "./graphql/DocumentTransform.js";
+export { DocumentTransform } from "./graphql/DocumentTransform.js";
+
+export type { FragmentMap, FragmentMapFunction } from "./graphql/fragments.js";
 export {
-  FragmentMap,
-  FragmentMapFunction,
   createFragmentMap,
   getFragmentQueryDocument,
   getFragmentFromSelection,
-} from './graphql/fragments';
+} from "./graphql/fragments.js";
 
 export {
   checkDocument,
@@ -29,14 +33,18 @@ export {
   getFragmentDefinition,
   getMainDefinition,
   getDefaultValues,
-} from './graphql/getFromAST';
+} from "./graphql/getFromAST.js";
 
-export {
+export { print } from "./graphql/print.js";
+
+export type {
   StoreObject,
   Reference,
   StoreValue,
   Directives,
   VariableValue,
+} from "./graphql/storeUtils.js";
+export {
   makeReference,
   isDocumentNode,
   isReference,
@@ -48,9 +56,9 @@ export {
   resultKeyNameFromField,
   getStoreKeyName,
   getTypenameFromResult,
-} from './graphql/storeUtils';
+} from "./graphql/storeUtils.js";
 
-export {
+export type {
   RemoveNodeConfig,
   GetNodeConfig,
   RemoveDirectiveConfig,
@@ -60,6 +68,8 @@ export {
   RemoveFragmentSpreadConfig,
   RemoveFragmentDefinitionConfig,
   RemoveVariableDefinitionConfig,
+} from "./graphql/transform.js";
+export {
   addTypenameToDocument,
   buildQueryFromSelectionSet,
   removeDirectivesFromDocument,
@@ -67,34 +77,54 @@ export {
   removeArgumentsFromDocument,
   removeFragmentSpreadFromDocument,
   removeClientSetsFromDocument,
-} from './graphql/transform';
+} from "./graphql/transform.js";
+
+export {
+  isMutationOperation,
+  isQueryOperation,
+  isSubscriptionOperation,
+} from "./graphql/operations.js";
 
 export {
   concatPagination,
   offsetLimitPagination,
   relayStylePagination,
-} from './policies/pagination';
+} from "./policies/pagination.js";
 
-export {
-  Observable,
+export type {
   Observer,
-  ObservableSubscription
-} from './observables/Observable';
+  ObservableSubscription,
+} from "./observables/Observable.js";
+export { Observable } from "./observables/Observable.js";
+
+export {
+  isStatefulPromise,
+  createFulfilledPromise,
+  createRejectedPromise,
+  wrapPromiseWithState,
+} from "./promises/decoration.js";
+
+export * from "./common/mergeDeep.js";
+export * from "./common/cloneDeep.js";
+export * from "./common/maybeDeepFreeze.js";
+export * from "./observables/iteration.js";
+export * from "./observables/asyncMap.js";
+export * from "./observables/Concast.js";
+export * from "./observables/subclassing.js";
+export * from "./common/arrays.js";
+export * from "./common/objects.js";
+export * from "./common/errorHandling.js";
+export * from "./common/canUse.js";
+export * from "./common/compact.js";
+export * from "./common/makeUniqueId.js";
+export * from "./common/stringifyForDisplay.js";
+export * from "./common/mergeOptions.js";
+export * from "./common/responseIterator.js";
+export * from "./common/incrementalResult.js";
 
-export * from './common/mergeDeep';
-export * from './common/cloneDeep';
-export * from './common/maybeDeepFreeze';
-export * from './observables/iteration';
-export * from './observables/asyncMap';
-export * from './observables/Concast';
-export * from './observables/subclassing';
-export * from './common/arrays';
-export * from './common/objects';
-export * from './common/errorHandling';
-export * from './common/canUse';
-export * from './common/compact';
-export * from './common/makeUniqueId';
-export * from './common/stringifyForDisplay';
-export * from './common/mergeOptions';
+export { omitDeep } from "./common/omitDeep.js";
+export { stripTypename } from "./common/stripTypename.js";
 
-export * from './types/IsStrictlyAny';
+export * from "./types/IsStrictlyAny.js";
+export type { DeepOmit } from "./types/DeepOmit.js";
+export type { DeepPartial } from "./types/DeepPartial.js";
diff --git a/src/utilities/observables/Concast.ts b/src/utilities/observables/Concast.ts
--- a/src/utilities/observables/Concast.ts
+++ b/src/utilities/observables/Concast.ts
@@ -1,6 +1,7 @@
-import { Observable, Observer, ObservableSubscription, Subscriber } from "./Observable";
-import { iterateObserversSafely } from "./iteration";
-import { fixObservableSubclass } from "./subclassing";
+import type { Observer, ObservableSubscription, Subscriber } from "./Observable.js";
+import { Observable } from "./Observable.js";
+import { iterateObserversSafely } from "./iteration.js";
+import { fixObservableSubclass } from "./subclassing.js";
 
 type MaybeAsync<T> = T | PromiseLike<T>;
 
diff --git a/src/utilities/observables/Observable.ts b/src/utilities/observables/Observable.ts
--- a/src/utilities/observables/Observable.ts
+++ b/src/utilities/observables/Observable.ts
@@ -1,8 +1,9 @@
-import {
-  Observable,
+import type {
   Observer,
   Subscription as ObservableSubscription,
-  Subscriber,
+  Subscriber} from 'zen-observable-ts';
+import {
+  Observable
 } from 'zen-observable-ts';
 
 // This simplified polyfill attempts to follow the ECMAScript Observable
diff --git a/src/utilities/observables/asyncMap.ts b/src/utilities/observables/asyncMap.ts
--- a/src/utilities/observables/asyncMap.ts
+++ b/src/utilities/observables/asyncMap.ts
@@ -1,4 +1,5 @@
-import { Observable, Observer } from "./Observable";
+import type { Observer } from "./Observable.js";
+import { Observable } from "./Observable.js";
 
 // Like Observable.prototype.map, except that the mapping function can
 // optionally return a Promise (or be async).
diff --git a/src/utilities/observables/iteration.ts b/src/utilities/observables/iteration.ts
--- a/src/utilities/observables/iteration.ts
+++ b/src/utilities/observables/iteration.ts
@@ -1,4 +1,4 @@
-import { Observer } from "./Observable";
+import type { Observer } from "./Observable.js";
 
 export function iterateObserversSafely<E, A>(
   observers: Set<Observer<E>>,
diff --git a/src/utilities/observables/subclassing.ts b/src/utilities/observables/subclassing.ts
--- a/src/utilities/observables/subclassing.ts
+++ b/src/utilities/observables/subclassing.ts
@@ -1,5 +1,5 @@
-import { Observable } from "./Observable";
-import { canUseSymbol } from "../common/canUse";
+import { Observable } from "./Observable.js";
+import { canUseSymbol } from "../common/canUse.js";
 
 // Generic implementations of Observable.prototype methods like map and
 // filter need to know how to create a new Observable from an Observable
diff --git a/src/utilities/policies/pagination.ts b/src/utilities/policies/pagination.ts
--- a/src/utilities/policies/pagination.ts
+++ b/src/utilities/policies/pagination.ts
@@ -1,7 +1,7 @@
 import { __rest } from "tslib";
 
-import { FieldPolicy, Reference } from '../../cache';
-import { mergeDeep } from '../common/mergeDeep';
+import type { FieldPolicy, Reference } from '../../cache/index.js';
+import { mergeDeep } from '../common/mergeDeep.js';
 
 type KeyArgs = FieldPolicy<any>["keyArgs"];
 
diff --git a/src/utilities/promises/decoration.ts b/src/utilities/promises/decoration.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/promises/decoration.ts
@@ -0,0 +1,79 @@
+export interface PendingPromise<TValue> extends Promise<TValue> {
+  status: "pending";
+}
+
+export interface FulfilledPromise<TValue> extends Promise<TValue> {
+  status: "fulfilled";
+  value: TValue;
+}
+
+export interface RejectedPromise<TValue> extends Promise<TValue> {
+  status: "rejected";
+  reason: unknown;
+}
+
+export type PromiseWithState<TValue> =
+  | PendingPromise<TValue>
+  | FulfilledPromise<TValue>
+  | RejectedPromise<TValue>;
+
+export function createFulfilledPromise<TValue>(value: TValue) {
+  const promise = Promise.resolve(value) as FulfilledPromise<TValue>;
+
+  promise.status = "fulfilled";
+  promise.value = value;
+
+  return promise;
+}
+
+export function createRejectedPromise<TValue = unknown>(reason: unknown) {
+  const promise = Promise.reject(reason) as RejectedPromise<TValue>;
+
+  // prevent potential edge cases leaking unhandled error rejections
+  promise.catch(() => {});
+
+  promise.status = "rejected";
+  promise.reason = reason;
+
+  return promise;
+}
+
+export function isStatefulPromise<TValue>(
+  promise: Promise<TValue>
+): promise is PromiseWithState<TValue> {
+  return "status" in promise;
+}
+
+export function wrapPromiseWithState<TValue>(
+  promise: Promise<TValue>
+): PromiseWithState<TValue> {
+  if (isStatefulPromise(promise)) {
+    return promise;
+  }
+
+  const pendingPromise = promise as PendingPromise<TValue>;
+  pendingPromise.status = "pending";
+
+  pendingPromise.then(
+    (value) => {
+      if (pendingPromise.status === "pending") {
+        const fulfilledPromise =
+          pendingPromise as unknown as FulfilledPromise<TValue>;
+
+        fulfilledPromise.status = "fulfilled";
+        fulfilledPromise.value = value;
+      }
+    },
+    (reason: unknown) => {
+      if (pendingPromise.status === "pending") {
+        const rejectedPromise =
+          pendingPromise as unknown as RejectedPromise<TValue>;
+
+        rejectedPromise.status = "rejected";
+        rejectedPromise.reason = reason;
+      }
+    }
+  );
+
+  return promise as PromiseWithState<TValue>;
+}
diff --git a/src/utilities/types/DeepOmit.ts b/src/utilities/types/DeepOmit.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/types/DeepOmit.ts
@@ -0,0 +1,32 @@
+import type { Primitive } from "./Primitive.js";
+
+// DeepOmit primitives include functions since these are unmodified.
+type DeepOmitPrimitive = Primitive | Function;
+
+export type DeepOmitArray<T extends any[], K> = {
+  [P in keyof T]: DeepOmit<T[P], K>;
+};
+
+// Unfortunately there is one major flaw in this type: This will omit properties
+// from class instances in the return type even though our omitDeep helper
+// ignores class instances, therefore resulting in a type mismatch between
+// the return value and the runtime value.
+//
+// It is not currently possible with TypeScript to distinguish between plain
+// objects and class instances.
+// https://github.com/microsoft/TypeScript/issues/29063
+//
+// This should be fine as of the time of this writing until omitDeep gets
+// broader use since this utility is only used to strip __typename from
+// `variables`; a case in which class instances are invalid anyways.
+export type DeepOmit<T, K> = T extends DeepOmitPrimitive
+  ? T
+  : {
+      [P in Exclude<keyof T, K>]: T[P] extends infer TP
+        ? TP extends DeepOmitPrimitive
+          ? TP
+          : TP extends any[]
+          ? DeepOmitArray<TP, K>
+          : DeepOmit<TP, K>
+        : never;
+    };
diff --git a/src/utilities/types/DeepPartial.ts b/src/utilities/types/DeepPartial.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/types/DeepPartial.ts
@@ -0,0 +1,58 @@
+// Inspired by type-fest PartialDeep: https://github.com/sindresorhus/type-fest/blob/9feb8c89be9a0f2f688bf2f497230298a8e2472e/source/partial-deep.d.ts
+//
+// We're including the license to give credit to the original implementation.
+// https://github.com/sindresorhus/type-fest/blob/main/license-mit
+
+/*
+ * MIT License
+ *
+ * Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+import type { Primitive } from "./Primitive.js";
+
+type DeepPartialPrimitive = Primitive | Date | RegExp;
+
+export type DeepPartial<T> = T extends DeepPartialPrimitive
+  ? T
+  : T extends Map<infer TKey, infer TValue>
+  ? DeepPartialMap<TKey, TValue>
+  : T extends ReadonlyMap<infer TKey, infer TValue>
+  ? DeepPartialReadonlyMap<TKey, TValue>
+  : T extends Set<infer TItem>
+  ? DeepPartialSet<TItem>
+  : T extends ReadonlySet<infer TItem>
+  ? DeepPartialReadonlySet<TItem>
+  : T extends (...args: any[]) => unknown
+  ? T | undefined
+  : T extends object
+  ? T extends ReadonlyArray<infer TItem> // Test for arrays/tuples
+    ? TItem[] extends T // Test for non-tuples
+      ? readonly TItem[] extends T
+        ? ReadonlyArray<DeepPartial<TItem | undefined>>
+        : Array<DeepPartial<TItem | undefined>>
+      : DeepPartialObject<T>
+    : DeepPartialObject<T>
+  : unknown;
+
+type DeepPartialMap<TKey, TValue> = {} & Map<
+  DeepPartial<TKey>,
+  DeepPartial<TValue>
+>;
+
+type DeepPartialReadonlyMap<TKey, TValue> = {} & ReadonlyMap<
+  DeepPartial<TKey>,
+  DeepPartial<TValue>
+>;
+
+type DeepPartialSet<T> = {} & Set<DeepPartial<T>>;
+type DeepPartialReadonlySet<T> = {} & ReadonlySet<DeepPartial<T>>;
+
+type DeepPartialObject<T extends object> = {
+  [K in keyof T]?: DeepPartial<T[K]>;
+};
diff --git a/src/utilities/types/Primitive.ts b/src/utilities/types/Primitive.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/types/Primitive.ts
@@ -0,0 +1,9 @@
+// Matches any primitive value: https://developer.mozilla.org/en-US/docs/Glossary/Primitive.
+export type Primitive =
+  | null
+  | undefined
+  | string
+  | number
+  | boolean
+  | symbol
+  | bigint;


Test patch:
diff --git a/integration-tests/browser-esm/tests/playwright/jsdeliver-esm.test.ts b/integration-tests/browser-esm/tests/playwright/jsdeliver-esm.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/browser-esm/tests/playwright/jsdeliver-esm.test.ts
@@ -0,0 +1,10 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture";
+
+test("Basic Test", async ({ page, withHar }) => {
+  await page.goto("http://localhost:3000/jsdeliver-esm.html");
+
+  await expect(page.getByText("loading")).toBeVisible();
+  await expect(page.getByText("loading")).not.toBeVisible({ timeout: 10000 });
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/integration-tests/browser-esm/tests/playwright/jspm-prepared.test.ts b/integration-tests/browser-esm/tests/playwright/jspm-prepared.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/browser-esm/tests/playwright/jspm-prepared.test.ts
@@ -0,0 +1,10 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture";
+
+test("Basic Test", async ({ page, withHar }) => {
+  await page.goto("http://localhost:3000/jspm-prepared.html");
+
+  await expect(page.getByText("loading")).toBeVisible();
+  await expect(page.getByText("loading")).not.toBeVisible({ timeout: 10000 });
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/integration-tests/browser-esm/tests/playwright/unpkg-unmangled.test.ts b/integration-tests/browser-esm/tests/playwright/unpkg-unmangled.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/browser-esm/tests/playwright/unpkg-unmangled.test.ts
@@ -0,0 +1,12 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture";
+
+test("Basic Test", async ({ withHar }) => {
+  await withHar.goto("http://localhost:3000/unpkg-unmangled.html");
+
+  await expect(withHar.getByText("loading")).toBeVisible();
+  await expect(withHar.getByText("loading")).not.toBeVisible({
+    timeout: 10000,
+  });
+  await expect(withHar.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/integration-tests/cra4/tests/playwright/apollo-client.test.ts b/integration-tests/cra4/tests/playwright/apollo-client.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/cra4/tests/playwright/apollo-client.test.ts
@@ -0,0 +1,10 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture";
+
+test("Basic Test", async ({ page, withHar }) => {
+  await page.goto("http://localhost:3000");
+
+  await expect(page.getByText("loading")).toBeVisible();
+  await expect(page.getByText("loading")).not.toBeVisible();
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/integration-tests/cra5/tests/playwright/apollo-client.test.ts b/integration-tests/cra5/tests/playwright/apollo-client.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/cra5/tests/playwright/apollo-client.test.ts
@@ -0,0 +1,10 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture";
+
+test("Basic Test", async ({ page, withHar }) => {
+  await page.goto("http://localhost:3000");
+
+  await expect(page.getByText("loading")).toBeVisible();
+  await expect(page.getByText("loading")).not.toBeVisible();
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/integration-tests/next/tests/playwright/apollo-client.test.ts b/integration-tests/next/tests/playwright/apollo-client.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/next/tests/playwright/apollo-client.test.ts
@@ -0,0 +1,28 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture";
+
+test("RSC", async ({ page, blockRequest }) => {
+  await page.goto("http://localhost:3000");
+
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
+
+test("CC", async ({ page, blockRequest }) => {
+  await page.goto("http://localhost:3000/cc");
+
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
+
+test("Pages", async ({ page, blockRequest }) => {
+  await page.goto("http://localhost:3000/pages");
+
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
+
+test("Pages without SSR", async ({ page, withHar }) => {
+  await page.goto("http://localhost:3000/pages-no-ssr");
+
+  await expect(page.getByText("loading")).toBeVisible();
+  await expect(page.getByText("loading")).not.toBeVisible();
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/integration-tests/vite-swc/tests/playwright/apollo-client.test.ts b/integration-tests/vite-swc/tests/playwright/apollo-client.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/vite-swc/tests/playwright/apollo-client.test.ts
@@ -0,0 +1,10 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture.ts";
+
+test("Basic Test", async ({ page, withHar }) => {
+  await page.goto("http://localhost:3000");
+
+  await expect(page.getByText("loading")).toBeVisible();
+  await expect(page.getByText("loading")).not.toBeVisible();
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/integration-tests/vite/tests/playwright/apollo-client.test.ts b/integration-tests/vite/tests/playwright/apollo-client.test.ts
new file mode 100644
--- /dev/null
+++ b/integration-tests/vite/tests/playwright/apollo-client.test.ts
@@ -0,0 +1,10 @@
+import { expect } from "@playwright/test";
+import { test } from "shared/fixture.ts";
+
+test("Basic Test", async ({ page, withHar }) => {
+  await page.goto("http://localhost:3000");
+
+  await expect(page.getByText("loading")).toBeVisible();
+  await expect(page.getByText("loading")).not.toBeVisible();
+  await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+});
diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts
--- a/src/__tests__/ApolloClient.ts
+++ b/src/__tests__/ApolloClient.ts
@@ -2,6 +2,7 @@ import gql from 'graphql-tag';
 
 import {
   ApolloClient,
+  ApolloError,
   DefaultOptions,
   FetchPolicy,
   QueryOptions,
@@ -15,7 +16,7 @@ import { HttpLink } from '../link/http';
 import { InMemoryCache } from '../cache';
 import { itAsync, withErrorSpy } from '../testing';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { invariant } from 'ts-invariant';
+import { invariant } from '../utilities/globals';
 
 describe('ApolloClient', () => {
   describe('constructor', () => {
@@ -2370,7 +2371,7 @@ describe('ApolloClient', () => {
             setTimeout(() => {
               try {
                 expect(invariantDebugSpy).toHaveBeenCalledTimes(1);
-                expect(invariantDebugSpy).toHaveBeenCalledWith('In client.refetchQueries, Promise.all promise rejected with error ApolloError: refetch failed');
+                expect(invariantDebugSpy).toHaveBeenCalledWith('In client.refetchQueries, Promise.all promise rejected with error %o', new ApolloError({errorMessage:"refetch failed"}));
                 resolve();
               } catch (err) {
                 reject(err);
diff --git a/src/__tests__/__snapshots__/ApolloClient.ts.snap b/src/__tests__/__snapshots__/ApolloClient.ts.snap
--- a/src/__tests__/__snapshots__/ApolloClient.ts.snap
+++ b/src/__tests__/__snapshots__/ApolloClient.ts.snap
@@ -213,10 +213,12 @@ exports[`ApolloClient writeFragment should warn when the data provided does not
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'e' while writing result {
-  \\"__typename\\": \\"Bar\\",
-  \\"i\\": 10
-}",
+      "Missing field '%s' while writing result %o",
+      "e",
+      Object {
+        "__typename": "Bar",
+        "i": 10,
+      },
     ],
   ],
   "results": Array [
@@ -382,11 +384,13 @@ exports[`ApolloClient writeQuery should warn when the data provided does not mat
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'description' while writing result {
-  \\"id\\": \\"1\\",
-  \\"name\\": \\"Todo 1\\",
-  \\"__typename\\": \\"Todo\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "description",
+      Object {
+        "__typename": "Todo",
+        "id": "1",
+        "name": "Todo 1",
+      },
     ],
   ],
   "results": Array [
diff --git a/src/__tests__/__snapshots__/client.ts.snap b/src/__tests__/__snapshots__/client.ts.snap
--- a/src/__tests__/__snapshots__/client.ts.snap
+++ b/src/__tests__/__snapshots__/client.ts.snap
@@ -46,12 +46,14 @@ exports[`client should warn if server returns wrong data 1`] = `
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'description' while writing result {
-  \\"id\\": \\"1\\",
-  \\"name\\": \\"Todo 1\\",
-  \\"price\\": 100,
-  \\"__typename\\": \\"Todo\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "description",
+      Object {
+        "__typename": "Todo",
+        "id": "1",
+        "name": "Todo 1",
+        "price": 100,
+      },
     ],
   ],
   "results": Array [
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
@@ -9,6 +9,7 @@ Array [
   "ApolloLink",
   "ApolloProvider",
   "Cache",
+  "DocumentTransform",
   "DocumentType",
   "HttpLink",
   "InMemoryCache",
@@ -16,6 +17,7 @@ Array [
   "NetworkStatus",
   "Observable",
   "ObservableQuery",
+  "SuspenseCache",
   "checkFetcher",
   "concat",
   "createHttpLink",
@@ -34,6 +36,7 @@ Array [
   "getApolloContext",
   "gql",
   "isApolloError",
+  "isNetworkRequestSettled",
   "isReference",
   "makeReference",
   "makeVar",
@@ -49,16 +52,20 @@ Array [
   "selectURI",
   "serializeFetchParameter",
   "setLogVerbosity",
+  "skipToken",
   "split",
   "throwServerError",
   "toPromise",
   "useApolloClient",
-  "useFragment_experimental",
+  "useBackgroundQuery",
+  "useFragment",
   "useLazyQuery",
   "useMutation",
   "useQuery",
   "useReactiveVar",
+  "useReadQuery",
   "useSubscription",
+  "useSuspenseQuery",
 ]
 `;
 
@@ -88,6 +95,7 @@ Array [
   "ApolloError",
   "ApolloLink",
   "Cache",
+  "DocumentTransform",
   "HttpLink",
   "InMemoryCache",
   "MissingFieldError",
@@ -111,6 +119,7 @@ Array [
   "fromPromise",
   "gql",
   "isApolloError",
+  "isNetworkRequestSettled",
   "isReference",
   "makeReference",
   "makeVar",
@@ -129,6 +138,14 @@ Array [
 ]
 `;
 
+exports[`exports of public entry points @apollo/client/dev 1`] = `
+Array [
+  "loadDevMessages",
+  "loadErrorMessageHandler",
+  "loadErrorMessages",
+]
+`;
+
 exports[`exports of public entry points @apollo/client/errors 1`] = `
 Array [
   "ApolloError",
@@ -200,6 +217,13 @@ Array [
 ]
 `;
 
+exports[`exports of public entry points @apollo/client/link/remove-typename 1`] = `
+Array [
+  "KEEP",
+  "removeTypenameFromVariables",
+]
+`;
+
 exports[`exports of public entry points @apollo/client/link/retry 1`] = `
 Array [
   "RetryLink",
@@ -221,6 +245,7 @@ Array [
 exports[`exports of public entry points @apollo/client/link/utils 1`] = `
 Array [
   "createOperation",
+  "filterOperationVariables",
   "fromError",
   "fromPromise",
   "throwServerError",
@@ -241,17 +266,22 @@ Array [
   "ApolloConsumer",
   "ApolloProvider",
   "DocumentType",
+  "SuspenseCache",
   "getApolloContext",
   "operationName",
   "parser",
   "resetApolloContext",
+  "skipToken",
   "useApolloClient",
-  "useFragment_experimental",
+  "useBackgroundQuery",
+  "useFragment",
   "useLazyQuery",
   "useMutation",
   "useQuery",
   "useReactiveVar",
+  "useReadQuery",
   "useSubscription",
+  "useSuspenseQuery",
 ]
 `;
 
@@ -284,13 +314,17 @@ Array [
 
 exports[`exports of public entry points @apollo/client/react/hooks 1`] = `
 Array [
+  "skipToken",
   "useApolloClient",
-  "useFragment_experimental",
+  "useBackgroundQuery",
+  "useFragment",
   "useLazyQuery",
   "useMutation",
   "useQuery",
   "useReactiveVar",
+  "useReadQuery",
   "useSubscription",
+  "useSuspenseQuery",
 ]
 `;
 
@@ -352,6 +386,7 @@ Array [
   "Concast",
   "DEV",
   "DeepMerger",
+  "DocumentTransform",
   "Observable",
   "addTypenameToDocument",
   "argumentsObjectFromField",
@@ -368,6 +403,8 @@ Array [
   "compact",
   "concatPagination",
   "createFragmentMap",
+  "createFulfilledPromise",
+  "createRejectedPromise",
   "fixObservableSubclass",
   "getDefaultValues",
   "getDirectiveNames",
@@ -388,13 +425,28 @@ Array [
   "hasAnyDirectives",
   "hasClientExports",
   "hasDirectives",
+  "isApolloPayloadResult",
   "isArray",
+  "isAsyncIterableIterator",
+  "isBlob",
   "isDocumentNode",
+  "isExecutionPatchIncrementalResult",
+  "isExecutionPatchInitialResult",
+  "isExecutionPatchResult",
   "isField",
   "isInlineFragment",
+  "isMutationOperation",
+  "isNodeReadableStream",
+  "isNodeResponse",
   "isNonEmptyArray",
   "isNonNullObject",
+  "isPlainObject",
+  "isQueryOperation",
+  "isReadableStream",
   "isReference",
+  "isStatefulPromise",
+  "isStreamableBlob",
+  "isSubscriptionOperation",
   "iterateObserversSafely",
   "makeReference",
   "makeUniqueId",
@@ -402,8 +454,11 @@ Array [
   "maybeDeepFreeze",
   "mergeDeep",
   "mergeDeepArray",
+  "mergeIncrementalData",
   "mergeOptions",
   "offsetLimitPagination",
+  "omitDeep",
+  "print",
   "relayStylePagination",
   "removeArgumentsFromDocument",
   "removeClientSetsFromDocument",
@@ -414,7 +469,9 @@ Array [
   "shouldInclude",
   "storeKeyNameFromField",
   "stringifyForDisplay",
+  "stripTypename",
   "valueToObjectRepresentation",
+  "wrapPromiseWithState",
 ]
 `;
 
@@ -422,9 +479,10 @@ exports[`exports of public entry points @apollo/client/utilities/globals 1`] = `
 Array [
   "DEV",
   "InvariantError",
-  "checkDEV",
+  "__DEV__",
   "global",
   "invariant",
   "maybe",
+  "newInvariantError",
 ]
 `;
diff --git a/src/__tests__/__snapshots__/mutationResults.ts.snap b/src/__tests__/__snapshots__/mutationResults.ts.snap
--- a/src/__tests__/__snapshots__/mutationResults.ts.snap
+++ b/src/__tests__/__snapshots__/mutationResults.ts.snap
@@ -4,11 +4,13 @@ exports[`mutation results should warn when the result fields don't match the que
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'description' while writing result {
-  \\"id\\": \\"2\\",
-  \\"name\\": \\"Todo 2\\",
-  \\"__typename\\": \\"createTodo\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "description",
+      Object {
+        "__typename": "createTodo",
+        "id": "2",
+        "name": "Todo 2",
+      },
     ],
   ],
   "results": Array [
diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts
--- a/src/__tests__/client.ts
+++ b/src/__tests__/client.ts
@@ -1,5 +1,5 @@
 import { cloneDeep, assign } from 'lodash';
-import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql';
+import { GraphQLError, ExecutionResult, DocumentNode, Kind, print, visit } from 'graphql';
 import gql from 'graphql-tag';
 
 import {
@@ -8,12 +8,14 @@ import {
   WatchQueryFetchPolicy,
   QueryOptions,
   ObservableQuery,
+  Operation,
   TypedDocumentNode,
+  NetworkStatus,
 } from '../core';
 
-import { Observable, ObservableSubscription } from '../utilities';
+import { DocumentTransform, Observable, ObservableSubscription, offsetLimitPagination, removeDirectivesFromDocument } from '../utilities';
 import { ApolloLink } from '../link/core';
-import { InMemoryCache, makeVar, PossibleTypesMap } from '../cache';
+import { createFragmentRegistry, InMemoryCache, makeVar, PossibleTypesMap } from '../cache';
 import { ApolloError } from '../errors';
 
 import {
@@ -21,7 +23,10 @@ import {
   subscribeAndCount,
   mockSingleLink,
   withErrorSpy,
+  MockLink,
+  wait,
 } from '../testing';
+import { waitFor } from '@testing-library/react';
 
 describe('client', () => {
   it('can be loaded via require', () => {
@@ -940,6 +945,54 @@ describe('client', () => {
       .then(resolve, reject);
   });
 
+  it('removes @client fields from the query before it reaches the link', async () => {
+    const result: { current: Operation | undefined } = {
+      current: undefined
+    }
+
+    const query = gql`
+      query {
+        author {
+          firstName
+          lastName
+          isInCollection @client
+        }
+      }
+    `;
+
+    const transformedQuery = gql`
+      query {
+        author {
+          firstName
+          lastName
+        }
+      }
+    `;
+
+    const link = new ApolloLink((operation) => {
+      result.current = operation;
+
+      return Observable.of({
+        data: {
+          author: {
+            firstName: 'John',
+            lastName: 'Smith',
+            __typename: 'Author',
+          }
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({ addTypename: false }),
+    });
+
+    await client.query({ query });
+
+    expect(print(result.current!.query)).toEqual(print(transformedQuery));
+  });
+
   itAsync('should handle named fragments on mutations', (resolve, reject) => {
     const mutation = gql`
       mutation {
@@ -3545,6 +3598,2507 @@ describe('@connection', () => {
   });
 });
 
+describe('custom document transforms', () => {
+  it('runs custom document transform when calling `query`', async ()  => {
+    const query = gql`
+      query TestQuery {
+        dogs {
+          id
+          name
+          breed @custom
+        }
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          dogs: [
+            {
+              id: 1,
+              name: 'Buddy',
+              breed: 'German Shepard',
+              __typename: 'Dog'
+            }
+          ],
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+      documentTransform,
+    });
+
+    const { data } = await client.query({ query });
+
+    expect(document!).toMatchDocument(gql`
+      query TestQuery {
+        dogs {
+          id
+          name
+          breed
+          __typename
+        }
+      }
+    `);
+
+    expect(data).toEqual({
+      dogs: [
+        {
+          id: 1,
+          name: 'Buddy',
+          breed: 'German Shepard',
+          __typename: 'Dog',
+        }
+      ]
+    });
+  });
+
+  it('requests and caches fields added from custom document transforms when calling `query`', async ()  => {
+    const query = gql`
+      query TestQuery {
+        dogs {
+          name
+          breed
+        }
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return visit(document, {
+        Field(node) {
+          if (node.name.value === 'dogs' && node.selectionSet) {
+            return {
+              ...node,
+              selectionSet: {
+                ...node.selectionSet,
+                selections: [
+                  {
+                    kind: Kind.FIELD,
+                    name: { kind: Kind.NAME, value: 'id' },
+                  },
+                  ...node.selectionSet.selections,
+                ]
+              }
+            }
+          }
+        }
+      })
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          dogs: [
+            {
+              id: 1,
+              name: 'Buddy',
+              breed: 'German Shepard',
+              __typename: 'Dog'
+            }
+          ],
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+      documentTransform,
+    });
+
+    const { data } = await client.query({ query });
+
+    expect(document!).toMatchDocument(gql`
+      query TestQuery {
+        dogs {
+          id
+          name
+          breed
+          __typename
+        }
+      }
+    `);
+
+    expect(data).toEqual({
+      dogs: [
+        {
+          id: 1,
+          name: 'Buddy',
+          breed: 'German Shepard',
+          __typename: 'Dog'
+        }
+      ]
+    });
+
+    const cache = client.cache.extract();
+
+    expect(cache['Dog:1']).toEqual({
+      id: 1,
+      name: 'Buddy',
+      breed: 'German Shepard',
+      __typename: 'Dog',
+    });
+  });
+
+  it('runs document transforms before reading from the cache when calling `query`', async ()  => {
+    const query = gql`
+      query TestQuery {
+        product {
+          id
+          name
+        }
+      }
+    `;
+
+    const documentTransform = new DocumentTransform((document) => {
+      return visit(document, {
+        Field(node) {
+          if (node.name.value === 'product' && node.selectionSet) {
+            return {
+              ...node,
+              selectionSet: {
+                ...node.selectionSet,
+                selections: [
+                  ...node.selectionSet.selections,
+                  {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'ProductFields' }
+                  }
+                ]
+              }
+            }
+          }
+        } 
+      });
+    });
+
+    const link = new ApolloLink(() => {
+      return Observable.of({
+        data: {
+          product: {
+            __typename: 'Product',
+            id: 2,
+            name: 'unused',
+            description: 'unused',
+          }
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({
+        fragments: createFragmentRegistry(gql`
+          fragment ProductFields on Product {
+            description
+          }
+        `)
+      }),
+      documentTransform,
+    });
+
+    // Use the transformed document to write to the cache to ensure it contains
+    // the fragment spread
+    client.writeQuery({
+      query: documentTransform.transformDocument(query),
+      data: {
+        product: {
+          __typename: 'Product',
+          id: 1,
+          name: 'Cached product',
+          description: 'Cached product description'
+        }
+      }
+    });
+
+    const { data } = await client.query({ query });
+
+    expect(data).toEqual({
+      product: {
+        __typename: 'Product',
+        id: 1,
+        name: 'Cached product',
+        description: 'Cached product description',
+      }
+    });
+  });
+
+  it('runs @client directives added from custom transforms through local state', async ()  => {
+    const query = gql`
+      query TestQuery {
+        currentUser {
+          id
+        }
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return visit(document, {
+        Field(node) {
+          if (node.name.value === 'currentUser' && node.selectionSet) {
+            return {
+              ...node,
+              selectionSet: {
+                ...node.selectionSet,
+                selections: [
+                  ...node.selectionSet.selections,
+                  {
+                    kind: Kind.FIELD,
+                    name: { kind: Kind.NAME, value: 'isLoggedIn' },
+                    directives: [
+                      {
+                        kind: Kind.DIRECTIVE,
+                        name: { kind: Kind.NAME, value: 'client' }
+                      }
+                    ]
+                  },
+                ]
+              }
+            }
+          }
+        }
+      })
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          currentUser: {
+            id: 1,
+            __typename: 'User' ,
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      documentTransform,
+      cache: new InMemoryCache({
+        typePolicies: {
+          User: {
+            fields: {
+              isLoggedIn: {
+                read() {
+                  return true;
+                }
+              }
+            }
+          }
+        }
+      }),
+    });
+
+    const { data } = await client.query({ query });
+
+    expect(document!).toMatchDocument(gql`
+      query TestQuery {
+        currentUser {
+          id
+          __typename
+        }
+      }
+    `);
+
+    expect(data).toEqual({
+      currentUser: {
+        id: 1,
+        isLoggedIn: true,
+        __typename: 'User'
+      }
+    });
+  });
+
+  it('runs custom transform only once when calling `query`', async ()  => {
+    const query = gql`
+      query TestQuery {
+        currentUser {
+          id
+        }
+      }
+    `;
+
+    const transform = jest.fn((document: DocumentNode) => document);
+    const documentTransform = new DocumentTransform(transform, { cache: false });
+
+    const link = new ApolloLink(() => {
+      return Observable.of({
+        data: {
+          currentUser: {
+            id: 1,
+            __typename: 'User' ,
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    await client.query({ query });
+
+    expect(transform).toHaveBeenCalledTimes(1);
+  });
+
+  it('runs default transforms with no custom document transform when calling `query`', async ()  => {
+    const query = gql`
+      query TestQuery {
+        currentUser @nonreactive {
+          id
+          isLoggedIn @client
+          favoriteFlavors @connection {
+            flavor
+          }
+        }
+      }
+    `;
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of();
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    await client.query({ query });
+
+    expect(document!).toMatchDocument(gql`
+      query TestQuery {
+        currentUser {
+          id
+          favoriteFlavors {
+            flavor
+            __typename
+          }
+          __typename
+        }
+      }
+    `);
+  });
+
+  it('runs custom transform when calling `mutate`', async ()  => {
+    const mutation = gql`
+      mutation TestMutation($username: String) {
+        changeUsername(username: $username) {
+          id
+          username @custom
+        }
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          changeUsername: {
+            id: 1,
+            username: operation.variables.username,
+            __typename: 'User' ,
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    const { data } = await client.mutate({
+      mutation,
+      variables: { username: 'foo' }
+    });
+
+    expect(document!).toMatchDocument(gql`
+      mutation TestMutation($username: String) {
+        changeUsername(username: $username) {
+          id
+          username
+          __typename
+        }
+      }
+    `);
+
+    expect(data).toEqual({
+      changeUsername: {
+        id: 1,
+        username: 'foo',
+        __typename: 'User' ,
+      },
+    })
+  });
+
+  it('runs custom transform on queries defined in refetchQueries using legacy option when calling `mutate`', async ()  => {
+    const mutation = gql`
+      mutation TestMutation($username: String) {
+        changeUsername(username: $username) {
+          id
+          username @custom
+        }
+      }
+    `;
+
+    const query = gql`
+      query TestQuery {
+        currentUser {
+          id
+          username @custom
+        }
+      }
+    `;
+
+    const requests: Operation[] = [];
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const mocks = [
+      {
+        request: { 
+          query: documentTransform.transformDocument(mutation),
+          variables: { username: 'foo' }
+        },
+        result: {
+          data: {
+            changeUsername: { __typename: 'User', id: 1, username: 'foo' }
+          }
+        }
+      },
+      {
+        request: { query: documentTransform.transformDocument(query) },
+        result: {
+          data: {
+            currentUser: { __typename: 'User', id: 1, username: 'foo' }
+          }
+        }
+      }
+    ];
+
+    const link = new ApolloLink((operation, forward) => {
+      requests.push(operation);
+
+      return forward(operation);
+    });
+
+    const client = new ApolloClient({
+      link: ApolloLink.from([link, new MockLink(mocks)]),
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    const { data } = await client.mutate({
+      mutation,
+      variables: { username: 'foo' },
+      refetchQueries: [{ query }],
+      awaitRefetchQueries: true,
+    });
+
+    expect(data).toEqual({
+      changeUsername: {
+        id: 1,
+        username: 'foo',
+        __typename: 'User' ,
+      },
+    })
+
+    expect(requests[0].query).toMatchDocument(gql`
+      mutation TestMutation($username: String) {
+        changeUsername(username: $username) {
+          id
+          username
+          __typename
+        }
+      }
+    `);
+
+    expect(requests[1].query).toMatchDocument(gql`
+      query TestQuery {
+        currentUser {
+          id
+          username
+          __typename
+        }
+      }
+    `);
+  });
+
+  it('requests and caches fields added from custom document transforms when calling `mutate`', async ()  => {
+    const mutation = gql`
+      mutation TestMutation($username: String) {
+        changeUsername(username: $username) {
+          username
+        }
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return visit(document, {
+        Field(node) {
+          if (node.name.value === 'changeUsername' && node.selectionSet) {
+            return {
+              ...node,
+              selectionSet: {
+                ...node.selectionSet,
+                selections: [
+                  {
+                    kind: Kind.FIELD,
+                    name: { kind: Kind.NAME, value: 'id' }
+                  },
+                  ...node.selectionSet.selections,
+                ]
+              }
+            }
+          }
+        }
+      })
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          changeUsername: {
+            id: 1,
+            username: operation.variables.username,
+            __typename: 'User' ,
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    const { data } = await client.mutate({
+      mutation,
+      variables: { username: 'foo' }
+    });
+
+    expect(document!).toMatchDocument(gql`
+      mutation TestMutation($username: String) {
+        changeUsername(username: $username) {
+          id
+          username
+          __typename
+        }
+      }
+    `);
+
+    expect(data).toEqual({
+      changeUsername: {
+        id: 1,
+        username: 'foo',
+        __typename: 'User' ,
+      },
+    });
+
+    const cache = client.cache.extract();
+
+    expect(cache['User:1']).toEqual({
+      __typename: 'User',
+      id: 1,
+      username: 'foo'
+    });
+  });
+
+  it('runs custom transforms only once when running `mutation`', async ()  => {
+    const mutation = gql`
+      mutation TestMutation($username: String) {
+        changeUsername(username: $username) {
+          id
+          username
+        }
+      }
+    `;
+
+    const transform = jest.fn((document: DocumentNode) => document);
+    const documentTransform = new DocumentTransform(transform, { cache: false });
+
+    const link = new ApolloLink((operation) => {
+      return Observable.of({
+        data: {
+          changeUsername: {
+            id: 1,
+            username: operation.variables.username,
+            __typename: 'User' ,
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    await client.mutate({ mutation, variables: { username: 'foo' } });
+
+    expect(transform).toHaveBeenCalledTimes(1);
+  });
+
+  it('runs default transforms with no custom document transform when calling `mutate`', async ()  => {
+    const mutation = gql`
+      mutation TestMutation {
+        updateProfile @nonreactive {
+          id
+          isLoggedIn @client
+          favoriteFlavors @connection {
+            flavor
+          }
+        }
+      }
+    `;
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          updateProfile: {
+            __typename: 'Profile',
+            id: 1,
+            favoriteFlavors: [{ __typename: 'Flavor', flavor: 'Strawberry '}]
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    await client.mutate({ mutation });
+
+    expect(document!).toMatchDocument(gql`
+      mutation TestMutation {
+        updateProfile {
+          id
+          favoriteFlavors {
+            flavor
+            __typename
+          }
+          __typename
+        }
+      }
+    `);
+  });
+
+  it('runs custom document transforms when calling `subscribe`', async () => {
+    const query = gql`
+      subscription TestSubscription {
+        profileUpdated {
+          id
+          username @custom
+        }
+      }
+    `;
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          profileUpdated: {
+            id: 1,
+            username: 'foo',
+            __typename: 'Profile'
+          }
+        }
+      })
+    });
+
+    const client = new ApolloClient({
+      link,
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    const onNext = jest.fn()
+
+    const subscription = client
+      .subscribe({ query })
+      .subscribe(onNext);
+
+    await waitFor(() => subscription.closed);
+
+    expect(document!).toMatchDocument(gql`
+      subscription TestSubscription {
+        profileUpdated {
+          id
+          username
+          __typename
+        }
+      }
+    `);
+
+    expect(onNext).toHaveBeenLastCalledWith({
+      data: {
+        profileUpdated: { id: 1, username: 'foo', __typename: 'Profile', }
+      }
+    });
+  });
+
+  it('requests and caches fields added from custom document transforms when calling `subscribe`', async ()  => {
+    const query = gql`
+      subscription TestSubscription {
+        profileUpdated {
+          username
+        }
+      }
+    `;
+
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return visit(document, {
+        Field(node) {
+          if (node.name.value === 'profileUpdated' && node.selectionSet) {
+            return {
+              ...node,
+              selectionSet: {
+                ...node.selectionSet,
+                selections: [
+                  {
+                    kind: Kind.FIELD,
+                    name: { kind: Kind.NAME, value: 'id' }
+                  },
+                  ...node.selectionSet.selections,
+                ]
+              }
+            }
+          }
+        }
+      })
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          profileUpdated: {
+            id: 1,
+            username: 'foo',
+            __typename: 'Profile',
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    const onNext = jest.fn();
+
+    const subscription = client
+      .subscribe({ query })
+      .subscribe(onNext);
+
+    await waitFor(() => subscription.closed);
+
+    expect(document!).toMatchDocument(gql`
+      subscription TestSubscription {
+        profileUpdated {
+          id
+          username
+          __typename
+        }
+      }
+    `);
+
+    expect(onNext).toHaveBeenLastCalledWith({
+      data: {
+        profileUpdated: { id: 1, username: 'foo', __typename: 'Profile', }
+      }
+    });
+
+    const cache = client.cache.extract();
+
+    expect(cache['Profile:1']).toEqual({
+      __typename: 'Profile',
+      id: 1,
+      username: 'foo'
+    });
+  });
+
+  it('runs custom transforms only once when calling `subscribe`', async ()  => {
+    const query = gql`
+      subscription TestSubscription {
+        profileUpdated {
+          username
+        }
+      }
+    `;
+
+    const transform = jest.fn((document: DocumentNode) => document);
+    const documentTransform = new DocumentTransform(transform, { cache: false });
+
+    const client = new ApolloClient({
+      link: ApolloLink.empty(),
+      documentTransform,
+      cache: new InMemoryCache(),
+    });
+
+    const subscription = client
+      .subscribe({ query })
+      .subscribe(jest.fn());
+
+    await waitFor(() => subscription.closed);
+
+    expect(transform).toHaveBeenCalledTimes(1);
+  });
+
+  it('runs default transforms with no custom document transform when calling `subscribe`', async ()  => {
+    const query = gql`
+      subscription TestSubscription {
+        profileUpdated @nonreactive {
+          id
+          isLoggedIn @client
+          favoriteFlavors @connection {
+            flavor
+          }
+        }
+      }
+    `;
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          profileUpdated: {
+            __typename: 'Profile',
+            id: 1,
+            favoriteFlavors: [{ __typename: 'Flavor', flavor: 'Strawberry '}]
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    const subscription = client
+      .subscribe({ query })
+      .subscribe(jest.fn());
+
+    await waitFor(() => subscription.closed);
+
+    expect(document!).toMatchDocument(gql`
+      subscription TestSubscription {
+        profileUpdated {
+          id
+          favoriteFlavors {
+            flavor
+            __typename
+          }
+          __typename
+        }
+      }
+    `);
+  });
+
+  it('runs custom document transforms when subscribing to observable after calling `watchQuery`', async () => {
+    const query = gql`
+      query TestQuery {
+        currentUser {
+          id
+          name @custom
+        }
+      }
+    `;
+
+    const transformedQuery = gql`
+      query TestQuery {
+        currentUser {
+          id
+          name
+          __typename
+        }
+      }
+    `;
+
+    const transform = jest.fn((document: DocumentNode) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const documentTransform = new DocumentTransform(
+      transform,
+      { cache: false }
+    );
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: { currentUser: { __typename: 'User', id: 1, name: 'John Doe' }}
+      })
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+      documentTransform,
+    });
+
+    const observable = client.watchQuery({ query });
+
+    expect(transform).toHaveBeenCalledTimes(1);
+    // `options.query` should always reflect the raw, untransformed query 
+    expect(observable.options.query).toMatchDocument(query);
+    // The computed `query` property should always reflect the last requested 
+    // transformed document.
+    expect(observable.query).toMatchDocument(transformedQuery);
+
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: {
+          currentUser: { __typename: 'User', id: 1, name: 'John Doe' }
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+
+      expect(document).toMatchDocument(transformedQuery);
+      expect(observable.options.query).toMatchDocument(query);
+      expect(observable.query).toMatchDocument(transformedQuery);
+      expect(transform).toHaveBeenCalledTimes(2);
+    });
+  });
+
+  it('runs default transforms with no custom document transform when calling `watchQuery`', async ()  => {
+    const query = gql`
+      query TestQuery @nonreactive {
+        currentUser {
+          id
+          isLoggedIn @client
+          favorites @connection {
+            id
+          }
+        }
+      }
+    `;
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          currentUser: {
+            __typename: 'User',
+            id: 1,
+            favorites: [{ __typename: 'Favorite', id: 1 }]
+          },
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    const observable = client.watchQuery({ query })
+
+    observable.subscribe(jest.fn());
+
+    await waitFor(() => {
+      expect(document!).toMatchDocument(gql`
+        query TestQuery {
+          currentUser {
+            id
+            favorites {
+              id
+              __typename
+            }
+            __typename
+          }
+        }
+      `);
+    });
+  });
+
+  it('runs document transforms before reading from the cache when calling `watchQuery`', async ()  => {
+    const query = gql`
+      query TestQuery {
+        product {
+          id
+          name
+        }
+      }
+    `;
+
+    const documentTransform = new DocumentTransform((document) => {
+      return visit(document, {
+        Field(node) {
+          if (node.name.value === 'product' && node.selectionSet) {
+            return {
+              ...node,
+              selectionSet: {
+                ...node.selectionSet,
+                selections: [
+                  ...node.selectionSet.selections,
+                  {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'ProductFields' }
+                  }
+                ]
+              }
+            }
+          }
+        } 
+      });
+    });
+
+    const link = new ApolloLink(() => {
+      return Observable.of({
+        data: {
+          product: {
+            __typename: 'Product',
+            id: 2,
+            name: 'unused',
+            description: 'unused',
+          }
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({
+        fragments: createFragmentRegistry(gql`
+          fragment ProductFields on Product {
+            description
+          }
+        `)
+      }),
+      documentTransform,
+    });
+
+    // Use the transformed document to write to the cache to ensure it contains
+    // the fragment spread
+    client.writeQuery({
+      query: documentTransform.transformDocument(query),
+      data: {
+        product: {
+          __typename: 'Product',
+          id: 1,
+          name: 'Cached product',
+          description: 'Cached product description'
+        }
+      }
+    });
+
+    const observable = client.watchQuery({ query });
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: {
+          product: {
+            __typename: 'Product',
+            id: 1,
+            name: 'Cached product',
+            description: 'Cached product description'
+          }
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      })
+    });
+  });
+
+  it('re-runs custom document transforms when calling `refetch`', async () => {
+    const query = gql`
+      query TestQuery {
+        product {
+          id
+          metrics @whenEnabled
+        }
+      }
+    `;
+
+    const enabledQuery = gql`
+      query TestQuery {
+        product {
+          id
+          metrics
+          __typename
+        }
+      }
+    `;
+
+    const disabledQuery = gql`
+      query TestQuery {
+        product {
+          id
+          __typename
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: enabledQuery },
+        result: {
+          data: {
+            product: { __typename: 'Product', id: 1, metrics: '1000/vpm' }
+          }
+        }
+      },
+      {
+        request: { query: disabledQuery },
+        result: {
+          data: {
+            product: { __typename: 'Product', id: 1 }
+          }
+        }
+      }
+    ];
+
+    let enabled = true;
+
+    const documentTransform = new DocumentTransform((document: DocumentNode) => {
+      return removeDirectivesFromDocument(
+        [{ name: 'whenEnabled', remove: !enabled }],
+        document
+      )!;
+    }, { cache: false });
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation, forward) => {
+      document = operation.query;
+
+      return forward(operation);
+    });
+
+    const client = new ApolloClient({
+      link: ApolloLink.from([link, new MockLink(mocks)]),
+      cache: new InMemoryCache(),
+      documentTransform,
+    });
+
+    const observable = client.watchQuery({ query });
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: {
+          product: { __typename: 'Product', id: 1, metrics: '1000/vpm' }
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+
+      expect(document).toMatchDocument(enabledQuery);
+      expect(observable.options.query).toMatchDocument(query);
+      expect(observable.query).toMatchDocument(enabledQuery);
+    });
+
+    enabled = false;
+
+    const { data } = await observable.refetch();
+
+    expect(document!).toMatchDocument(disabledQuery);
+    expect(observable.options.query).toMatchDocument(query);
+    expect(observable.query).toMatchDocument(disabledQuery);
+
+    expect(data).toEqual({
+      product: { __typename: 'Product', id: 1 }
+    })
+
+    expect(handleNext).toHaveBeenLastCalledWith({
+      data: {
+        product: { __typename: 'Product', id: 1 }
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+    });
+  });
+
+  it('re-runs custom document transforms when calling `fetchMore`', async () => {
+    const query = gql`
+      query TestQuery($offset: Int) {
+        products(offset: $offset) {
+          id
+          metrics @whenEnabled
+        }
+      }
+    `;
+
+    const enabledQuery = gql`
+      query TestQuery($offset: Int) {
+        products(offset: $offset) {
+          id
+          metrics
+          __typename
+        }
+      }
+    `;
+
+    const disabledQuery = gql`
+      query TestQuery($offset: Int) {
+        products(offset: $offset) {
+          id
+          __typename
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: enabledQuery, variables: { offset: 0 } },
+        result: {
+          data: {
+            products: [{ __typename: 'Product', id: 1, metrics: '1000/vpm' }]
+          }
+        }
+      },
+      {
+        request: { query: disabledQuery, variables: { offset: 1 } },
+        result: {
+          data: {
+            products: [{ __typename: 'Product', id: 2 }]
+          }
+        }
+      }
+    ];
+
+    let enabled = true;
+
+    const documentTransform = new DocumentTransform((document: DocumentNode) => {
+      return removeDirectivesFromDocument(
+        [{ name: 'whenEnabled', remove: !enabled }],
+        document
+      )!;
+    }, { cache: false });
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation, forward) => {
+      document = operation.query;
+
+      return forward(operation);
+    });
+
+    const client = new ApolloClient({
+      link: ApolloLink.from([link, new MockLink(mocks)]),
+      cache: new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              products: offsetLimitPagination()
+            }
+          }
+        }
+      }),
+      documentTransform,
+    });
+
+    const observable = client.watchQuery({ query, variables: { offset: 0 } });
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: {
+          products: [{ __typename: 'Product', id: 1, metrics: '1000/vpm' }]
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+
+      expect(document).toMatchDocument(enabledQuery);
+      expect(observable.options.query).toMatchDocument(query);
+      expect(observable.query).toMatchDocument(enabledQuery);
+    });
+
+    enabled = false;
+
+    const { data } = await observable.fetchMore({ variables: { offset: 1 }});
+
+    expect(document!).toMatchDocument(disabledQuery);
+    expect(observable.options.query).toMatchDocument(query);
+    expect(observable.query).toMatchDocument(disabledQuery);
+
+    expect(data).toEqual({
+      products: [{ __typename: 'Product', id: 2 }]
+    })
+
+    expect(handleNext).toHaveBeenLastCalledWith({
+      data: {
+        products: [
+          { __typename: 'Product', id: 1 },
+          { __typename: 'Product', id: 2 }
+        ]
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+    });
+  });
+
+  it('runs custom document transforms on the passed query and original query when calling `fetchMore` with a different query', async () => {
+    const initialQuery = gql`
+      query TestQuery($offset: Int) {
+        currentUser {
+          id
+        }
+        products(offset: $offset) {
+          id
+          metrics @whenEnabled
+        }
+      }
+    `;
+
+    const enabledInitialQuery = gql`
+      query TestQuery($offset: Int) {
+        currentUser {
+          id
+          __typename
+        }
+        products(offset: $offset) {
+          id
+          metrics
+          __typename
+        }
+      }
+    `;
+
+    const disabledInitialQuery = gql`
+      query TestQuery($offset: Int) {
+        currentUser {
+          id
+          __typename
+        }
+        products(offset: $offset) {
+          id
+          __typename
+        }
+      }
+    `;
+
+    const productsQuery = gql`
+      query TestQuery($offset: Int) {
+        products(offset: $offset) {
+          id
+          metrics @whenEnabled
+        }
+      }
+    `;
+
+    const transformedProductsQuery = gql`
+      query TestQuery($offset: Int) {
+        products(offset: $offset) {
+          id
+          __typename
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: enabledInitialQuery, variables: { offset: 0 } },
+        result: {
+          data: {
+            currentUser: { id: 1 },
+            products: [{ __typename: 'Product', id: 1, metrics: '1000/vpm' }]
+          }
+        }
+      },
+      {
+        request: { query: transformedProductsQuery, variables: { offset: 1 } },
+        result: {
+          data: {
+            products: [{ __typename: 'Product', id: 2 }]
+          }
+        }
+      }
+    ];
+
+    let enabled = true;
+
+    const documentTransform = new DocumentTransform((document: DocumentNode) => {
+      return removeDirectivesFromDocument(
+        [{ name: 'whenEnabled', remove: !enabled }],
+        document
+      )!;
+    }, { cache: false });
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation, forward) => {
+      document = operation.query;
+
+      return forward(operation);
+    });
+
+    const client = new ApolloClient({
+      link: ApolloLink.from([link, new MockLink(mocks)]),
+      cache: new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              products: {
+                keyArgs: false,
+                merge(existing = [], incoming) {
+                  return [...existing, ...incoming] 
+                }
+              }
+            }
+          }
+        }
+      }),
+      documentTransform,
+    });
+
+    const observable = client.watchQuery({
+      query: initialQuery,
+      variables: { offset: 0 },
+    });
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: {
+          currentUser: { id: 1 },
+          products: [{ __typename: 'Product', id: 1, metrics: '1000/vpm' }]
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+
+      expect(handleNext).toHaveBeenCalledTimes(1);
+      expect(document).toMatchDocument(enabledInitialQuery);
+      expect(observable.options.query).toMatchDocument(initialQuery);
+      expect(observable.query).toMatchDocument(enabledInitialQuery);
+    });
+
+    enabled = false;
+
+    const { data } = await observable.fetchMore({
+      query: productsQuery,
+      variables: { offset: 1 },
+    });
+
+    expect(data).toEqual({
+      products: [{ __typename: 'Product', id: 2 }]
+    });
+
+    expect(document!).toMatchDocument(transformedProductsQuery);
+    expect(observable.options.query).toMatchDocument(initialQuery);
+      // Even though we pass a different query to `fetchMore`, we don't want to
+      // override the original query. We do however run transforms on the 
+      // initial query to ensure the broadcasted result and the cache match
+      // the expected query document in case the transforms contain a runtime
+      // condition that impacts the query in a significant way (such as removing 
+      // a field).
+    expect(observable.query).toMatchDocument(disabledInitialQuery);
+
+    // QueryInfo.notify is run in a setTimeout, so give time for it to run 
+    // before we make assertions on it.
+    await wait(0);
+
+    expect(handleNext).toHaveBeenCalledTimes(2);
+    expect(handleNext).toHaveBeenLastCalledWith({
+      data: {
+        currentUser: { id: 1 },
+        products: [
+          { __typename: 'Product', id: 1 },
+          { __typename: 'Product', id: 2 }
+        ]
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+    });
+  });
+
+  it('re-runs custom document transforms when calling `setVariables`', async () => {
+    const query = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          metrics @whenEnabled
+        }
+      }
+    `;
+
+    const enabledQuery = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          metrics
+          __typename
+        }
+      }
+    `;
+
+    const disabledQuery = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          __typename
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: enabledQuery, variables: { id: 1 } },
+        result: {
+          data: {
+            product: { __typename: 'Product', id: 1, metrics: '1000/vpm' }
+          }
+        }
+      },
+      {
+        request: { query: disabledQuery, variables: { id: 2 } },
+        result: {
+          data: {
+            product: { __typename: 'Product', id: 2 }
+          }
+        }
+      }
+    ];
+
+    let enabled = true;
+
+    const documentTransform = new DocumentTransform((document: DocumentNode) => {
+      return removeDirectivesFromDocument(
+        [{ name: 'whenEnabled', remove: !enabled }],
+        document
+      )!;
+    }, { cache: false });
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation, forward) => {
+      document = operation.query;
+
+      return forward(operation);
+    });
+
+    const client = new ApolloClient({
+      link: ApolloLink.from([link, new MockLink(mocks)]),
+      cache: new InMemoryCache(),
+      documentTransform,
+    });
+
+    const observable = client.watchQuery({ query, variables: { id: 1 } });
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: {
+          product: { __typename: 'Product', id: 1, metrics: '1000/vpm' }
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+
+      expect(document).toMatchDocument(enabledQuery);
+      expect(observable.options.query).toMatchDocument(query);
+      expect(observable.query).toMatchDocument(enabledQuery);
+    });
+
+    enabled = false;
+
+    const result = await observable.setVariables({ id: 2 });
+
+    expect(document!).toMatchDocument(disabledQuery);
+    expect(observable.options.query).toMatchDocument(query);
+    expect(observable.query).toMatchDocument(disabledQuery);
+
+    expect(result!.data).toEqual({
+      product: { __typename: 'Product', id: 2 }
+    })
+
+    expect(handleNext).toHaveBeenLastCalledWith({
+      data: {
+        product: { __typename: 'Product', id: 2 }
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+    });
+  });
+
+  it('re-runs custom document transforms when calling `setOptions`', async () => {
+    const query = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          metrics @whenEnabled
+        }
+      }
+    `;
+
+    const enabledQuery = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          metrics
+          __typename
+        }
+      }
+    `;
+
+    const disabledQuery = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          __typename
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: enabledQuery, variables: { id: 1 } },
+        result: {
+          data: {
+            product: { __typename: 'Product', id: 1, metrics: '1000/vpm' }
+          }
+        }
+      },
+      {
+        request: { query: disabledQuery, variables: { id: 2 } },
+        result: {
+          data: {
+            product: { __typename: 'Product', id: 2 }
+          }
+        }
+      }
+    ];
+
+    let enabled = true;
+
+    const documentTransform = new DocumentTransform((document: DocumentNode) => {
+      return removeDirectivesFromDocument(
+        [{ name: 'whenEnabled', remove: !enabled }],
+        document
+      )!;
+    }, { cache: false });
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation, forward) => {
+      document = operation.query;
+
+      return forward(operation);
+    });
+
+    const client = new ApolloClient({
+      link: ApolloLink.from([link, new MockLink(mocks)]),
+      cache: new InMemoryCache(),
+      documentTransform,
+    });
+
+    const observable = client.watchQuery({ query, variables: { id: 1 } });
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: {
+          product: { __typename: 'Product', id: 1, metrics: '1000/vpm' }
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+
+      expect(document).toMatchDocument(enabledQuery);
+      expect(observable.options.query).toMatchDocument(query);
+      expect(observable.query).toMatchDocument(enabledQuery);
+    });
+
+    enabled = false;
+
+    const { data } = await observable.setOptions({ variables: { id: 2 }});
+
+    expect(document!).toMatchDocument(disabledQuery);
+    expect(observable.options.query).toMatchDocument(query);
+    expect(observable.query).toMatchDocument(disabledQuery);
+
+    expect(data).toEqual({
+      product: { __typename: 'Product', id: 2 }
+    })
+
+    expect(handleNext).toHaveBeenLastCalledWith({
+      data: {
+        product: { __typename: 'Product', id: 2 }
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+    });
+  });
+
+  it('runs custom document transforms when passing a new query to `setOptions`', async () => {
+    const query = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          metrics @custom
+        }
+      }
+    `;
+
+    const transformedQuery = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          metrics
+          __typename
+        }
+      }
+    `;
+
+    const updatedQuery = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          name
+          metrics @custom
+        }
+      }
+    `;
+
+    const transformedUpdatedQuery = gql`
+      query TestQuery($id: ID!) {
+        product(id: $id) {
+          id
+          name
+          metrics
+          __typename
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: transformedQuery, variables: { id: 1 } },
+        result: {
+          data: {
+            product: { __typename: 'Product', id: 1, metrics: '1000/vpm' }
+          }
+        }
+      },
+      {
+        request: { query: transformedUpdatedQuery, variables: { id: 1 } },
+        result: {
+          data: {
+            product: { 
+              __typename: 'Product',
+              id: 1,
+              name: 'Acme Inc Product',
+              metrics: '1000/vpm'
+            }
+          }
+        }
+      }
+    ];
+
+    const documentTransform = new DocumentTransform((document: DocumentNode) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!;
+    });
+
+    let document: DocumentNode;
+
+    const link = new ApolloLink((operation, forward) => {
+      document = operation.query;
+
+      return forward(operation);
+    });
+
+    const client = new ApolloClient({
+      link: ApolloLink.from([link, new MockLink(mocks)]),
+      cache: new InMemoryCache(),
+      documentTransform,
+    });
+
+    const observable = client.watchQuery({ query, variables: { id: 1 } });
+    const handleNext = jest.fn();
+
+    observable.subscribe(handleNext);
+
+    await waitFor(() => {
+      expect(handleNext).toHaveBeenLastCalledWith({
+        data: mocks[0].result.data,
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+
+      expect(document).toMatchDocument(transformedQuery);
+      expect(observable.options.query).toMatchDocument(query);
+      expect(observable.query).toMatchDocument(transformedQuery);
+    });
+
+    const { data } = await observable.setOptions({ query: updatedQuery });
+
+    expect(document!).toMatchDocument(transformedUpdatedQuery);
+    expect(observable.options.query).toMatchDocument(updatedQuery);
+    expect(observable.query).toMatchDocument(transformedUpdatedQuery);
+
+    expect(data).toEqual(mocks[1].result.data);
+
+    expect(handleNext).toHaveBeenLastCalledWith({
+      data: mocks[1].result.data,
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+    });
+  });
+
+  it('runs custom document transforms with fragments defined in the fragment registery', async ()  => {
+    const query = gql`
+      query TestQuery {
+        product {
+          id
+          name @custom
+          ...ProductFields
+        }
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          product: {
+            __typename: 'Product',
+            id: 1,
+            name: 'Product',
+            description: 'Product description',
+          }
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({
+        fragments: createFragmentRegistry(gql`
+          fragment ProductFields on Product {
+            description @custom 
+          }
+        `)
+      }),
+      documentTransform,
+    });
+
+    const { data } = await client.query({ query });
+
+    expect(document!).toMatchDocument(gql`
+      query TestQuery {
+        product {
+          id
+          name
+          ...ProductFields
+          __typename
+        }
+      }
+
+      fragment ProductFields on Product {
+        description
+        __typename
+      }
+    `);
+
+    expect(data).toEqual({
+      product: {
+        __typename: 'Product',
+        id: 1,
+        name: 'Product',
+        description: 'Product description',
+      }
+    });
+  });
+
+  it('runs custom document transforms on fragments that override registered fragments in the fragment registery', async ()  => {
+    const query = gql`
+      query TestQuery {
+        product {
+          id
+          name @custom
+          ...ProductFields
+        }
+      }
+
+      fragment ProductFields on Product {
+        description @custom
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          product: {
+            __typename: 'Product',
+            id: 1,
+            name: 'Product',
+            description: 'Product description',
+          }
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({
+        fragments: createFragmentRegistry(gql`
+          fragment ProductFields on Product {
+            unused @custom
+          }
+        `)
+      }),
+      documentTransform,
+    });
+
+    const { data } = await client.query({ query });
+
+    expect(document!).toMatchDocument(gql`
+      query TestQuery {
+        product {
+          id
+          name
+          ...ProductFields
+          __typename
+        }
+      }
+
+      fragment ProductFields on Product {
+        description
+        __typename
+      }
+    `);
+
+    expect(data).toEqual({
+      product: {
+        __typename: 'Product',
+        id: 1,
+        name: 'Product',
+        description: 'Product description',
+      }
+    });
+  });
+
+  it('adds fragment definitions to the query for fragment spreads added from custom document transforms', async ()  => {
+    const query = gql`
+      query TestQuery {
+        product {
+          id
+          name
+        }
+      }
+    `;
+
+    let document: DocumentNode
+
+    const documentTransform = new DocumentTransform((document) => {
+      return visit(document, {
+        Field(node) {
+          if (node.name.value === 'product' && node.selectionSet) {
+            return {
+              ...node,
+              selectionSet: {
+                ...node.selectionSet,
+                selections: [
+                  ...node.selectionSet.selections,
+                  {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'ProductFields' }
+                  }
+                ]
+              }
+            }
+          }
+        } 
+      });
+    });
+
+    const link = new ApolloLink((operation) => {
+      document = operation.query;
+
+      return Observable.of({
+        data: {
+          product: {
+            __typename: 'Product',
+            id: 1,
+            name: 'Product',
+            description: 'Product description',
+          }
+        }
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({
+        fragments: createFragmentRegistry(gql`
+          fragment ProductFields on Product {
+            description
+          }
+        `)
+      }),
+      documentTransform,
+    });
+
+    const { data } = await client.query({ query });
+
+    expect(document!).toMatchDocument(gql`
+      query TestQuery {
+        product {
+          id
+          name
+          __typename
+          ...ProductFields
+        }
+      }
+
+      fragment ProductFields on Product {
+        description
+        __typename
+      }
+    `);
+
+    expect(data).toEqual({
+      product: {
+        __typename: 'Product',
+        id: 1,
+        name: 'Product',
+        description: 'Product description',
+      }
+    });
+  });
+
+  it('runs custom transforms on active queries when calling `refetchQueries` with "include"', async () => {
+    const aQuery = gql`
+      query A { 
+        a @custom
+      }
+    `;
+    const bQuery = gql`
+      query B { 
+        b @custom
+      }
+    `;
+    const abQuery = gql`
+      query AB { 
+        a @custom
+        b
+      }
+    `;
+
+    const requests: Operation[] = []
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const client = new ApolloClient({
+      documentTransform,
+      cache: new InMemoryCache(),
+      link: new ApolloLink((operation) => {
+        requests.push(operation);
+
+        return Observable.of({
+          data: operation.operationName
+            .split('')
+            .reduce<Record<string, string>>(
+              (memo, letter) => ({ 
+                ...memo, 
+                [letter.toLowerCase()]: letter.toUpperCase()
+              }), 
+              {}
+            )
+        });
+      })
+    });
+
+    client.watchQuery({ query: aQuery }).subscribe(jest.fn());
+    client.watchQuery({ query: bQuery }).subscribe(jest.fn());
+    // purposely avoid subscribing to prevent it from being an "active" query
+    client.watchQuery({ query: abQuery });
+
+    await waitFor(() => {
+      return (
+        client.readQuery({ query: aQuery }) && 
+        client.readQuery({ query: bQuery })
+      );
+    });
+
+    expect(requests.length).toBe(2);
+    expect(requests[0].query).toMatchDocument(gql`
+      query A {
+        a
+      }      
+    `);
+    expect(requests[1].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+
+    const results = await client.refetchQueries({ include: 'active' });
+
+    expect(results.map(r => r.data)).toEqual([
+      { a: 'A' },
+      { b: 'B' }
+    ]);
+
+    expect(requests.length).toBe(4);
+    expect(requests[2].query).toMatchDocument(gql`
+      query A {
+        a
+      }      
+    `);
+    expect(requests[3].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+  });
+
+  it('runs custom transforms on all queries when calling `refetchQueries` with "all"', async () => {
+    const aQuery = gql`
+      query A { 
+        a @custom
+      }
+    `;
+    const bQuery = gql`
+      query B { 
+        b @custom
+      }
+    `;
+    const abQuery = gql`
+      query AB { 
+        a @custom
+        b
+      }
+    `;
+
+    const requests: Operation[] = []
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const client = new ApolloClient({
+      documentTransform,
+      cache: new InMemoryCache(),
+      link: new ApolloLink((operation) => {
+        requests.push(operation);
+
+        return Observable.of({
+          data: operation.operationName
+            .split('')
+            .reduce<Record<string, string>>(
+              (memo, letter) => ({ 
+                ...memo, 
+                [letter.toLowerCase()]: letter.toUpperCase()
+              }), 
+              {}
+            )
+        });
+      })
+    });
+
+    client.watchQuery({ query: aQuery }).subscribe(jest.fn());
+    client.watchQuery({ query: bQuery }).subscribe(jest.fn());
+    // purposely avoid subscribing to prevent it from being an "active" query
+    client.watchQuery({ query: abQuery });
+
+    await waitFor(() => {
+      return (
+        client.readQuery({ query: aQuery }) && 
+        client.readQuery({ query: bQuery })
+      );
+    });
+
+    expect(requests.length).toBe(2);
+    expect(requests[0].query).toMatchDocument(gql`
+      query A {
+        a
+      }      
+    `);
+    expect(requests[1].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+
+    const results = await client.refetchQueries({ include: 'all' });
+
+    expect(results.map(r => r.data)).toEqual([
+      { a: 'A' },
+      { b: 'B' },
+      { a: 'A', b: 'B' }
+    ]);
+
+    expect(requests.length).toBe(5);
+    expect(requests[2].query).toMatchDocument(gql`
+      query A {
+        a
+      }      
+    `);
+    expect(requests[3].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+    expect(requests[4].query).toMatchDocument(gql`
+      query AB {
+        a
+        b
+      }      
+    `);
+  });
+
+  it('runs custom transforms on matched queries when calling `refetchQueries` with string array', async () => {
+    const aQuery = gql`
+      query A { 
+        a @custom
+      }
+    `;
+    const bQuery = gql`
+      query B { 
+        b @custom
+      }
+    `;
+
+    const requests: Operation[] = []
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const client = new ApolloClient({
+      documentTransform,
+      cache: new InMemoryCache(),
+      link: new ApolloLink((operation) => {
+        requests.push(operation);
+
+        return Observable.of({
+          data: operation.operationName
+            .split('')
+            .reduce<Record<string, string>>(
+              (memo, letter) => ({ 
+                ...memo, 
+                [letter.toLowerCase()]: letter.toUpperCase()
+              }), 
+              {}
+            )
+        });
+      })
+    });
+
+    client.watchQuery({ query: aQuery }).subscribe(jest.fn());
+    client.watchQuery({ query: bQuery }).subscribe(jest.fn());
+
+    await waitFor(() => {
+      return (
+        client.readQuery({ query: aQuery }) && 
+        client.readQuery({ query: bQuery })
+      );
+    });
+
+    expect(requests.length).toBe(2);
+    expect(requests[0].query).toMatchDocument(gql`
+      query A {
+        a
+      }      
+    `);
+    expect(requests[1].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+
+    const results = await client.refetchQueries({ 
+      include: ['B']
+    });
+
+    expect(results.map(r => r.data)).toEqual([
+      { b: 'B' },
+    ]);
+
+    expect(requests.length).toBe(3);
+    expect(requests[2].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+  });
+
+  it('runs custom transforms on matched queries when calling `refetchQueries` with document nodes', async () => {
+    const aQuery = gql`
+      query A { 
+        a @custom
+      }
+    `;
+    const bQuery = gql`
+      query B { 
+        b @custom
+      }
+    `;
+
+    const requests: Operation[] = []
+
+    const documentTransform = new DocumentTransform((document) => {
+      return removeDirectivesFromDocument([{ name: 'custom' }], document)!
+    });
+
+    const client = new ApolloClient({
+      documentTransform,
+      cache: new InMemoryCache(),
+      link: new ApolloLink((operation) => {
+        requests.push(operation);
+
+        return Observable.of({
+          data: operation.operationName
+            .split('')
+            .reduce<Record<string, string>>(
+              (memo, letter) => ({ 
+                ...memo, 
+                [letter.toLowerCase()]: letter.toUpperCase()
+              }), 
+              {}
+            )
+        });
+      })
+    });
+
+    client.watchQuery({ query: aQuery }).subscribe(jest.fn());
+    client.watchQuery({ query: bQuery }).subscribe(jest.fn());
+
+    await waitFor(() => {
+      return (
+        client.readQuery({ query: aQuery }) && 
+        client.readQuery({ query: bQuery })
+      );
+    });
+
+    expect(requests.length).toBe(2);
+    expect(requests[0].query).toMatchDocument(gql`
+      query A {
+        a
+      }      
+    `);
+    expect(requests[1].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+
+    const results = await client.refetchQueries({ 
+      include: [bQuery]
+    });
+
+    expect(results.map(r => r.data)).toEqual([
+      { b: 'B' },
+    ]);
+
+    expect(requests.length).toBe(3);
+    expect(requests[2].query).toMatchDocument(gql`
+      query B {
+        b
+      }      
+    `);
+  });
+});
+
 function clientRoundtrip(
   resolve: (result: any) => any,
   reject: (reason: any) => any,
diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts
--- a/src/__tests__/exports.ts
+++ b/src/__tests__/exports.ts
@@ -6,6 +6,7 @@
 import * as cache from "../cache";
 import * as client from "..";
 import * as core from "../core";
+import * as dev from '../dev';
 import * as errors from "../errors";
 import * as linkBatch from "../link/batch";
 import * as linkBatchHTTP from "../link/batch-http";
@@ -15,6 +16,7 @@ import * as linkError from "../link/error";
 import * as linkHTTP from "../link/http";
 import * as linkPersistedQueries from "../link/persisted-queries";
 import * as linkRetry from "../link/retry";
+import * as linkRemoveTypename from "../link/remove-typename";
 import * as linkSchema from "../link/schema";
 import * as linkSubscriptions from "../link/subscriptions";
 import * as linkUtils from "../link/utils";
@@ -48,6 +50,7 @@ describe('exports of public entry points', () => {
   check("@apollo/client", client);
   check("@apollo/client/cache", cache);
   check("@apollo/client/core", core);
+  check("@apollo/client/dev", dev);
   check("@apollo/client/errors", errors);
   check("@apollo/client/link/batch", linkBatch);
   check("@apollo/client/link/batch-http", linkBatchHTTP);
@@ -56,6 +59,7 @@ describe('exports of public entry points', () => {
   check("@apollo/client/link/error", linkError);
   check("@apollo/client/link/http", linkHTTP);
   check("@apollo/client/link/persisted-queries", linkPersistedQueries);
+  check("@apollo/client/link/remove-typename", linkRemoveTypename);
   check("@apollo/client/link/retry", linkRetry);
   check("@apollo/client/link/schema", linkSchema);
   check("@apollo/client/link/subscriptions", linkSubscriptions);
diff --git a/src/__tests__/local-state/__snapshots__/export.ts.snap b/src/__tests__/local-state/__snapshots__/export.ts.snap
--- a/src/__tests__/local-state/__snapshots__/export.ts.snap
+++ b/src/__tests__/local-state/__snapshots__/export.ts.snap
@@ -4,14 +4,18 @@ exports[`@client @export tests should NOT refetch if an @export variable has not
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'postCount' while writing result {
-  \\"currentAuthorId\\": 101
-}",
+      "Missing field '%s' while writing result %o",
+      "postCount",
+      Object {
+        "currentAuthorId": 101,
+      },
     ],
     Array [
-      "Missing field 'postCount' while writing result {
-  \\"currentAuthorId\\": 100
-}",
+      "Missing field '%s' while writing result %o",
+      "postCount",
+      Object {
+        "currentAuthorId": 100,
+      },
     ],
   ],
   "results": Array [
@@ -31,13 +35,15 @@ exports[`@client @export tests should allow @client @export variables to be used
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'postCount' while writing result {
-  \\"currentAuthor\\": {
-    \\"name\\": \\"John Smith\\",
-    \\"authorId\\": 100,
-    \\"__typename\\": \\"Author\\"
-  }
-}",
+      "Missing field '%s' while writing result %o",
+      "postCount",
+      Object {
+        "currentAuthor": Object {
+          "__typename": "Author",
+          "authorId": 100,
+          "name": "John Smith",
+        },
+      },
     ],
   ],
   "results": Array [
@@ -53,63 +59,79 @@ exports[`@client @export tests should refetch if an @export variable changes, th
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'postCount' while writing result {
-  \\"appContainer\\": {
-    \\"systemDetails\\": {
-      \\"currentAuthor\\": {
-        \\"name\\": \\"John Smith\\",
-        \\"authorId\\": 100,
-        \\"__typename\\": \\"Author\\"
+      "Missing field '%s' while writing result %o",
+      "postCount",
+      Object {
+        "appContainer": Object {
+          "__typename": "AppContainer",
+          "systemDetails": Object {
+            "__typename": "SystemDetails",
+            "currentAuthor": Object {
+              "__typename": "Author",
+              "authorId": 100,
+              "name": "John Smith",
+            },
+          },
+        },
       },
-      \\"__typename\\": \\"SystemDetails\\"
-    },
-    \\"__typename\\": \\"AppContainer\\"
-  }
-}",
     ],
     Array [
-      "Missing field 'title' while writing result {
-  \\"loggedInReviewerId\\": 100,
-  \\"__typename\\": \\"Post\\",
-  \\"id\\": 10
-}",
+      "Missing field '%s' while writing result %o",
+      "title",
+      Object {
+        "__typename": "Post",
+        "id": 10,
+        "loggedInReviewerId": 100,
+      },
     ],
     Array [
-      "Missing field 'reviewerDetails' while writing result {
-  \\"postRequiringReview\\": {
-    \\"loggedInReviewerId\\": 100,
-    \\"__typename\\": \\"Post\\",
-    \\"id\\": 10
-  }
-}",
+      "Missing field '%s' while writing result %o",
+      "reviewerDetails",
+      Object {
+        "postRequiringReview": Object {
+          "__typename": "Post",
+          "id": 10,
+          "loggedInReviewerId": 100,
+        },
+      },
     ],
     Array [
-      "Missing field 'id' while writing result {
-  \\"__typename\\": \\"Post\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "id",
+      Object {
+        "__typename": "Post",
+      },
     ],
     Array [
-      "Missing field 'title' while writing result {
-  \\"__typename\\": \\"Post\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "title",
+      Object {
+        "__typename": "Post",
+      },
     ],
     Array [
-      "Missing field 'reviewerDetails' while writing result {
-  \\"postRequiringReview\\": {
-    \\"__typename\\": \\"Post\\"
-  }
-}",
+      "Missing field '%s' while writing result %o",
+      "reviewerDetails",
+      Object {
+        "postRequiringReview": Object {
+          "__typename": "Post",
+        },
+      },
     ],
     Array [
-      "Missing field 'post' while writing result {
-  \\"primaryReviewerId\\": 100,
-  \\"secondaryReviewerId\\": 200
-}",
+      "Missing field '%s' while writing result %o",
+      "post",
+      Object {
+        "primaryReviewerId": 100,
+        "secondaryReviewerId": 200,
+      },
     ],
     Array [
-      "Missing field 'postCount' while writing result {
-  \\"currentAuthorId\\": 100
-}",
+      "Missing field '%s' while writing result %o",
+      "postCount",
+      Object {
+        "currentAuthorId": 100,
+      },
     ],
   ],
   "results": Array [
diff --git a/src/__tests__/local-state/__snapshots__/general.ts.snap b/src/__tests__/local-state/__snapshots__/general.ts.snap
--- a/src/__tests__/local-state/__snapshots__/general.ts.snap
+++ b/src/__tests__/local-state/__snapshots__/general.ts.snap
@@ -8,9 +8,11 @@ exports[`Combining client and server state/operations should handle a simple que
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'lastCount' while writing result {
-  \\"count\\": 0
-}",
+      "Missing field '%s' while writing result %o",
+      "lastCount",
+      Object {
+        "count": 0,
+      },
     ],
   ],
   "results": Array [
@@ -26,11 +28,13 @@ exports[`Combining client and server state/operations should support nested quer
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'lastName' while writing result {
-  \\"__typename\\": \\"User\\",
-  \\"id\\": 123,
-  \\"firstName\\": \\"John\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "lastName",
+      Object {
+        "__typename": "User",
+        "firstName": "John",
+        "id": 123,
+      },
     ],
   ],
   "results": Array [
diff --git a/src/cache/core/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts
--- a/src/cache/core/__tests__/cache.ts
+++ b/src/cache/core/__tests__/cache.ts
@@ -2,7 +2,7 @@ import gql from 'graphql-tag';
 import { ApolloCache } from '../cache';
 import { Cache, DataProxy } from '../..';
 import { Reference } from '../../../utilities/graphql/storeUtils';
-
+import { expectTypeOf } from 'expect-type'
 class TestCache extends ApolloCache<unknown> {
   constructor() {
     super();
@@ -308,3 +308,142 @@ describe('abstract cache', () => {
     });
   });
 });
+
+describe.skip('Cache type tests', () => {
+  describe('modify', () => {
+    test('field types are inferred correctly from passed entity type', () => {
+      const cache = new TestCache();
+      cache.modify<{
+        prop1: string;
+        prop2: number;
+        child: {
+          someObject: true
+        },
+        children: {
+          anotherObject: false
+        }[]
+      }>({
+        fields: {
+          prop1(field) {
+            expectTypeOf(field).toEqualTypeOf<string>();
+            return field;
+          },
+          prop2(field) {
+            expectTypeOf(field).toEqualTypeOf<number>();
+            return field;
+          },
+          child(field) {
+            expectTypeOf(field).toEqualTypeOf<{ someObject: true } | Reference>();
+            return field;
+          },
+          children(field) {
+            expectTypeOf(field).toEqualTypeOf<(ReadonlyArray<{ anotherObject: false }>) | ReadonlyArray<Reference>>();
+            return field;
+          }
+        }
+      })
+    })
+    test('field method needs to return a value of the correct type', () => {
+      const cache = new TestCache();
+      cache.modify<{ p1: string, p2: string, p3: string, p4: string, p5: string }>({
+        fields: {
+          p1() { return "" },
+          // @ts-expect-error returns wrong type
+          p2() { return 1 },
+          // @ts-expect-error needs return statement
+          p3() {},
+          p4(_, { DELETE }) { return DELETE },
+          p5(_, { INVALIDATE }) { return INVALIDATE },
+        }
+      })
+    })
+    test('passing a function as `field` should infer all entity properties as possible input (interfaces)', () => {
+      interface ParentEntity {
+        prop1: string;
+        prop2: number;
+        child: ChildEntity;
+      }
+      interface ChildEntity {
+        prop1: boolean;
+        prop2: symbol;
+        children: OtherChildEntry[];
+      }
+      interface OtherChildEntry {
+        foo: false
+      }
+
+      const cache = new TestCache();
+      // with reference
+      cache.modify<ParentEntity>({
+        id: 'foo',
+        fields(field) {
+          expectTypeOf(field).toEqualTypeOf<string | number | ChildEntity | Reference>();
+          return field;
+        }
+      })
+      // without reference
+      cache.modify<ChildEntity>({
+        id: 'foo',
+        fields(field) {
+          expectTypeOf(field).toEqualTypeOf<boolean | symbol | readonly OtherChildEntry[] | readonly Reference[]>();
+          return field;
+        }
+      })
+    })
+    test('passing a function as `field` should infer all entity properties as possible input (types)', () => {
+      type ParentEntity = {
+        prop1: string;
+        prop2: number;
+        child: ChildEntity;
+      }
+      type ChildEntity = {
+        prop1: boolean;
+        prop2: symbol;
+        children: OtherChildEntry[];
+      }
+      type OtherChildEntry = {
+        foo: false
+      }
+
+      const cache = new TestCache();
+      // with reference
+      cache.modify<ParentEntity>({
+        id: 'foo',
+        fields(field) {
+          expectTypeOf(field).toEqualTypeOf<string | number | ChildEntity | Reference>();
+          return field;
+        }
+      })
+      // without reference
+      cache.modify<ChildEntity>({
+        id: 'foo',
+        fields(field) {
+          expectTypeOf(field).toEqualTypeOf<boolean | symbol | readonly OtherChildEntry[] | readonly Reference[]>();
+          return field;
+        }
+      })
+    })
+    test('passing a function as `field` w/o specifying an entity type', () => {
+      const cache = new TestCache();
+      cache.modify({
+        id: 'foo',
+        fields(field) {
+          expectTypeOf(field).toEqualTypeOf<any>();
+          return field;
+        }
+      });
+    });
+    test('passing a function as `field` property w/o specifying an entity type', () => {
+      const cache = new TestCache();
+      cache.modify({
+        id: 'foo',
+        fields: {
+          p1(field) {
+            expectTypeOf(field).toEqualTypeOf<any>();
+            return field;
+          }
+        }
+      });
+    });
+  });
+});
diff --git a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap
--- a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap
+++ b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap
@@ -54,29 +54,33 @@ exports[`type policies complains about missing key fields 1`] = `
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'title' while writing result {
-  \\"year\\": 2011,
-  \\"theInformationBookData\\": {
-    \\"__typename\\": \\"Book\\",
-    \\"isbn\\": \\"1400096235\\",
-    \\"title\\": \\"The Information\\",
-    \\"subtitle\\": \\"A History, a Theory, a Flood\\",
-    \\"author\\": {
-      \\"name\\": \\"James Gleick\\"
-    }
-  }
-}",
+      "Missing field '%s' while writing result %o",
+      "title",
+      Object {
+        "theInformationBookData": Object {
+          "__typename": "Book",
+          "author": Object {
+            "name": "James Gleick",
+          },
+          "isbn": "1400096235",
+          "subtitle": "A History, a Theory, a Flood",
+          "title": "The Information",
+        },
+        "year": 2011,
+      },
     ],
     Array [
-      "Missing field 'year' while writing result {
-  \\"__typename\\": \\"Book\\",
-  \\"isbn\\": \\"1400096235\\",
-  \\"title\\": \\"The Information\\",
-  \\"subtitle\\": \\"A History, a Theory, a Flood\\",
-  \\"author\\": {
-    \\"name\\": \\"James Gleick\\"
-  }
-}",
+      "Missing field '%s' while writing result %o",
+      "year",
+      Object {
+        "__typename": "Book",
+        "author": Object {
+          "name": "James Gleick",
+        },
+        "isbn": "1400096235",
+        "subtitle": "A History, a Theory, a Flood",
+        "title": "The Information",
+      },
     ],
   ],
   "results": Array [
@@ -1280,11 +1284,13 @@ exports[`type policies field policies readField helper function calls custom rea
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'blockers' while writing result {
-  \\"__typename\\": \\"Task\\",
-  \\"id\\": 4,
-  \\"description\\": \\"grandchild task\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "blockers",
+      Object {
+        "__typename": "Task",
+        "description": "grandchild task",
+        "id": 4,
+      },
     ],
   ],
   "results": Array [
@@ -1300,34 +1306,40 @@ exports[`type policies field policies runs nested merge functions as well as anc
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'time' while writing result {
-  \\"__typename\\": \\"Event\\",
-  \\"id\\": 123
-}",
+      "Missing field '%s' while writing result %o",
+      "time",
+      Object {
+        "__typename": "Event",
+        "id": 123,
+      },
     ],
     Array [
-      "Missing field 'time' while writing result {
-  \\"__typename\\": \\"Event\\",
-  \\"id\\": 345,
-  \\"name\\": \\"Rooftop dog party\\",
-  \\"attendees\\": [
-    {
-      \\"__typename\\": \\"Attendee\\",
-      \\"id\\": 456,
-      \\"name\\": \\"Inspector Beckett\\"
-    },
-    {
-      \\"__typename\\": \\"Attendee\\",
-      \\"id\\": 234
-    }
-  ]
-}",
+      "Missing field '%s' while writing result %o",
+      "time",
+      Object {
+        "__typename": "Event",
+        "attendees": Array [
+          Object {
+            "__typename": "Attendee",
+            "id": 456,
+            "name": "Inspector Beckett",
+          },
+          Object {
+            "__typename": "Attendee",
+            "id": 234,
+          },
+        ],
+        "id": 345,
+        "name": "Rooftop dog party",
+      },
     ],
     Array [
-      "Missing field 'name' while writing result {
-  \\"__typename\\": \\"Attendee\\",
-  \\"id\\": 234
-}",
+      "Missing field '%s' while writing result %o",
+      "name",
+      Object {
+        "__typename": "Attendee",
+        "id": 234,
+      },
     ],
   ],
   "results": Array [
@@ -1351,10 +1363,12 @@ exports[`type policies readField warns if explicitly passed undefined \`from\` o
 [MockFunction] {
   "calls": Array [
     Array [
-      "Undefined 'from' passed to readField with arguments [{\\"fieldName\\":\\"firstName\\",\\"from\\":<undefined>}]",
+      "Undefined 'from' passed to readField with arguments %s",
+      "[{\\"fieldName\\":\\"firstName\\",\\"from\\":<undefined>}]",
     ],
     Array [
-      "Undefined 'from' passed to readField with arguments [\\"lastName\\",<undefined>]",
+      "Undefined 'from' passed to readField with arguments %s",
+      "[\\"lastName\\",<undefined>]",
     ],
   ],
   "results": Array [
diff --git a/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap
--- a/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap
+++ b/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap
@@ -4,11 +4,13 @@ exports[`roundtrip fragments should throw an error on two of the same inline fra
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'rank' while writing result {
-  \\"__typename\\": \\"Jedi\\",
-  \\"name\\": \\"Luke Skywalker\\",
-  \\"side\\": \\"bright\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "rank",
+      Object {
+        "__typename": "Jedi",
+        "name": "Luke Skywalker",
+        "side": "bright",
+      },
     ],
   ],
   "results": Array [
@@ -24,11 +26,13 @@ exports[`roundtrip fragments should throw on error on two of the same spread fra
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'rank' while writing result {
-  \\"__typename\\": \\"Jedi\\",
-  \\"name\\": \\"Luke Skywalker\\",
-  \\"side\\": \\"bright\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "rank",
+      Object {
+        "__typename": "Jedi",
+        "name": "Luke Skywalker",
+        "side": "bright",
+      },
     ],
   ],
   "results": Array [
diff --git a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
--- a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
+++ b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
@@ -75,10 +75,12 @@ exports[`writing to the store should not keep reference when type of mixed inlin
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'id' while writing result {
-  \\"__typename\\": \\"Cat\\",
-  \\"name\\": \\"cat\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "id",
+      Object {
+        "__typename": "Cat",
+        "name": "cat",
+      },
     ],
   ],
   "results": Array [
@@ -205,12 +207,14 @@ exports[`writing to the store writeResultToStore shape checking should warn when
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'price' while writing result {
-  \\"id\\": \\"1\\",
-  \\"name\\": \\"Todo 1\\",
-  \\"description\\": \\"Description 1\\",
-  \\"__typename\\": \\"ShoppingCartItem\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "price",
+      Object {
+        "__typename": "ShoppingCartItem",
+        "description": "Description 1",
+        "id": "1",
+        "name": "Todo 1",
+      },
     ],
   ],
   "results": Array [
@@ -226,10 +230,12 @@ exports[`writing to the store writeResultToStore shape checking should warn when
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'description' while writing result {
-  \\"id\\": \\"1\\",
-  \\"name\\": \\"Todo 1\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "description",
+      Object {
+        "id": "1",
+        "name": "Todo 1",
+      },
     ],
   ],
   "results": Array [
@@ -245,10 +251,12 @@ exports[`writing to the store writeResultToStore shape checking should write the
 [MockFunction] {
   "calls": Array [
     Array [
-      "Missing field 'description' while writing result {
-  \\"id\\": \\"1\\",
-  \\"name\\": \\"Todo 1\\"
-}",
+      "Missing field '%s' while writing result %o",
+      "description",
+      Object {
+        "id": "1",
+        "name": "Todo 1",
+      },
     ],
   ],
   "results": Array [
diff --git a/src/cache/inmemory/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts
--- a/src/cache/inmemory/__tests__/cache.ts
+++ b/src/cache/inmemory/__tests__/cache.ts
@@ -1,4 +1,5 @@
 import gql, { disableFragmentWarnings } from 'graphql-tag';
+import { expectTypeOf } from 'expect-type'
 
 import { cloneDeep } from '../../../utilities/common/cloneDeep';
 import { makeReference, Reference, makeVar, TypedDocumentNode, isReference, DocumentNode } from '../../../core';
@@ -6,8 +7,6 @@ import { Cache } from '../../../cache';
 import { InMemoryCache } from '../inMemoryCache';
 import { InMemoryCacheConfig } from '../types';
 
-jest.mock('optimism');
-import { wrap } from 'optimism';
 import { StoreReader } from '../readFromStore';
 import { StoreWriter } from '../writeToStore';
 import { ObjectCanon } from '../object-canon';
@@ -2011,32 +2010,21 @@ describe('Cache', () => {
 });
 
 describe('resultCacheMaxSize', () => {
-  let wrapSpy: jest.Mock = wrap as jest.Mock;
-  beforeEach(() => {
-    wrapSpy.mockClear();
-  });
-
-  it("does not set max size on caches if resultCacheMaxSize is not configured", () => {
-    new InMemoryCache();
-    expect(wrapSpy).toHaveBeenCalled();
+  const defaultMaxSize = Math.pow(2, 16);
 
-    // The first wrap call is for getFragmentQueryDocument which intentionally
-    // does not have a max set since it's not expected to grow.
-    wrapSpy.mock.calls.splice(1).forEach(([, { max }]) => {
-      expect(max).toBeUndefined();
-    })
+  it("uses default max size on caches if resultCacheMaxSize is not configured", () => {
+    const cache = new InMemoryCache();
+    expect(cache["maybeBroadcastWatch"].options.max).toBe(defaultMaxSize);
+    expect(cache["storeReader"]["executeSelectionSet"].options.max).toBe(defaultMaxSize);
+    expect(cache["getFragmentDoc"].options.max).toBe(defaultMaxSize);
   });
 
   it("configures max size on caches when resultCacheMaxSize is set", () => {
     const resultCacheMaxSize = 12345;
-    new InMemoryCache({ resultCacheMaxSize });
-    expect(wrapSpy).toHaveBeenCalled();
-
-    // The first wrap call is for getFragmentQueryDocument which intentionally
-    // does not have a max set since it's not expected to grow.
-    wrapSpy.mock.calls.splice(1).forEach(([, { max }]) => {
-      expect(max).toBe(resultCacheMaxSize);
-    })
+    const cache = new InMemoryCache({ resultCacheMaxSize });
+    expect(cache["maybeBroadcastWatch"].options.max).toBe(resultCacheMaxSize);
+    expect(cache["storeReader"]["executeSelectionSet"].options.max).toBe(resultCacheMaxSize);
+    expect(cache["getFragmentDoc"].options.max).toBe(defaultMaxSize);
   });
 });
 
@@ -2830,7 +2818,7 @@ describe("InMemoryCache#modify", () => {
 
     cache.modify({
       fields: {
-        comments(comments: Reference[], { readField }) {
+        comments(comments: readonly Reference[], { readField }) {
           expect(Object.isFrozen(comments)).toBe(true);
           expect(comments.length).toBe(3);
           const filtered = comments.filter(comment => {
@@ -2915,6 +2903,7 @@ describe("InMemoryCache#modify", () => {
         expect(fieldName).not.toBe("b");
         if (fieldName === "a") expect(value).toBe(1);
         if (fieldName === "c") expect(value).toBe(3);
+        return value;
       },
       optimistic: true,
     });
@@ -3918,18 +3907,43 @@ describe('TypedDocumentNode<Data, Variables>', () => {
     }
   `;
 
-  it('should determine Data and Variables types of {write,read}{Query,Fragment}', () => {
-    const cache = new InMemoryCache({
+  // We need to define these objects separately from calling writeQuery,
+  // because passing them directly to writeQuery will trigger excess property
+  // warnings due to the extra __typename and isbn fields. Internally, we
+  // almost never pass object literals to writeQuery or writeFragment, so
+  // excess property checks should not be a problem in practice.
+  const jcmAuthor = {
+    __typename: "Author",
+    name: "John C. Mitchell",
+  };
+
+  const ffplBook = {
+    __typename: "Book",
+    isbn: "0262133210",
+    title: "Foundations for Programming Languages",
+    author: jcmAuthor,
+  };
+
+  const ffplVariables = {
+    isbn: "0262133210",
+  };
+
+  function getBookCache() {
+    return new InMemoryCache({
       typePolicies: {
         Query: {
           fields: {
             book(existing, { args, toReference }) {
-              return existing ?? (args && toReference({
-                __typename: "Book",
-                isbn: args.isbn,
-              }));
-            }
-          }
+              return (
+                existing ??
+                (args &&
+                  toReference({
+                    __typename: "Book",
+                    isbn: args.isbn,
+                  }))
+              );
+            },
+          },
         },
 
         Book: {
@@ -3941,27 +3955,10 @@ describe('TypedDocumentNode<Data, Variables>', () => {
         },
       },
     });
+  }
 
-    // We need to define these objects separately from calling writeQuery,
-    // because passing them directly to writeQuery will trigger excess property
-    // warnings due to the extra __typename and isbn fields. Internally, we
-    // almost never pass object literals to writeQuery or writeFragment, so
-    // excess property checks should not be a problem in practice.
-    const jcmAuthor = {
-      __typename: "Author",
-      name: "John C. Mitchell",
-    };
-
-    const ffplBook = {
-      __typename: "Book",
-      isbn: "0262133210",
-      title: "Foundations for Programming Languages",
-      author: jcmAuthor,
-    };
-
-    const ffplVariables = {
-      isbn: "0262133210",
-    };
+  it("should determine Data and Variables types of {write,read}{Query,Fragment}", () => {
+    const cache = getBookCache();
 
     cache.writeQuery({
       query,
@@ -4041,4 +4038,40 @@ describe('TypedDocumentNode<Data, Variables>', () => {
       },
     });
   });
+
+  it.skip("should infer the types of modifier fields", () => {
+    const cache = getBookCache();
+
+    cache.writeQuery({
+      query,
+      variables: ffplVariables,
+      data: {
+        book: ffplBook,
+      },
+    });
+
+    cache.modify<Book>({
+      id: cache.identify(ffplBook),
+      fields: {
+        isbn: (value) => {
+          expectTypeOf(value).toEqualTypeOf<string>();
+          return value;
+        },
+        title: (value, { INVALIDATE }) => {
+          expectTypeOf(value).toEqualTypeOf<string>();
+          return INVALIDATE;
+        },
+        author: (value, { DELETE, isReference }) => {
+          expectTypeOf(value).toEqualTypeOf<Reference | Book["author"]>();
+          if (isReference(value)) {
+            expectTypeOf(value).toEqualTypeOf<Reference>();
+          } else {
+            expectTypeOf(value).toEqualTypeOf<Book["author"]>();
+          }
+
+          return DELETE;
+        },
+      },
+    });
+  });
 });
diff --git a/src/cache/inmemory/__tests__/entityStore.ts b/src/cache/inmemory/__tests__/entityStore.ts
--- a/src/cache/inmemory/__tests__/entityStore.ts
+++ b/src/cache/inmemory/__tests__/entityStore.ts
@@ -8,6 +8,8 @@ import { Cache } from '../../core/types/Cache';
 import { Reference, makeReference, isReference, StoreValue } from '../../../utilities/graphql/storeUtils';
 import { MissingFieldError } from '../..';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { stringifyForDisplay } from '../../../utilities';
+import { InvariantError } from '../../../utilities/globals';
 
 describe('EntityStore', () => {
   it('should support result caching if so configured', () => {
@@ -1782,11 +1784,11 @@ describe('EntityStore', () => {
       try {
         expect(cache.identify(ABCs)).toBeUndefined();
         expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
-        expect(consoleWarnSpy).toHaveBeenCalledWith(
-          new Error(`Missing field 'b' while extracting keyFields from ${
-            JSON.stringify(ABCs)
-          }`),
-        );
+        expect(consoleWarnSpy).toHaveBeenCalledWith(new InvariantError(
+          `Missing field 'b' while extracting keyFields from ${
+            stringifyForDisplay(ABCs, 2)
+          }`,
+        ));
       } finally {
         consoleWarnSpy.mockRestore();
       }
diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts
--- a/src/cache/inmemory/__tests__/fragmentMatcher.ts
+++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts
@@ -234,8 +234,8 @@ describe("policies.fragmentMatches", () => {
 
   beforeEach(() => {
     warnings.length = 0;
-    console.warn = function (message: any) {
-      warnings.push(message);
+    console.warn = function (...args: any) {
+      warnings.push(args);
     };
   });
 
@@ -384,9 +384,9 @@ describe("policies.fragmentMatches", () => {
     });
 
     expect(warnings).toEqual([
-      "Inferring subtype E of supertype C",
-      "Inferring subtype F of supertype C",
-      "Inferring subtype G of supertype C",
+      ["Inferring subtype %s of supertype %s", "E", "C"],
+      ["Inferring subtype %s of supertype %s", "F", "C"],
+      ["Inferring subtype %s of supertype %s", "G", "C"],
       // Note that TooLong is not inferred here.
     ]);
 
diff --git a/src/cache/inmemory/__tests__/fragmentRegistry.ts b/src/cache/inmemory/__tests__/fragmentRegistry.ts
--- a/src/cache/inmemory/__tests__/fragmentRegistry.ts
+++ b/src/cache/inmemory/__tests__/fragmentRegistry.ts
@@ -90,6 +90,7 @@ describe("FragmentRegistry", () => {
           loading: true,
           networkStatus: NetworkStatus.loading,
           data: {
+            __typename: 'Query',
             source: "local",
           },
         });
@@ -99,6 +100,7 @@ describe("FragmentRegistry", () => {
           loading: false,
           networkStatus: NetworkStatus.ready,
           data: {
+            __typename: 'Query',
             source: "link",
           },
         });
diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts
--- a/src/cache/inmemory/__tests__/policies.ts
+++ b/src/cache/inmemory/__tests__/policies.ts
@@ -4,7 +4,7 @@ import { InMemoryCache } from "../inMemoryCache";
 import { ReactiveVar, makeVar } from "../reactiveVars";
 import { Reference, StoreObject, ApolloClient, NetworkStatus, TypedDocumentNode, DocumentNode } from "../../../core";
 import { MissingFieldError } from "../..";
-import { relayStylePagination } from "../../../utilities";
+import { relayStylePagination, stringifyForDisplay } from "../../../utilities";
 import { FieldPolicy, StorageType } from "../policies";
 import {
   itAsync,
@@ -443,8 +443,9 @@ describe("type policies", function () {
         },
       });
     }).toThrowError(
-      `Missing field 'year' while extracting keyFields from ${JSON.stringify(
-        theInformationBookData
+      `Missing field 'year' while extracting keyFields from ${stringifyForDisplay(
+        theInformationBookData,
+        2
       )}`,
     );
   });
@@ -627,6 +628,182 @@ describe("type policies", function () {
     })).toBe('DeathAdder:{"tagId":"LethalAbacus666"}');
   });
 
+  it("typePolicies can be inherited from supertypes with fuzzy possibleTypes", () => {
+    const cache = new InMemoryCache({
+      possibleTypes: {
+        EntitySupertype: [".*Entity"],
+      },
+      typePolicies: {
+        Query: {
+          fields: {
+            coworkers: {
+              merge(existing, incoming) {
+                return existing ? existing.concat(incoming) : incoming;
+              },
+            },
+          },
+        },
+
+        // The point of this test is to ensure keyFields: ["uid"] can be
+        // registered for all __typename strings matching the RegExp /.*Entity/,
+        // without manually enumerating all of them.
+        EntitySupertype: {
+          keyFields: ["uid"],
+        },
+      },
+    });
+
+    type Coworker = {
+      __typename: "CoworkerEntity" | "ManagerEntity";
+      uid: string;
+      name: string;
+    }
+
+    const query: TypedDocumentNode<{
+      coworkers: Coworker[];
+    }> = gql`
+      query {
+        coworkers {
+          uid
+          name
+        }
+      }
+    `;
+
+    cache.writeQuery({
+      query,
+      data: {
+        coworkers: [
+          { __typename: "CoworkerEntity", uid: "qwer", name: "Alessia" },
+          { __typename: "CoworkerEntity", uid: "asdf", name: "Jerel" },
+          { __typename: "CoworkerEntity", uid: "zxcv", name: "Lenz" },
+          { __typename: "ManagerEntity", uid: "uiop", name: "Jeff" },
+        ],
+      },
+    });
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        coworkers: [
+          { __ref: 'CoworkerEntity:{"uid":"qwer"}' },
+          { __ref: 'CoworkerEntity:{"uid":"asdf"}' },
+          { __ref: 'CoworkerEntity:{"uid":"zxcv"}' },
+          { __ref: 'ManagerEntity:{"uid":"uiop"}' },
+        ],
+      },
+      'CoworkerEntity:{"uid":"qwer"}': {
+        __typename: "CoworkerEntity",
+        uid: "qwer",
+        name: "Alessia",
+      },
+      'CoworkerEntity:{"uid":"asdf"}': {
+        __typename: "CoworkerEntity",
+        uid: "asdf",
+        name: "Jerel",
+      },
+      'CoworkerEntity:{"uid":"zxcv"}': {
+        __typename: "CoworkerEntity",
+        uid: "zxcv",
+        name: "Lenz",
+      },
+      'ManagerEntity:{"uid":"uiop"}': {
+        __typename: "ManagerEntity",
+        uid: "uiop",
+        name: "Jeff",
+      },
+    });
+
+    interface CoworkerWithAlias extends Omit<Coworker, "uid"> {
+      idAlias: string;
+    }
+
+    const queryWithAlias: TypedDocumentNode<{
+      coworkers: CoworkerWithAlias[];
+    }> = gql`
+      query {
+        coworkers {
+          idAlias: uid
+          name
+        }
+      }
+    `;
+
+    expect(cache.readQuery({ query: queryWithAlias })).toEqual({
+      coworkers: [
+        { __typename: "CoworkerEntity", idAlias: "qwer", name: "Alessia" },
+        { __typename: "CoworkerEntity", idAlias: "asdf", name: "Jerel" },
+        { __typename: "CoworkerEntity", idAlias: "zxcv", name: "Lenz" },
+        { __typename: "ManagerEntity", idAlias: "uiop", name: "Jeff" },
+      ],
+    });
+
+    cache.writeQuery({
+      query: queryWithAlias,
+      data: {
+        coworkers: [
+          { __typename: "CoworkerEntity", idAlias: "hjkl", name: "Martijn" },
+          { __typename: "ManagerEntity", idAlias: "vbnm", name: "Hugh" },
+        ],
+      },
+    });
+
+    expect(cache.readQuery({ query })).toEqual({
+      coworkers: [
+        { __typename: "CoworkerEntity", uid: "qwer", name: "Alessia" },
+        { __typename: "CoworkerEntity", uid: "asdf", name: "Jerel" },
+        { __typename: "CoworkerEntity", uid: "zxcv", name: "Lenz" },
+        { __typename: "ManagerEntity", uid: "uiop", name: "Jeff" },
+        { __typename: "CoworkerEntity", uid: "hjkl", name: "Martijn" },
+        { __typename: "ManagerEntity", uid: "vbnm", name: "Hugh" },
+      ],
+    });
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        coworkers: [
+          { __ref: 'CoworkerEntity:{"uid":"qwer"}' },
+          { __ref: 'CoworkerEntity:{"uid":"asdf"}' },
+          { __ref: 'CoworkerEntity:{"uid":"zxcv"}' },
+          { __ref: 'ManagerEntity:{"uid":"uiop"}' },
+          { __ref: 'CoworkerEntity:{"uid":"hjkl"}' },
+          { __ref: 'ManagerEntity:{"uid":"vbnm"}' },
+        ],
+      },
+      'CoworkerEntity:{"uid":"qwer"}': {
+        __typename: "CoworkerEntity",
+        uid: "qwer",
+        name: "Alessia",
+      },
+      'CoworkerEntity:{"uid":"asdf"}': {
+        __typename: "CoworkerEntity",
+        uid: "asdf",
+        name: "Jerel",
+      },
+      'CoworkerEntity:{"uid":"zxcv"}': {
+        __typename: "CoworkerEntity",
+        uid: "zxcv",
+        name: "Lenz",
+      },
+      'ManagerEntity:{"uid":"uiop"}': {
+        __typename: "ManagerEntity",
+        uid: "uiop",
+        name: "Jeff",
+      },
+      'CoworkerEntity:{"uid":"hjkl"}': {
+        __typename: "CoworkerEntity",
+        uid: "hjkl",
+        name: "Martijn",
+      },
+      'ManagerEntity:{"uid":"vbnm"}': {
+        __typename: "ManagerEntity",
+        uid: "vbnm",
+        name: "Hugh",
+      },
+    });
+  });
+
   describe("field policies", function () {
     it(`can filter arguments using keyArgs`, function () {
       const cache = new InMemoryCache({
diff --git a/src/cache/inmemory/__tests__/readFromStore.ts b/src/cache/inmemory/__tests__/readFromStore.ts
--- a/src/cache/inmemory/__tests__/readFromStore.ts
+++ b/src/cache/inmemory/__tests__/readFromStore.ts
@@ -18,37 +18,22 @@ import {
   TypedDocumentNode,
 } from '../../../core';
 
-jest.mock('optimism');
-import { wrap } from 'optimism';
-
 describe('resultCacheMaxSize', () => {
   const cache = new InMemoryCache();
-  let wrapSpy: jest.Mock = wrap as jest.Mock;
-  beforeEach(() => {
-    wrapSpy.mockClear();
-  });
-
-  it("does not set max size on caches if resultCacheMaxSize is not configured", () => {
-    new StoreReader({ cache });
-    expect(wrapSpy).toHaveBeenCalled();
+  const defaultMaxSize = Math.pow(2, 16);
 
-    wrapSpy.mock.calls.forEach(([, { max }]) => {
-      expect(max).toBeUndefined();
-    })
+  it("uses default max size on caches if resultCacheMaxSize is not configured", () => {
+    const reader = new StoreReader({ cache });
+    expect(reader["executeSelectionSet"].options.max).toBe(defaultMaxSize);
   });
 
   it("configures max size on caches when resultCacheMaxSize is set", () => {
     const resultCacheMaxSize = 12345;
-    new StoreReader({ cache, resultCacheMaxSize });
-    expect(wrapSpy).toHaveBeenCalled();
-
-    wrapSpy.mock.calls.forEach(([, { max }]) => {
-      expect(max).toBe(resultCacheMaxSize);
-    })
+    const reader = new StoreReader({ cache, resultCacheMaxSize });
+    expect(reader["executeSelectionSet"].options.max).toBe(resultCacheMaxSize);
   });
 });
 
-
 describe('reading from the store', () => {
   const reader = new StoreReader({
     cache: new InMemoryCache(),
diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts
--- a/src/core/__tests__/ObservableQuery.ts
+++ b/src/core/__tests__/ObservableQuery.ts
@@ -2,11 +2,16 @@ import gql from "graphql-tag";
 import { GraphQLError } from "graphql";
 import { TypedDocumentNode } from "@graphql-typed-document-node/core";
 
-import { ApolloClient, ApolloQueryResult, NetworkStatus, WatchQueryFetchPolicy } from "../../core";
+import { 
+  ApolloClient,
+  ApolloQueryResult,
+  NetworkStatus,
+  WatchQueryFetchPolicy
+} from "../../core";
 import { ObservableQuery } from "../ObservableQuery";
 import { QueryManager } from "../QueryManager";
 
-import { Observable } from "../../utilities";
+import { DocumentTransform, Observable, removeDirectivesFromDocument } from "../../utilities";
 import { ApolloLink, FetchResult } from "../../link/core";
 import { InMemoryCache, NormalizedCacheObject } from "../../cache";
 import { ApolloError } from "../../errors";
@@ -21,18 +26,16 @@ import { SubscriptionObserver } from "zen-observable-ts";
 import { waitFor } from "@testing-library/react";
 
 export const mockFetchQuery = (queryManager: QueryManager<any>) => {
-  const fetchQueryObservable = queryManager.fetchQueryObservable;
   const fetchConcastWithInfo = queryManager['fetchConcastWithInfo'];
   const fetchQueryByPolicy: QueryManager<any>["fetchQueryByPolicy"] = (queryManager as any)
     .fetchQueryByPolicy;
 
-  const mock = <T extends typeof fetchQueryObservable | typeof fetchConcastWithInfo | typeof fetchQueryByPolicy>(original: T) =>
+  const mock = <T extends typeof fetchConcastWithInfo | typeof fetchQueryByPolicy>(original: T) =>
     jest.fn<ReturnType<T>, Parameters<T>>(function () {
       return original.apply(queryManager, arguments);
     });
 
   const mocks = {
-    fetchQueryObservable: mock(fetchQueryObservable),
     fetchConcastWithInfo: mock(fetchConcastWithInfo),
     fetchQueryByPolicy: mock(fetchQueryByPolicy),
   };
@@ -1471,9 +1474,11 @@ describe("ObservableQuery", () => {
               expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
               expect(consoleWarnSpy).toHaveBeenCalledWith(
                 [
-                  'Called refetch({"variables":["d","e"]}) for query QueryWithoutVariables, which does not declare a $variables variable.',
+                  'Called refetch(%o) for query %o, which does not declare a $variables variable.',
                   "Did you mean to call refetch(variables) instead of refetch({ variables })?",
-                ].join("\n")
+                ].join("\n"),
+                {"variables": ["d", "e"]}, 
+                "QueryWithoutVariables"
               );
               consoleWarnSpy.mockRestore();
 
@@ -1578,9 +1583,11 @@ describe("ObservableQuery", () => {
                       expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
                       expect(consoleWarnSpy).toHaveBeenCalledWith(
                         [
-                          'Called refetch({"variables":{"vars":["d","e"]}}) for query QueryWithVarsVar, which does not declare a $variables variable.',
+                          'Called refetch(%o) for query %o, which does not declare a $variables variable.',
                           "Did you mean to call refetch(variables) instead of refetch({ variables })?",
-                        ].join("\n")
+                        ].join("\n"),
+                        {"variables":{"vars":["d","e"]}},
+                        "QueryWithVarsVar"
                       );
                       consoleWarnSpy.mockRestore();
 
@@ -2685,6 +2692,132 @@ describe("ObservableQuery", () => {
     });
   });
 
+  describe('.query computed property', () => {
+    it('is equal to transformed query when instantiating via `watchQuery`', () => {
+      const query = gql`
+        query {
+          currentUser {
+            id
+          }
+        }
+      `;
+
+      const client = new ApolloClient({
+        link: ApolloLink.empty(),
+        cache: new InMemoryCache(),
+      });
+
+      const observable = client.watchQuery({ query });
+
+      expect(observable.query).toMatchDocument(gql`
+        query {
+          currentUser {
+            id
+            __typename
+          }
+        }
+      `);
+    });
+
+    it('is referentially stable', () => {
+      const query = gql`
+        query {
+          currentUser {
+            id
+          }
+        }
+      `;
+
+      const client = new ApolloClient({
+        link: ApolloLink.empty(),
+        cache: new InMemoryCache(),
+      });
+
+      const observable = client.watchQuery({ query });
+      const result = observable.query;
+
+      expect(observable.query).toBe(result);
+    });
+
+    it('is updated with transformed query when `setOptions` changes the query', () => {
+      const query = gql`
+        query {
+          currentUser {
+            id
+          }
+        }
+      `;
+
+      const updatedQuery = gql`
+        query {
+          product {
+            id
+          }
+        }
+      `
+
+      const client = new ApolloClient({
+        link: ApolloLink.empty(),
+        cache: new InMemoryCache(),
+      });
+
+      const observable = client.watchQuery({ query });
+
+      expect(observable.query).toMatchDocument(gql`
+        query {
+          currentUser {
+            id
+            __typename
+          }
+        }
+      `);
+
+      observable.setOptions({ query: updatedQuery });
+
+      expect(observable.query).toMatchDocument(gql`
+        query {
+          product {
+            id
+            __typename
+          }
+        }
+      `);
+    });
+
+    it('reflects query run through custom transforms', () => {
+      const query = gql`
+        query {
+          currentUser {
+            id
+            name @client
+          }
+        }
+      `;
+
+      const documentTransform = new DocumentTransform((document) => {
+        return removeDirectivesFromDocument([{ name: 'client' }], document)!
+      });
+
+      const client = new ApolloClient({
+        link: ApolloLink.empty(),
+        cache: new InMemoryCache(),
+        documentTransform,
+      });
+
+      const observable = client.watchQuery({ query });
+
+      expect(observable.query).toMatchDocument(gql`
+        query {
+          currentUser {
+            id
+            name
+            __typename
+          }
+        }
+      `);
+    });
+  });
+
   itAsync("QueryInfo does not notify for !== but deep-equal results", (resolve, reject) => {
     const queryManager = mockQueryManager({
       request: { query, variables },
diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts
--- a/src/core/__tests__/QueryManager/index.ts
+++ b/src/core/__tests__/QueryManager/index.ts
@@ -4847,7 +4847,7 @@ describe('QueryManager', () => {
         result => {
           expect(result.data).toEqual(secondReqData);
           expect(consoleWarnSpy).toHaveBeenLastCalledWith(
-            'Unknown query named "fakeQuery" requested in refetchQueries options.include array'
+            'Unknown query named "%s" requested in refetchQueries options.include array', "fakeQuery"
           );
         },
       ).then(resolve, reject);
@@ -4914,7 +4914,7 @@ describe('QueryManager', () => {
         });
       }).then(() => {
         expect(consoleWarnSpy).toHaveBeenLastCalledWith(
-          'Unknown query named "getAuthors" requested in refetchQueries options.include array'
+          'Unknown query named "%s" requested in refetchQueries options.include array', "getAuthors"
         );
       }).then(resolve, reject);
     });
diff --git a/src/core/__tests__/QueryManager/links.ts b/src/core/__tests__/QueryManager/links.ts
--- a/src/core/__tests__/QueryManager/links.ts
+++ b/src/core/__tests__/QueryManager/links.ts
@@ -1,5 +1,6 @@
 // externals
 import gql from 'graphql-tag';
+import { print } from 'graphql'
 
 import { Observable, ObservableSubscription } from '../../../utilities/observables/Observable';
 import { ApolloLink } from '../../../link/core';
@@ -360,4 +361,51 @@ describe('Link interactions', () => {
         });
       });
   });
+
+  it('removes @client fields from the query before it reaches the link', async () => {
+    const result: { current: Operation | undefined } = {
+      current: undefined
+    };
+
+    const query = gql`
+      query {
+        books {
+          id
+          title
+          isRead @client
+        }
+      }
+    `;
+
+    const expectedQuery = gql`
+      query {
+        books {
+          id
+          title
+        }
+      }
+    `;
+
+    const link = new ApolloLink((operation) => {
+      result.current = operation;
+
+      return Observable.of({
+        data: {
+          books: [
+            { id: 1, title: 'Woo', __typename: 'Book' },
+            { id: 2, title: 'Foo', __typename: 'Book' },
+          ],
+        }
+      });
+    });
+
+    const queryManager = new QueryManager({
+      link,
+      cache: new InMemoryCache({ addTypename: false }),
+    });
+
+    await queryManager.query({ query });
+
+    expect(print(result.current!.query)).toEqual(print(expectedQuery))
+  });
 });
diff --git a/src/core/__tests__/equalByQuery.ts b/src/core/__tests__/equalByQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/core/__tests__/equalByQuery.ts
@@ -0,0 +1,722 @@
+import { GraphQLError } from "graphql";
+import { TypedDocumentNode, gql } from "../index";
+import { equalByQuery } from "../equalByQuery";
+
+describe("equalByQuery", () => {
+  it("is importable and a function", () => {
+    expect(typeof equalByQuery).toBe("function");
+  });
+
+  it("works with a basic single-field query", () => {
+    const query = gql`
+      query {
+        hello
+      }
+    `;
+
+    expect(
+      equalByQuery(query, { data: { hello: "hi" } }, { data: { hello: "hi" } })
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { hello: "hi", unrelated: 1 } },
+        { data: { hello: "hi", unrelated: 100 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(query, { data: { hello: "hi" } }, { data: { hello: "hey" } })
+    ).toBe(false);
+
+    expect(equalByQuery(query, { data: {} }, { data: { hello: "hi" } })).toBe(
+      false
+    );
+
+    expect(equalByQuery(query, { data: { hello: "hi" } }, { data: {} })).toBe(
+      false
+    );
+
+    expect(equalByQuery(query, { data: { hello: "hi" } }, { data: null })).toBe(
+      false
+    );
+
+    expect(equalByQuery(query, { data: null }, { data: { hello: "hi" } })).toBe(
+      false
+    );
+
+    expect(equalByQuery(query, { data: null }, { data: null })).toBe(true);
+
+    expect(equalByQuery(query, { data: {} }, { data: {} })).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { unrelated: "whatever" } },
+        { data: { unrelated: "no matter" } }
+      )
+    ).toBe(true);
+  });
+
+  it("is not confused by properties in different orders", () => {
+    const query = gql`
+      query {
+        a
+        b
+        c
+      }
+    `;
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { b: 2, c: 3, a: 1 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { d: "bogus", a: 1, b: 2, c: 3 } },
+        { data: { b: 2, c: 3, a: 1, d: "also bogus" } }
+      )
+    ).toBe(true);
+  });
+
+  it("respects the @nonreactive directive on fields", () => {
+    const query = gql`
+      query {
+        a
+        b
+        c @nonreactive
+      }
+    `;
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 2, c: "different" } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: "different", b: 2, c: 4 } }
+      )
+    ).toBe(false);
+  });
+
+  describe("@skip and @include directives", () => {
+    // The @skip and @include directives use query variables to determine
+    // whether subtrees of the query should be executed at all, so they can
+    // influence the comparison of results in ways similar to @nonreactive. The
+    // key difference is that @skip and @include will be sent to the server,
+    // whereas @nonreactive is a client-only directive, and does not prevent
+    // execution of nonreactive fields/subtrees on the server.
+    it("respects @skip directive, depending on variables", () => {
+      const skipQuery = gql`
+        query SkipC($condition: Boolean!) {
+          a
+          b
+          c @skip(if: $condition)
+        }
+      `;
+
+      expect(
+        equalByQuery(
+          skipQuery,
+          { data: { a: 1, b: 2, c: 3 } },
+          { data: { a: 1, b: 2, c: 3 } },
+          { condition: false }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          skipQuery,
+          { data: { a: 1, b: 2, c: 3 } },
+          { data: { a: 1, b: 2 } },
+          { condition: false }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          skipQuery,
+          { data: { a: 1, b: 2, c: 3 } },
+          { data: { a: 1, b: 2 } },
+          { condition: true }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          skipQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2, c: 3 } },
+          { condition: false }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          skipQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2, c: 3 } },
+          { condition: true }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          skipQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2 } },
+          { condition: false }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          skipQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2 } },
+          { condition: true }
+        )
+      ).toBe(true);
+    });
+
+    it("respects @include directive, depending on variables", () => {
+      const includeQuery = gql`
+        query IncludeC($condition: Boolean!) {
+          a
+          b
+          c @include(if: $condition)
+        }
+      `;
+
+      expect(
+        equalByQuery(
+          includeQuery,
+          { data: { a: 1, b: 2, c: 3 } },
+          { data: { a: 1, b: 2, c: 3 } },
+          { condition: true }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          includeQuery,
+          { data: { a: 1, b: 2, c: 3 } },
+          { data: { a: 1, b: 2 } },
+          { condition: true }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          includeQuery,
+          { data: { a: 1, b: 2, c: 3 } },
+          { data: { a: 1, b: 2 } },
+          { condition: false }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          includeQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2, c: 3 } },
+          { condition: true }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          includeQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2, c: 3 } },
+          { condition: false }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          includeQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2 } },
+          { condition: true }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          includeQuery,
+          { data: { a: 1, b: 2 } },
+          { data: { a: 1, b: 2 } },
+          { condition: false }
+        )
+      ).toBe(true);
+    });
+  });
+
+  it("considers errors as well as data", () => {
+    const query = gql`
+      query {
+        a
+        b @nonreactive
+        c
+      }
+    `;
+
+    const data123 = { a: 1, b: 2, c: 3 };
+    const oopsError = new GraphQLError("oops");
+    const differentError = new GraphQLError("different");
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123 },
+        { data: data123, errors: [oopsError] }
+      )
+    ).toBe(false);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123, errors: [oopsError] },
+        { data: data123 }
+      )
+    ).toBe(false);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123, errors: [oopsError] },
+        { data: data123, errors: [oopsError] }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123, errors: [oopsError] },
+        { data: data123, errors: [differentError] }
+      )
+    ).toBe(false);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123, errors: [oopsError] },
+        { data: data123, errors: [oopsError] }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123, errors: [oopsError] },
+        { data: { ...data123, b: 100 }, errors: [oopsError] }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123, errors: [] },
+        { data: data123, errors: [] }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: data123, errors: [] },
+        { data: { ...data123, b: 100 }, errors: [] }
+      )
+    ).toBe(true);
+  });
+
+  it("respects the @nonreactive directive on inline fragments", () => {
+    const query = gql`
+      query {
+        a
+        ... @nonreactive {
+          b
+          c
+        }
+      }
+    `;
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 20, c: 30 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 10, b: 20, c: 30 } }
+      )
+    ).toBe(false);
+  });
+
+  it("respects the @nonreactive directive on named fragment ...spreads", () => {
+    const query = gql`
+      query {
+        a
+        ...BCFragment @nonreactive
+      }
+
+      fragment BCFragment on Query {
+        b
+        c
+      }
+    `;
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 2, c: 30 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 20, c: 3 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 20, c: 30 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 10, b: 20, c: 30 } }
+      )
+    ).toBe(false);
+  });
+
+  it("respects the @nonreactive directive on named fragment definitions", () => {
+    const query = gql`
+      query {
+        a
+        ...BCFragment
+      }
+
+      fragment BCFragment on Query @nonreactive {
+        b
+        c
+      }
+    `;
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 2, c: 30 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 20, c: 3 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 20, c: 30 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 10, b: 20, c: 30 } }
+      )
+    ).toBe(false);
+  });
+
+  it("traverses fragments without @nonreactive", () => {
+    const query = gql`
+      query {
+        a
+        ...BCFragment
+      }
+
+      fragment BCFragment on Query {
+        b
+        c
+      }
+    `;
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 2, c: 3 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { c: 3, a: 1, b: 2 } }
+      )
+    ).toBe(true);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 2, c: 30 } }
+      )
+    ).toBe(false);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 20, c: 3 } }
+      )
+    ).toBe(false);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 1, b: 20, c: 30 } }
+      )
+    ).toBe(false);
+
+    expect(
+      equalByQuery(
+        query,
+        { data: { a: 1, b: 2, c: 3 } },
+        { data: { a: 10, b: 20, c: 30 } }
+      )
+    ).toBe(false);
+  });
+
+  type Thing = {
+    __typename: "Thing";
+    id: string;
+    stable: number;
+    volatile: number;
+  };
+
+  it.each<TypedDocumentNode<Thing>>([
+    gql`
+      query {
+        things {
+          __typename
+          id
+          stable
+          volatile @nonreactive
+        }
+      }
+    `,
+    gql`
+      query {
+        things {
+          __typename
+          id
+          ...ThingDetails
+        }
+      }
+
+      fragment ThingDetails on Thing {
+        stable
+        volatile @nonreactive
+      }
+    `,
+    gql`
+      query {
+        things {
+          __typename
+          id
+          ... on Thing {
+            stable
+            volatile @nonreactive
+          }
+        }
+      }
+    `,
+    gql`
+      query {
+        things {
+          __typename
+          id
+          stable
+          ... on Thing @nonreactive {
+            volatile
+          }
+        }
+      }
+    `,
+    gql`
+      query {
+        things {
+          __typename
+          id
+          stable
+          ...Volatile @nonreactive
+        }
+      }
+
+      fragment Volatile on Thing {
+        volatile
+      }
+    `,
+    gql`
+      query {
+        things {
+          __typename
+          id
+          stable
+          ... @nonreactive {
+            volatile
+          }
+        }
+      }
+    `,
+  ])(
+    "iterates over array-valued result fields ignoring @nonreactive (%#)",
+    (query) => {
+      let nextVolatileIntegerPart = 0;
+      const makeThing = (id: string, stable = 1): Thing => ({
+        __typename: "Thing",
+        id,
+        stable,
+        // Thing.volatile is always a different randomized number, which normally
+        // would threatens any deep comparison of Thing objects. These test cases
+        // demonstrate (among other things) that we can make the result comparison
+        // insensitive to this volatility by marking the volatile field with the
+        // @nonreactive directive.
+        volatile: nextVolatileIntegerPart++ + Math.random(),
+      });
+
+      const makeThings = (
+        lettersToSplit: string,
+        stable: number = 1
+      ): Thing[] => lettersToSplit.split("").map((id) => makeThing(id, stable));
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("abc") } },
+          { data: { things: [makeThing("a"), makeThing("b"), makeThing("c")] } }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("abcdefg", 2) } },
+          { data: { things: makeThings("abcdefg") } }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("abcdefg", 2) } },
+          { data: { things: makeThings("abcdefg", 3) } }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("abcdefg", 3) } },
+          { data: { things: makeThings("abcdefg", 3) } }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("ab", 2345) } },
+          { data: { things: [makeThing("a"), makeThing("b", 2345)] } }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("ab", 3456) } },
+          { data: { things: [makeThing("a", 3456), makeThing("b")] } }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("ab", 3456) } },
+          { data: { things: [makeThing("a", 3456), makeThing("b", 3456)] } }
+        )
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          query,
+          { data: { things: makeThings("abc") } },
+          { data: { things: "not an array" } }
+        )
+      ).toBe(false);
+
+      expect(
+        equalByQuery(query, { data: { things: {} } }, { data: { things: [] } })
+      ).toBe(false);
+
+      expect(
+        equalByQuery(query, { data: { things: [] } }, { data: { things: {} } })
+      ).toBe(false);
+
+      expect(
+        equalByQuery(query, { data: { things: [] } }, { data: { things: [] } })
+      ).toBe(true);
+
+      expect(
+        equalByQuery(
+          query,
+          // There's nothing inherently array-like about the Query.things field as
+          // it's represented in query syntax, since `query { things { id } }` could
+          // (depending on the server/schema) return a single object for the things
+          // field, rather than an array. Although this might seem like a strange
+          // edge case to test, it demonstrates the equalByQuery function can handle
+          // any combination of array/non-array values.
+          { data: { things: {} } },
+          { data: { things: {} } }
+        )
+      ).toBe(true);
+    }
+  );
+});
diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts
--- a/src/link/batch-http/__tests__/batchHttpLink.ts
+++ b/src/link/batch-http/__tests__/batchHttpLink.ts
@@ -4,9 +4,10 @@ import { ASTNode, print, stripIgnoredCharacters } from 'graphql';
 
 import { ApolloLink } from '../../core/ApolloLink';
 import { execute } from '../../core/execute';
-import { Observable } from '../../../utilities/observables/Observable';
+import { Observable, Observer } from '../../../utilities/observables/Observable';
 import { BatchHttpLink } from '../batchHttpLink';
 import { itAsync } from '../../../testing';
+import { FetchResult } from '../../core';
 
 const sampleQuery = gql`
   query SampleQuery {
@@ -298,7 +299,7 @@ describe('SharedHttpTest', () => {
   it('raises warning if called with concat', () => {
     const link = createHttpLink();
     const _warn = console.warn;
-    console.warn = (warning: any) => expect(warning['message']).toBeDefined();
+    console.warn = (...args: any) => expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", link]);
     expect(link.concat((operation, forward) => forward(operation))).toEqual(
       link,
     );
@@ -352,6 +353,62 @@ describe('SharedHttpTest', () => {
     );
   });
 
+  itAsync('strips unused variables, respecting nested fragments', (resolve, reject) => {
+    const link = createHttpLink({ uri: '/data' });
+
+    const query = gql`
+      query PEOPLE (
+        $declaredAndUsed: String,
+        $declaredButUnused: Int,
+      ) {
+        people(
+          surprise: $undeclared,
+          noSurprise: $declaredAndUsed,
+        ) {
+          ... on Doctor {
+            specialty(var: $usedByInlineFragment)
+          }
+          ...LawyerFragment
+        }
+      }
+      fragment LawyerFragment on Lawyer {
+        caseCount(var: $usedByNamedFragment)
+      }
+    `;
+
+    const variables = {
+      unused: 'strip',
+      declaredButUnused: 'strip',
+      declaredAndUsed: 'keep',
+      undeclared: 'keep',
+      usedByInlineFragment: 'keep',
+      usedByNamedFragment: 'keep',
+    };
+
+    execute(link, {
+      query,
+      variables,
+    }).subscribe({
+      next: makeCallback(resolve, reject, () => {
+        const [uri, options] = fetchMock.lastCall()!;
+        const { method, body } = options!;
+        expect(JSON.parse(body as string)).toEqual([{
+          operationName: "PEOPLE",
+          query: print(query),
+          variables: {
+            declaredAndUsed: 'keep',
+            undeclared: 'keep',
+            usedByInlineFragment: 'keep',
+            usedByNamedFragment: 'keep',
+          },
+        }]);
+        expect(method).toBe('POST');
+        expect(uri).toBe('/data');
+      }),
+      error: error => reject(error),
+    });
+  });
+
   itAsync('unsubscribes without calling subscriber', (resolve, reject) => {
     const link = createHttpLink({ uri: '/data' });
     const observable = execute(link, {
@@ -371,6 +428,7 @@ describe('SharedHttpTest', () => {
     link: ApolloLink,
     after: () => void,
     includeExtensions: boolean,
+    includeUnusedVariables: boolean,
     reject: (e: Error) => void,
   ) => {
     const next = jest.fn();
@@ -389,7 +447,7 @@ describe('SharedHttpTest', () => {
         try {
           let body = convertBatchedBody(fetchMock.lastCall()![1]!.body);
           expect(body.query).toBe(print(sampleMutation));
-          expect(body.variables).toEqual(variables);
+          expect(body.variables).toEqual(includeUnusedVariables ? variables : {});
           expect(body.context).not.toBeDefined();
           if (includeExtensions) {
             expect(body.extensions).toBeDefined();
@@ -410,8 +468,9 @@ describe('SharedHttpTest', () => {
     const link = createHttpLink({ uri: '/data', includeExtensions: true });
     verifyRequest(
       link,
-      () => verifyRequest(link, resolve, true, reject),
+      () => verifyRequest(link, resolve, true, false, reject),
       true,
+      false,
       reject,
     );
   });
@@ -420,7 +479,8 @@ describe('SharedHttpTest', () => {
     const link = createHttpLink({ uri: '/data' });
     verifyRequest(
       link,
-      () => verifyRequest(link, resolve, false, reject),
+      () => verifyRequest(link, resolve, false, false, reject),
+      false,
       false,
       reject,
     );
@@ -582,7 +642,7 @@ describe('SharedHttpTest', () => {
     const variables = { params: 'stub' };
     const link = createHttpLink({
       uri: '/data',
-      headers: { 
+      headers: {
         authorization: '1234',
         AUTHORIZATION: '1234',
         'CONTENT-TYPE': 'application/json',
@@ -898,4 +958,186 @@ describe('SharedHttpTest', () => {
       () => {},
     );
   });
+
+  it('removes @client fields from the query before sending it to the server', async () => {
+    fetchMock.mock('https://example.com/graphql', {
+      status: 200,
+      body: JSON.stringify({
+        data: {
+          author: { __typename: 'Author', name: 'Test User' }
+        }
+      }),
+      headers: { 'content-type': 'application/json' }
+    });
+
+    const query = gql`
+      query {
+        author {
+          name
+          isInCollection @client
+        }
+      }
+    `;
+
+    const serverQuery = gql`
+      query {
+        author {
+          name
+        }
+      }
+    `;
+
+    const link = createHttpLink({ uri: 'https://example.com/graphql' });
+
+    await new Promise((resolve, reject) => {
+      execute(link, { query }).subscribe({
+        next: resolve,
+        error: reject
+      });
+    });
+
+    const [, options] = fetchMock.lastCall()!;
+    const { body } = options!
+
+    expect(JSON.parse(body!.toString())).toEqual([
+      {
+        query: print(serverQuery),
+        variables: {}
+      }
+    ]);
+  });
+
+  it('responds with error when trying to send a client-only query', async () => {
+    const errorHandler = jest.fn()
+    const query = gql`
+      query {
+        author @client {
+          name
+        }
+      }
+    `;
+
+    const link = createHttpLink({ uri: 'https://example.com/graphql' });
+
+    await new Promise<void>((resolve, reject) => {
+      execute(link, { query }).subscribe({
+        next: reject,
+        error: errorHandler.mockImplementation(resolve)
+      });
+    });
+
+    expect(errorHandler).toHaveBeenCalledWith(
+      new Error('BatchHttpLink: Trying to send a client-only query to the server. To send to the server, ensure a non-client field is added to the query or enable the `transformOptions.removeClientFields` option.')
+    );
+  });
+
+  describe('AbortController', () => {
+    const originalAbortController = globalThis.AbortController;
+    afterEach(() => {
+      globalThis.AbortController = originalAbortController;
+    });
+
+    function trackGlobalAbortControllers() {
+      const instances: AbortController[] = []
+      class AbortControllerMock {
+        constructor() {
+          const instance = new originalAbortController()
+          instances.push(instance)
+          return instance
+        }
+      }
+
+      globalThis.AbortController = AbortControllerMock as any;
+      return instances;
+    }
+
+    const failingObserver: Observer<FetchResult> = {
+      next: () => {
+        fail('result should not have been called');
+      },
+      error: e => {
+        fail(e);
+      },
+      complete: () => {
+        fail('complete should not have been called');
+      },
+    }
+
+    function mockFetch() {
+      const text = jest.fn(async () => '{ "data": { "stub": { "id": "foo" } } }');
+      const fetch = jest.fn(async (uri, options) => ({ text }));
+      return { text, fetch }
+    }
+
+    it("aborts the request when unsubscribing before the request has completed", () => {
+      const { fetch } = mockFetch();
+      const abortControllers = trackGlobalAbortControllers();
+
+      const link = createHttpLink({ uri: 'data', fetch: fetch as any });
+
+      const sub = execute(link, { query: sampleQuery }).subscribe(failingObserver);
+      sub.unsubscribe();
+
+      expect(abortControllers.length).toBe(1);
+      expect(abortControllers[0].signal.aborted).toBe(true);
+    });
+
+    it('a passed-in signal will be forwarded to the `fetch` call and not be overwritten by an internally-created one', () => {
+      const { fetch } = mockFetch();
+      const externalAbortController = new AbortController();
+
+      const link = createHttpLink({ uri: 'data', fetch: fetch as any, fetchOptions: { signal: externalAbortController.signal } });
+
+      const sub = execute(link, { query: sampleQuery } ).subscribe(failingObserver);
+      sub.unsubscribe();
+
+      expect(fetch.mock.calls.length).toBe(1);
+      expect(fetch.mock.calls[0][1]).toEqual(expect.objectContaining({ signal: externalAbortController.signal }))
+    });
+
+    it('aborting the internal signal will not cause an error', async () => {
+      try { 
+        fetchMock.restore();
+        fetchMock.postOnce('data', async () => '{ "data": { "stub": { "id": "foo" } } }');
+        const abortControllers = trackGlobalAbortControllers();
+
+        const link = createHttpLink({ uri: '/data' });
+        execute(link, { query: sampleQuery } ).subscribe(failingObserver);
+        abortControllers[0].abort();
+      } finally {
+        fetchMock.restore();
+      }
+    });
+
+    it('resolving fetch does not cause the AbortController to be aborted', async () => {
+      const { text, fetch } = mockFetch();
+      const abortControllers = trackGlobalAbortControllers();
+      text.mockResolvedValueOnce('{ "data": { "hello": "world" } }');
+
+      // (the request is already finished at that point)
+      const link = createHttpLink({ uri: 'data', fetch: fetch as any });
+
+      await new Promise<void>(resolve => execute(link, { query: sampleQuery }).subscribe({
+        complete: resolve
+      }));
+
+      expect(abortControllers.length).toBe(1);
+      expect(abortControllers[0].signal.aborted).toBe(false);
+    });
+
+    it('an unsuccessful fetch does not cause the AbortController to be aborted', async () => {
+      const { fetch } = mockFetch();
+      const abortControllers = trackGlobalAbortControllers();
+      fetch.mockRejectedValueOnce("This is an error!")
+      // the request would be closed by the browser in the case of an error anyways
+      const link = createHttpLink({ uri: 'data', fetch: fetch as any });
+
+      await new Promise<void>(resolve => execute(link, { query: sampleQuery }).subscribe({
+        error: resolve
+      }));
+
+      expect(abortControllers.length).toBe(1);
+      expect(abortControllers[0].signal.aborted).toBe(false);
+    });
+  });
 });
diff --git a/src/link/batch/__tests__/batchLink.ts b/src/link/batch/__tests__/batchLink.ts
--- a/src/link/batch/__tests__/batchLink.ts
+++ b/src/link/batch/__tests__/batchLink.ts
@@ -793,9 +793,9 @@ describe('BatchLink', () => {
     });
     const link_no_op = new BatchLink({ batchHandler: () => Observable.of() });
     const _warn = console.warn;
-    console.warn = (warning: any) => {
+    console.warn = (...args: any) => {
       calls++;
-      expect(warning.message).toBeDefined();
+      expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", expect.any(BatchLink)]);
     };
     expect(
       link_one_op.concat((operation, forward) => forward(operation)),
diff --git a/src/link/core/__tests__/ApolloLink.ts b/src/link/core/__tests__/ApolloLink.ts
--- a/src/link/core/__tests__/ApolloLink.ts
+++ b/src/link/core/__tests__/ApolloLink.ts
@@ -921,9 +921,9 @@ describe('ApolloClient', () => {
 
   describe('Terminating links', () => {
     const _warn = console.warn;
-    const warningStub = jest.fn(warning => {
-      expect(warning.message).toBe(
-        `You are calling concat on a terminating link, which will have no effect`,
+    const warningStub = jest.fn((warning, link) => {
+      expect(warning).toBe(
+        `You are calling concat on a terminating link, which will have no effect %o`,
       );
     });
     const data = {
@@ -1017,7 +1017,7 @@ describe('ApolloClient', () => {
           link,
         );
         expect(warningStub).toHaveBeenCalledTimes(1);
-        expect(warningStub.mock.calls[0][0].link).toEqual(link);
+        expect(warningStub.mock.calls[0][1]).toEqual(link);
       });
 
       it('should warn if attempting to concat to a terminating Link', () => {
@@ -1026,7 +1026,7 @@ describe('ApolloClient', () => {
           link,
         );
         expect(warningStub).toHaveBeenCalledTimes(1);
-        expect(warningStub.mock.calls[0][0].link).toEqual(link);
+        expect(warningStub.mock.calls[0][1]).toEqual(link);
       });
 
       it('should not warn if attempting concat a terminating Link at end', () => {
diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts
--- a/src/link/http/__tests__/HttpLink.ts
+++ b/src/link/http/__tests__/HttpLink.ts
@@ -13,7 +13,7 @@ import { HttpLink } from '../HttpLink';
 import { createHttpLink } from '../createHttpLink';
 import { ClientParseError } from '../serializeFetchParameter';
 import { ServerParseError } from '../parseAndCheckHttpResponse';
-import { ServerError } from '../../..';
+import { FetchResult, ServerError } from '../../..';
 import { voidFetchDuringEachTest } from './helpers';
 import { itAsync } from '../../../testing';
 
@@ -454,7 +454,7 @@ describe('HttpLink', () => {
     it('raises warning if called with concat', () => {
       const link = createHttpLink();
       const _warn = console.warn;
-      console.warn = (warning: any) => expect(warning['message']).toBeDefined();
+      console.warn = (...args: any) => expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", link]);
       expect(link.concat((operation, forward) => forward(operation))).toEqual(
         link,
       );
@@ -1041,6 +1041,76 @@ describe('HttpLink', () => {
         () => {},
       );
     });
+
+    it('removes @client fields from the query before sending it to the server', async () => {
+      fetchMock.mock('https://example.com/graphql', {
+        status: 200,
+        body: JSON.stringify({
+          data: {
+            author: { __typename: 'Author', name: 'Test User' }
+          }
+        }),
+        headers: { 'content-type': 'application/json' }
+      });
+
+      const query = gql`
+        query {
+          author {
+            name
+            isInCollection @client
+          }
+        }
+      `;
+
+      const serverQuery = gql`
+        query {
+          author {
+            name
+          }
+        }
+      `;
+
+      const link = createHttpLink({ uri: 'https://example.com/graphql' });
+
+      await new Promise((resolve, reject) => {
+        execute(link, { query }).subscribe({
+          next: resolve,
+          error: reject
+        });
+      });
+
+      const [, options] = fetchMock.lastCall()!;
+      const { body } = options!
+
+      expect(JSON.parse(body!.toString())).toEqual({
+        query: print(serverQuery),
+        variables: {}
+      });
+    });
+
+    it('responds with error when trying to send a client-only query', async () => {
+      const errorHandler = jest.fn()
+      const query = gql`
+        query {
+          author @client {
+            name
+          }
+        }
+      `;
+
+      const link = createHttpLink({ uri: 'https://example.com/graphql' });
+
+      await new Promise<void>((resolve, reject) => {
+        execute(link, { query }).subscribe({
+          next: reject,
+          error: errorHandler.mockImplementation(resolve)
+        });
+      });
+
+      expect(errorHandler).toHaveBeenCalledWith(
+        new Error('HttpLink: Trying to send a client-only query to the server. To send to the server, ensure a non-client field is added to the query or set the `transformOptions.removeClientFields` option to `true`.')
+      );
+    });
   });
 
   describe('Dev warnings', () => {
@@ -1255,46 +1325,123 @@ describe('HttpLink', () => {
         }),
       );
     });
-    itAsync('supports being cancelled and does not throw', (resolve, reject) => {
-      let called = false;
-      class AbortController {
-        signal: {};
-        abort = () => {
-          called = true;
-        };
-      }
 
-      (global as any).AbortController = AbortController;
+    describe('AbortController', () => {
+      const originalAbortController = globalThis.AbortController;
+      afterEach(() => {
+        globalThis.AbortController = originalAbortController;
+      });
 
-      fetch.mockReturnValueOnce(Promise.resolve({ text }));
-      text.mockReturnValueOnce(
-        Promise.resolve('{ "data": { "hello": "world" } }'),
-      );
+      function trackGlobalAbortControllers() {
+        const instances: AbortController[] = []
+        class AbortControllerMock {
+          constructor() {
+            const instance = new originalAbortController()
+            instances.push(instance)
+            return instance
+          }
+        }
 
-      const link = createHttpLink({ uri: 'data', fetch: fetch as any });
+        globalThis.AbortController = AbortControllerMock as any;
+        return instances;
+      }
 
-      const sub = execute(link, { query: sampleQuery }).subscribe({
-        next: result => {
-          reject('result should not have been called');
+      const failingObserver: Observer<FetchResult> = {
+        next: () => {
+          fail('result should not have been called');
         },
         error: e => {
-          reject(e);
+          fail(e);
         },
         complete: () => {
-          reject('complete should not have been called');
+          fail('complete should not have been called');
         },
+      }
+
+      function mockFetch() {
+        const text = jest.fn(async () => '{ "data": { "stub": { "id": "foo" } } }');
+        const fetch = jest.fn(async (uri, options) => ({ text }));
+        return { text, fetch }
+      }
+
+      it("aborts the request when unsubscribing before the request has completed", () => {
+        const { fetch } = mockFetch();
+        const abortControllers = trackGlobalAbortControllers();
+
+        const link = createHttpLink({ uri: 'data', fetch: fetch as any });
+
+        const sub = execute(link, { query: sampleQuery }).subscribe(failingObserver);
+        sub.unsubscribe();
+
+        expect(abortControllers.length).toBe(1);
+        expect(abortControllers[0].signal.aborted).toBe(true);
       });
-      sub.unsubscribe();
 
-      setTimeout(
-        makeCallback(resolve, reject, () => {
-          delete (global as any).AbortController;
-          expect(called).toBe(true);
-          fetch.mockReset();
-          text.mockReset();
-        }),
-        150,
-      );
+      it('a passed-in signal will be forwarded to the `fetch` call and not be overwritten by an internally-created one', () => {
+        const { fetch } = mockFetch();
+        const externalAbortController = new AbortController();
+
+        const link = createHttpLink({ uri: 'data', fetch: fetch as any, fetchOptions: { signal: externalAbortController.signal } });
+
+        const sub = execute(link, { query: sampleQuery } ).subscribe(failingObserver);
+        sub.unsubscribe();
+
+        expect(fetch.mock.calls.length).toBe(1);
+        expect(fetch.mock.calls[0][1]).toEqual(expect.objectContaining({ signal: externalAbortController.signal }))
+      });
+
+      it('a passed-in signal that is cancelled will fail the observable with an `AbortError`', async () => {
+        try { 
+          fetchMock.restore();
+          fetchMock.postOnce('data', async () => '{ "data": { "stub": { "id": "foo" } } }');
+
+          const externalAbortController = new AbortController();
+
+          const link = createHttpLink({ uri: '/data', fetchOptions: { signal: externalAbortController.signal } });
+
+          const error = await new Promise<Error>(resolve => {
+            execute(link, { query: sampleQuery } ).subscribe({
+              ...failingObserver,
+              error: resolve,
+            });
+            externalAbortController.abort();
+          });
+          expect(error.name).toBe("AbortError")
+        } finally {
+          fetchMock.restore();
+        }
+      });
+
+      it('resolving fetch does not cause the AbortController to be aborted', async () => {
+        const { text, fetch } = mockFetch();
+        const abortControllers = trackGlobalAbortControllers();
+        text.mockResolvedValueOnce('{ "data": { "hello": "world" } }');
+
+        // (the request is already finished at that point)
+        const link = createHttpLink({ uri: 'data', fetch: fetch as any });
+
+        await new Promise<void>(resolve => execute(link, { query: sampleQuery }).subscribe({
+          complete: resolve
+        }));
+
+        expect(abortControllers.length).toBe(1);
+        expect(abortControllers[0].signal.aborted).toBe(false);
+      });
+
+      it('an unsuccessful fetch does not cause the AbortController to be aborted', async () => {
+        const { fetch } = mockFetch();
+        const abortControllers = trackGlobalAbortControllers();
+        fetch.mockRejectedValueOnce("This is an error!")
+        // the request would be closed by the browser in the case of an error anyways
+        const link = createHttpLink({ uri: 'data', fetch: fetch as any });
+
+        await new Promise<void>(resolve => execute(link, { query: sampleQuery }).subscribe({
+          error: resolve
+        }));
+
+        expect(abortControllers.length).toBe(1);
+        expect(abortControllers[0].signal.aborted).toBe(false);
+      });
     });
 
     const body = '{';
diff --git a/src/link/remove-typename/__tests__/removeTypenameFromVariables.ts b/src/link/remove-typename/__tests__/removeTypenameFromVariables.ts
new file mode 100644
--- /dev/null
+++ b/src/link/remove-typename/__tests__/removeTypenameFromVariables.ts
@@ -0,0 +1,540 @@
+import {
+  KEEP,
+  removeTypenameFromVariables,
+} from "../removeTypenameFromVariables";
+import { ApolloLink, Operation } from "../../core";
+import { Observable, gql } from "../../../core";
+import { createOperation, toPromise } from "../../utils";
+
+type PartialOperation = Partial<Pick<Operation, "variables">> &
+  Pick<Operation, "query">;
+
+// Since this link modifies the `operation` and we only care to test against
+// the changed operation, we use a custom `execute` helper here instead of the
+// version exported by the `core` module, which expects a well-formed response.
+async function execute(link: ApolloLink, operation: PartialOperation) {
+  function forward(operation: Operation) {
+    // use the `data` key to satisfy the TypeScript types required by
+    // `forward`'s' return value
+    return Observable.of({ data: operation });
+  }
+
+  const { data } = await toPromise(
+    link.request(createOperation({}, operation), forward)!
+  );
+
+  return data as Operation;
+}
+
+test("strips all __typename keys by default", async () => {
+  const query = gql`
+    query Test($foo: FooInput!, $bar: BarInput!) {
+      someField(foo: $foo, bar: $bar)
+    }
+  `;
+
+  const link = removeTypenameFromVariables();
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        __typename: "Foo",
+        foo: true,
+        bar: "Bar",
+        baz: { __typename: "Baz", baz: true },
+        qux: [{ __typename: "Qux", qux: 0 }],
+      },
+      bar: [{ __typename: "Bar", bar: true }],
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      foo: true,
+      bar: "Bar",
+      baz: { baz: true },
+      qux: [{ qux: 0 }],
+    },
+    bar: [{ bar: true }],
+  });
+});
+
+test("does nothing when no variables are passed", async () => {
+  const query = gql`
+    query Test {
+      foo {
+        bar
+      }
+    }
+  `;
+
+  const link = removeTypenameFromVariables();
+
+  const operation = { query };
+  const resultOperation = await execute(link, operation);
+
+  expect(resultOperation).toBe(operation);
+});
+
+test("does nothing when no variables are passed even if variables are declared in the document", async () => {
+  const query = gql`
+    query Test($unused: Boolean) {
+      foo {
+        bar
+      }
+    }
+  `;
+
+  const link = removeTypenameFromVariables();
+
+  const operation = { query };
+  const resultOperation = await execute(link, operation);
+
+  expect(resultOperation).toBe(operation);
+});
+
+test("keeps __typename for variables with types defined by `except`", async () => {
+  const query = gql`
+    query Test($foo: JSON, $bar: BarInput) {
+      someField(foo: $foo, bar: $bar)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      JSON: KEEP,
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        __typename: "Foo",
+        foo: true,
+        baz: { __typename: "Baz", baz: true },
+      },
+      bar: { __typename: "Bar", bar: true },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      __typename: "Foo",
+      foo: true,
+      baz: { __typename: "Baz", baz: true },
+    },
+    bar: { bar: true },
+  });
+});
+
+test("keeps __typename in all variables with types configured with `except`", async () => {
+  const query = gql`
+    query Test($foo: JSON, $bar: Config, $baz: BazInput) {
+      someField(foo: $foo, bar: $bar, baz: $baz)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      JSON: KEEP,
+      Config: KEEP,
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: { __typename: "Foo", foo: true },
+      bar: { __typename: "Bar", bar: true },
+      baz: { __typename: "Baz", baz: true },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: { __typename: "Foo", foo: true },
+    bar: { __typename: "Bar", bar: true },
+    baz: { baz: true },
+  });
+});
+
+test("handles variable declarations declared as non null and list types", async () => {
+  const query = gql`
+    query Test($foo: JSON!, $bar: [JSON], $baz: [JSON!]!, $qux: QuxInput!) {
+      someField(foo: $foo, bar: $bar, baz: $baz)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      JSON: KEEP,
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: { __typename: "Foo", foo: true },
+      bar: [
+        { __typename: "Bar", bar: true, baz: { __typename: "Baz", baz: true } },
+      ],
+      baz: [
+        { __typename: "Baz", baz: true },
+        { __typename: "Baz", baz: true },
+      ],
+      qux: { __typename: "Qux", qux: true },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: { __typename: "Foo", foo: true },
+    bar: [
+      { __typename: "Bar", bar: true, baz: { __typename: "Baz", baz: true } },
+    ],
+    baz: [
+      { __typename: "Baz", baz: true },
+      { __typename: "Baz", baz: true },
+    ],
+    qux: { qux: true },
+  });
+});
+
+test("keeps __typename at configured fields under input object types", async () => {
+  const query = gql`
+    query Test($foo: FooInput) {
+      someField(foo: $foo)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      FooInput: {
+        bar: KEEP,
+        baz: KEEP,
+      },
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        __typename: "Foo",
+        aa: true,
+        bar: {
+          __typename: "Bar",
+          bb: true,
+        },
+        baz: {
+          __typename: "Baz",
+          cc: true,
+        },
+        qux: {
+          __typename: "Qux",
+          dd: true,
+        },
+      },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      aa: true,
+      bar: {
+        __typename: "Bar",
+        bb: true,
+      },
+      baz: {
+        __typename: "Baz",
+        cc: true,
+      },
+      qux: {
+        dd: true,
+      },
+    },
+  });
+});
+
+test("keeps __typename at a deeply nested field", async () => {
+  const query = gql`
+    query Test($foo: FooInput) {
+      someField(foo: $foo)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      FooInput: {
+        bar: {
+          baz: {
+            qux: KEEP,
+          },
+        },
+      },
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        __typename: "Foo",
+        bar: {
+          __typename: "Bar",
+          baz: {
+            __typename: "Baz",
+            qux: {
+              __typename: "Qux",
+              quux: true,
+            },
+          },
+        },
+      },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      bar: {
+        baz: {
+          qux: {
+            __typename: "Qux",
+            quux: true,
+          },
+        },
+      },
+    },
+  });
+});
+
+test("handles configured fields varying nesting levels", async () => {
+  const query = gql`
+    query Test($foo: FooInput) {
+      someField(foo: $foo)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      FooInput: {
+        bar: KEEP,
+        baz: {
+          qux: KEEP,
+        },
+      },
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        __typename: "Foo",
+        bar: {
+          __typename: "Bar",
+          aa: true,
+        },
+        baz: {
+          __typename: "Baz",
+          qux: {
+            __typename: "Qux",
+            quux: true,
+          },
+        },
+      },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      bar: {
+        __typename: "Bar",
+        aa: true,
+      },
+      baz: {
+        qux: {
+          __typename: "Qux",
+          quux: true,
+        },
+      },
+    },
+  });
+});
+
+test("handles multiple configured types with fields", async () => {
+  const query = gql`
+    query Test($foo: FooInput, $baz: BazInput) {
+      someField(foo: $foo, baz: $baz)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      FooInput: {
+        bar: KEEP,
+      },
+      BazInput: {
+        qux: KEEP,
+      },
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        __typename: "Foo",
+        bar: {
+          __typename: "Bar",
+          aa: true,
+        },
+      },
+      baz: {
+        __typename: "Bar",
+        qux: {
+          __typename: "Qux",
+          bb: true,
+        },
+      },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      bar: {
+        __typename: "Bar",
+        aa: true,
+      },
+    },
+    baz: {
+      qux: {
+        __typename: "Qux",
+        bb: true,
+      },
+    },
+  });
+});
+
+test("handles when __typename is not present in all paths", async () => {
+  const query = gql`
+    query Test($foo: JSON, $bar: BarInput) {
+      someField(foo: $foo, bar: $bar)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      JSON: KEEP,
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        foo: true,
+        baz: { __typename: "Baz", baz: true },
+      },
+      bar: { bar: true },
+      qux: { __typename: "Qux", bar: true },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      foo: true,
+      baz: { __typename: "Baz", baz: true },
+    },
+    bar: { bar: true },
+    qux: { bar: true },
+  });
+});
+
+test("handles when __typename is not present in variables", async () => {
+  const query = gql`
+    query Test($foo: JSON, $bar: BarInput) {
+      someField(foo: $foo, bar: $bar)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      JSON: KEEP,
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        foo: true,
+        baz: { baz: true },
+      },
+      bar: { bar: true },
+      qux: [{ foo: true }],
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      foo: true,
+      baz: { baz: true },
+    },
+    bar: { bar: true },
+    qux: [{ foo: true }],
+  });
+});
+
+test("handles when declared variables are unused", async () => {
+  const query = gql`
+    query Test($foo: FooInput, $unused: JSON) {
+      someField(foo: $foo, bar: $bar)
+    }
+  `;
+
+  const link = removeTypenameFromVariables({
+    except: {
+      JSON: KEEP,
+    },
+  });
+
+  const { variables } = await execute(link, {
+    query,
+    variables: {
+      foo: {
+        __typename: "Foo",
+        foo: true,
+        baz: { __typename: "Bar", baz: true },
+      },
+    },
+  });
+
+  expect(variables).toStrictEqual({
+    foo: {
+      foo: true,
+      baz: { baz: true },
+    },
+  });
+});
+
+test("ensures operation.getContext and operation.setContext functions are properly forwarded", async () => {
+  const query = gql`
+    query Test($foo: FooInput) {
+      someField(foo: $foo)
+    }
+  `;
+
+  const link = removeTypenameFromVariables();
+
+  const operationWithoutVariables = await execute(link, { query });
+  const operationWithVariables = await execute(link, {
+    query,
+    variables: { foo: { __typename: "FooInput", bar: true } },
+  });
+
+  expect(typeof operationWithoutVariables.getContext).toBe("function");
+  expect(typeof operationWithoutVariables.setContext).toBe("function");
+  expect(typeof operationWithVariables.getContext).toBe("function");
+  expect(typeof operationWithVariables.setContext).toBe("function");
+});
diff --git a/src/link/schema/__tests__/schemaLink.ts b/src/link/schema/__tests__/schemaLink.ts
--- a/src/link/schema/__tests__/schemaLink.ts
+++ b/src/link/schema/__tests__/schemaLink.ts
@@ -29,7 +29,7 @@ describe('SchemaLink', () => {
   it('raises warning if called with concat', () => {
     const link = new SchemaLink({ schema });
     const _warn = console.warn;
-    console.warn = (warning: any) => expect(warning['message']).toBeDefined();
+    console.warn = (...args) => expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", link]);
     expect(link.concat((operation, forward) => forward(operation))).toEqual(
       link,
     );
diff --git a/src/link/utils/__tests__/filterOperationVariables.ts b/src/link/utils/__tests__/filterOperationVariables.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/__tests__/filterOperationVariables.ts
@@ -0,0 +1,38 @@
+import gql from 'graphql-tag';
+import { filterOperationVariables } from '../filterOperationVariables';
+
+const sampleQueryWithVariables = gql`
+  query MyQuery($a: Int!) {
+    stub(a: $a) {
+      id
+    }
+  }
+`;
+
+const sampleQueryWithoutVariables = gql`
+  query MyQuery {
+    stub {
+      id
+    }
+  }
+`;
+
+describe('filterOperationVariables', () => {
+  it('filters unused variables', () => {
+    const variables = { a: 1, b: 2, c: 3 };
+    const result = filterOperationVariables(
+      variables,
+      sampleQueryWithoutVariables
+    );
+    expect(result).toEqual({});
+  });
+
+  it('does not filter used variables', () => {
+    const variables = { a: 1, b: 2, c: 3 };
+    const result = filterOperationVariables(
+      variables,
+      sampleQueryWithVariables
+    );
+    expect(result).toEqual({ a: 1 });
+  });
+});
diff --git a/src/react/components/__tests__/client/Mutation.test.tsx b/src/react/components/__tests__/client/Mutation.test.tsx
--- a/src/react/components/__tests__/client/Mutation.test.tsx
+++ b/src/react/components/__tests__/client/Mutation.test.tsx
@@ -1,23 +1,21 @@
-import React, { useState, PropsWithChildren } from 'react';
-import gql from 'graphql-tag';
-import { ExecutionResult, GraphQLError } from 'graphql';
-import userEvent from '@testing-library/user-event';
-import { render, screen, waitFor, act } from '@testing-library/react';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloError } from '../../../../errors';
-import { DataProxy, InMemoryCache as Cache } from '../../../../cache';
-import { ApolloProvider } from '../../../context';
+import React, { useState, PropsWithChildren } from "react";
+import gql from "graphql-tag";
+import { ExecutionResult, GraphQLError } from "graphql";
+import userEvent from "@testing-library/user-event";
+import { render, screen, waitFor, act } from "@testing-library/react";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloError } from "../../../../errors";
+import { DataProxy, InMemoryCache as Cache } from "../../../../cache";
+import { ApolloProvider } from "../../../context";
 import {
   itAsync,
   MockedProvider,
   MockLink,
   mockSingleLink,
-} from '../../../../testing';
-import { Query } from '../../Query';
-import { Mutation } from '../../Mutation';
-
-const IS_REACT_18 = React.version.startsWith('18');
+} from "../../../../testing";
+import { Query } from "../../Query";
+import { Mutation } from "../../Mutation";
 
 const mutation = gql`
   mutation createTodo($text: String!) {
@@ -43,79 +41,79 @@ type Data = {
 
 const data: Data = {
   createTodo: {
-    __typename: 'Todo',
-    id: '99',
-    text: 'This one was created with a mutation.',
-    completed: true
+    __typename: "Todo",
+    id: "99",
+    text: "This one was created with a mutation.",
+    completed: true,
   },
-  __typename: 'Mutation'
+  __typename: "Mutation",
 };
 
 const data2: Data = {
   createTodo: {
-    __typename: 'Todo',
-    id: '100',
-    text: 'This one was created with a mutation.',
-    completed: true
+    __typename: "Todo",
+    id: "100",
+    text: "This one was created with a mutation.",
+    completed: true,
   },
-  __typename: 'Mutation'
+  __typename: "Mutation",
 };
 
 const mocks = [
   {
     request: { query: mutation },
-    result: { data }
+    result: { data },
   },
   {
     request: { query: mutation },
-    result: { data: data2 }
-  }
+    result: { data: data2 },
+  },
 ];
 
 const cache = new Cache({ addTypename: false });
 
-describe('General Mutation testing', () => {
-  it('pick prop client over context client', async () => {
+describe("General Mutation testing", () => {
+  it("pick prop client over context client", async () => {
     const mock = (text: string) => [
       {
         request: { query: mutation },
         result: {
           data: {
             createTodo: {
-              __typename: 'Todo',
-              id: '99',
+              __typename: "Todo",
+              id: "99",
               text,
-              completed: true
+              completed: true,
             },
-            __typename: 'Mutation'
-          }
-        }
+            __typename: "Mutation",
+          },
+        },
       },
       {
         request: { query: mutation },
         result: {
           data: {
             createTodo: {
-              __typename: 'Todo',
-              id: '100',
+              __typename: "Todo",
+              id: "100",
               text,
-              completed: true
+              completed: true,
             },
-            __typename: 'Mutation'
-          }
-        }
-      }
+            __typename: "Mutation",
+          },
+        },
+      },
     ];
 
-    const mocksProps = mock('This is the result of the prop client mutation.');
+    const mocksProps = mock("This is the result of the prop client mutation.");
     const mocksContext = mock(
-      'This is the result of the context client mutation.'
+      "This is the result of the context client mutation."
     );
 
     function mockClient(m: any) {
       return new ApolloClient({
         link: new MockLink(m, false),
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
     }
 
@@ -136,48 +134,66 @@ describe('General Mutation testing', () => {
     };
 
     const { rerender } = render(<Component />);
-    await waitFor(() => {
-      screen.getByText('Create');
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        screen.getByText("Create");
+      },
+      { interval: 1 }
+    );
 
     // context client mutation
-    await userEvent.click(screen.getByText('Create'));
+    await userEvent.click(screen.getByText("Create"));
 
-    await waitFor(() => {
-      expect(spy).toHaveBeenCalledWith(mocksContext[0].result);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(spy).toHaveBeenCalledWith(mocksContext[0].result);
+      },
+      { interval: 1 }
+    );
 
     // props client mutation
     rerender(<Component propsClient={propsClient} />);
 
-    await userEvent.click(screen.getByText('Create'));
+    await userEvent.click(screen.getByText("Create"));
 
-    await waitFor(() => {
-      expect(spy).toHaveBeenCalledWith(mocksProps[0].result);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(spy).toHaveBeenCalledWith(mocksProps[0].result);
+      },
+      { interval: 1 }
+    );
 
     // context client mutation
     rerender(<Component propsClient={undefined} />);
-    await userEvent.click(screen.getByText('Create'));
+    await userEvent.click(screen.getByText("Create"));
 
-    await waitFor(() => {
-      expect(spy).toHaveBeenCalledWith(mocksContext[1].result);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(spy).toHaveBeenCalledWith(mocksContext[1].result);
+      },
+      { interval: 1 }
+    );
 
     // props client mutation
     rerender(<Component propsClient={propsClient} />);
-    await userEvent.click(screen.getByText('Create'));
+    await userEvent.click(screen.getByText("Create"));
 
-    await waitFor(() => {
-      expect(spy).toHaveBeenCalledWith(mocksProps[1].result);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(spy).toHaveBeenCalledWith(mocksProps[1].result);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(spy).toHaveBeenCalledTimes(4);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(spy).toHaveBeenCalledTimes(4);
+      },
+      { interval: 1 }
+    );
   });
 
-  itAsync('performs a mutation', (resolve, reject) => {
+  itAsync("performs a mutation", (resolve, reject) => {
     let count = 0;
     const Component = () => (
       <Mutation mutation={mutation}>
@@ -215,37 +231,40 @@ describe('General Mutation testing', () => {
     }).then(resolve, reject);
   });
 
-  itAsync('can bind only the mutation and not rerender by props', (resolve, reject) => {
-    let count = 0;
-    const Component = () => (
-      <Mutation mutation={mutation} ignoreResults>
-        {(createTodo: any, result: any) => {
-          if (count === 0) {
-            expect(result.loading).toEqual(false);
-            expect(result.called).toEqual(false);
-            setTimeout(() => {
-              createTodo().then((r: any) => {
-                expect(r!.data).toEqual(data);
-                resolve();
+  itAsync(
+    "can bind only the mutation and not rerender by props",
+    (resolve, reject) => {
+      let count = 0;
+      const Component = () => (
+        <Mutation mutation={mutation} ignoreResults>
+          {(createTodo: any, result: any) => {
+            if (count === 0) {
+              expect(result.loading).toEqual(false);
+              expect(result.called).toEqual(false);
+              setTimeout(() => {
+                createTodo().then((r: any) => {
+                  expect(r!.data).toEqual(data);
+                  resolve();
+                });
               });
-            });
-          } else if (count === 1) {
-            reject('rerender happened with ignoreResults turned on');
-          }
-          count++;
-          return <div />;
-        }}
-      </Mutation>
-    );
+            } else if (count === 1) {
+              reject("rerender happened with ignoreResults turned on");
+            }
+            count++;
+            return <div />;
+          }}
+        </Mutation>
+      );
 
-    render(
-      <MockedProvider mocks={mocks}>
-        <Component />
-      </MockedProvider>
-    );
-  });
+      render(
+        <MockedProvider mocks={mocks}>
+          <Component />
+        </MockedProvider>
+      );
+    }
+  );
 
-  it('returns a resolved promise when calling the mutation function', async () => {
+  it("returns a resolved promise when calling the mutation function", async () => {
     let called = false;
     let result: any;
     const Component = () => (
@@ -253,7 +272,7 @@ describe('General Mutation testing', () => {
         {(createTodo: any) => {
           if (!called) {
             createTodo().then((_result: any) => {
-              result = _result
+              result = _result;
             });
           }
           called = true;
@@ -271,10 +290,10 @@ describe('General Mutation testing', () => {
 
     await waitFor(() => {
       expect(result!.data).toEqual(data);
-    })
+    });
   });
 
-  it('returns rejected promise when calling the mutation function', async () => {
+  it("returns rejected promise when calling the mutation function", async () => {
     let done = false;
     let called = false;
     const Component = () => (
@@ -282,7 +301,7 @@ describe('General Mutation testing', () => {
         {(createTodo: any) => {
           if (!called) {
             createTodo().catch((error: any) => {
-              expect(error).toEqual(new Error('Error 1'));
+              expect(error).toEqual(new Error("Error 1"));
               done = true;
             });
           }
@@ -296,8 +315,8 @@ describe('General Mutation testing', () => {
     const mocksWithErrors = [
       {
         request: { query: mutation },
-        error: new Error('Error 1')
-      }
+        error: new Error("Error 1"),
+      },
     ];
 
     render(
@@ -311,7 +330,7 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('only shows result for the latest mutation that is in flight', async () => {
+  it("only shows result for the latest mutation that is in flight", async () => {
     let count = 0;
 
     const onCompleted = (dataMutation: Data) => {
@@ -354,14 +373,14 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('only shows the error for the latest mutation in flight', async () => {
+  it("only shows the error for the latest mutation in flight", async () => {
     let count = 0;
 
     const onError = (error: Error) => {
       if (count === 1) {
-        expect(error).toEqual(new Error('Error 1'));
+        expect(error).toEqual(new Error("Error 1"));
       } else if (count === 3) {
-        expect(error).toEqual(new Error('Error 2'));
+        expect(error).toEqual(new Error("Error 2"));
       }
     };
     const Component = () => (
@@ -379,7 +398,7 @@ describe('General Mutation testing', () => {
             expect(result.loading).toEqual(false);
             expect(result.data).toEqual(undefined);
             expect(result.called).toEqual(true);
-            expect(result.error).toEqual(new Error('Error 2'));
+            expect(result.error).toEqual(new Error("Error 2"));
           }
           count++;
           return <div />;
@@ -390,12 +409,12 @@ describe('General Mutation testing', () => {
     const mocksWithErrors = [
       {
         request: { query: mutation },
-        error: new Error('Error 2')
+        error: new Error("Error 2"),
       },
       {
         request: { query: mutation },
-        error: new Error('Error 2')
-      }
+        error: new Error("Error 2"),
+      },
     ];
 
     render(
@@ -409,19 +428,19 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('calls the onCompleted prop as soon as the mutation is complete', async () => {
+  it("calls the onCompleted prop as soon as the mutation is complete", async () => {
     let onCompletedCalled = false;
 
     class Component extends React.Component {
       state = {
-        mutationDone: false
+        mutationDone: false,
       };
 
       onCompleted = (mutationData: Data) => {
         expect(mutationData).toEqual(data);
         onCompletedCalled = true;
         this.setState({
-          mutationDone: true
+          mutationDone: true,
         });
       };
 
@@ -450,11 +469,11 @@ describe('General Mutation testing', () => {
     );
 
     await waitFor(() => {
-      expect(onCompletedCalled).toEqual(true)
+      expect(onCompletedCalled).toEqual(true);
     });
   });
 
-  it('renders result of the children render prop', () => {
+  it("renders result of the children render prop", () => {
     const Component = () => (
       <Mutation mutation={mutation}>{() => <div>result</div>}</Mutation>
     );
@@ -464,26 +483,24 @@ describe('General Mutation testing', () => {
         <Component />
       </MockedProvider>
     );
-    expect(screen.getByText('result')).toBeTruthy();
+    expect(screen.getByText("result")).toBeTruthy();
     // unmount here or else the mutation will resolve later and schedule an update that's not wrapped in act.
-    unmount()
+    unmount();
   });
 
-  it('renders an error state', async () => {
+  it("renders an error state", async () => {
     let count = 0;
     const Component = () => (
       <Mutation mutation={mutation}>
         {(createTodo: any, result: any) => {
           if (count === 0) {
             createTodo().catch((err: any) => {
-              expect(err).toEqual(new Error('error occurred'));
+              expect(err).toEqual(new Error("error occurred"));
             });
           } else if (count === 1) {
             expect(result.loading).toBeTruthy();
           } else if (count === 2) {
-            expect(result.error).toEqual(
-              new Error('error occurred')
-            );
+            expect(result.error).toEqual(new Error("error occurred"));
           }
           count++;
           return <div />;
@@ -494,8 +511,8 @@ describe('General Mutation testing', () => {
     const mockError = [
       {
         request: { query: mutation },
-        error: new Error('error occurred')
-      }
+        error: new Error("error occurred"),
+      },
     ];
 
     render(
@@ -509,11 +526,11 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('renders an error state and throws when encountering graphql errors', async () => {
+  it("renders an error state and throws when encountering graphql errors", async () => {
     let count = 0;
 
     const expectedError = new ApolloError({
-      graphQLErrors: [new GraphQLError('error occurred')]
+      graphQLErrors: [new GraphQLError("error occurred")],
     });
 
     const Component = () => (
@@ -522,7 +539,7 @@ describe('General Mutation testing', () => {
           if (count === 0) {
             createTodo()
               .then(() => {
-                throw new Error('Did not expect a result');
+                throw new Error("Did not expect a result");
               })
               .catch((e: any) => {
                 expect(e).toEqual(expectedError);
@@ -542,9 +559,9 @@ describe('General Mutation testing', () => {
       {
         request: { query: mutation },
         result: {
-          errors: [new GraphQLError('error occurred')]
-        }
-      }
+          errors: [new GraphQLError("error occurred")],
+        },
+      },
     ];
 
     render(
@@ -558,7 +575,7 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('renders an error state and does not throw when encountering graphql errors when errorPolicy=all', async () => {
+  it("renders an error state and does not throw when encountering graphql errors when errorPolicy=all", async () => {
     let count = 0;
     const Component = () => (
       <Mutation mutation={mutation}>
@@ -569,7 +586,7 @@ describe('General Mutation testing', () => {
                 if (fetchResult && fetchResult.errors) {
                   expect(fetchResult.errors.length).toEqual(1);
                   expect(fetchResult.errors[0]).toEqual(
-                    new GraphQLError('error occurred')
+                    new GraphQLError("error occurred")
                   );
                 } else {
                   throw new Error(
@@ -585,7 +602,7 @@ describe('General Mutation testing', () => {
           } else if (count === 2) {
             expect(result.error).toEqual(
               new ApolloError({
-                graphQLErrors: [new GraphQLError('error occurred')]
+                graphQLErrors: [new GraphQLError("error occurred")],
               })
             );
           }
@@ -599,14 +616,14 @@ describe('General Mutation testing', () => {
       {
         request: { query: mutation },
         result: {
-          errors: [new GraphQLError('error occurred')]
-        }
-      }
+          errors: [new GraphQLError("error occurred")],
+        },
+      },
     ];
 
     render(
       <MockedProvider
-        defaultOptions={{ mutate: { errorPolicy: 'all' } }}
+        defaultOptions={{ mutate: { errorPolicy: "all" } }}
         mocks={mockError}
       >
         <Component />
@@ -618,10 +635,10 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('renders an error state and throws when encountering network errors when errorPolicy=all', async () => {
+  it("renders an error state and throws when encountering network errors when errorPolicy=all", async () => {
     let count = 0;
     const expectedError = new ApolloError({
-      networkError: new Error('network error')
+      networkError: new Error("network error"),
     });
     const Component = () => (
       <Mutation mutation={mutation}>
@@ -629,7 +646,7 @@ describe('General Mutation testing', () => {
           if (count === 0) {
             createTodo()
               .then(() => {
-                throw new Error('Did not expect a result');
+                throw new Error("Did not expect a result");
               })
               .catch((e: any) => {
                 expect(e).toEqual(expectedError);
@@ -648,13 +665,13 @@ describe('General Mutation testing', () => {
     const mockError = [
       {
         request: { query: mutation },
-        error: new Error('network error')
-      }
+        error: new Error("network error"),
+      },
     ];
 
     render(
       <MockedProvider
-        defaultOptions={{ mutate: { errorPolicy: 'all' } }}
+        defaultOptions={{ mutate: { errorPolicy: "all" } }}
         mocks={mockError}
       >
         <Component />
@@ -666,16 +683,16 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('calls the onError prop if the mutation encounters an error', async () => {
+  it("calls the onError prop if the mutation encounters an error", async () => {
     let onRenderCalled = false;
 
     class Component extends React.Component {
       state = {
-        mutationError: false
+        mutationError: false,
       };
 
       onError = (error: Error) => {
-        expect(error.message).toMatch('error occurred');
+        expect(error.message).toMatch("error occurred");
         onRenderCalled = true;
         this.setState({ mutationError: true });
       };
@@ -703,8 +720,8 @@ describe('General Mutation testing', () => {
     const mockError = [
       {
         request: { query: mutation },
-        error: new Error('error occurred')
-      }
+        error: new Error("error occurred"),
+      },
     ];
 
     render(
@@ -718,9 +735,9 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('performs a mutation with variables prop', async () => {
+  it("performs a mutation with variables prop", async () => {
     const variables = {
-      text: 'play tennis'
+      text: "play tennis",
     };
 
     let count = 0;
@@ -746,8 +763,8 @@ describe('General Mutation testing', () => {
     const mocks1 = [
       {
         request: { query: mutation, variables },
-        result: { data }
-      }
+        result: { data },
+      },
     ];
 
     render(
@@ -761,9 +778,9 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('allows passing a variable to the mutate function', async () => {
+  it("allows passing a variable to the mutate function", async () => {
     const variables = {
-      text: 'play tennis'
+      text: "play tennis",
     };
 
     let count = 0;
@@ -789,8 +806,8 @@ describe('General Mutation testing', () => {
     const mocks1 = [
       {
         request: { query: mutation, variables },
-        result: { data }
-      }
+        result: { data },
+      },
     ];
 
     render(
@@ -804,21 +821,21 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('allows an optimistic response prop', async () => {
+  it("allows an optimistic response prop", async () => {
     const link = mockSingleLink(...mocks);
     const client = new ApolloClient({
       link,
-      cache
+      cache,
     });
 
     const optimisticResponse = {
       createTodo: {
-        id: '99',
-        text: 'This is an optimistic response',
+        id: "99",
+        text: "This is an optimistic response",
         completed: false,
-        __typename: 'Todo'
+        __typename: "Todo",
       },
-      __typename: 'Mutation'
+      __typename: "Mutation",
     };
 
     let count = 0;
@@ -828,7 +845,7 @@ describe('General Mutation testing', () => {
           if (count === 0) {
             createTodo();
             const dataInStore = client.cache.extract(true);
-            expect(dataInStore['Todo:99']).toEqual(
+            expect(dataInStore["Todo:99"]).toEqual(
               optimisticResponse.createTodo
             );
           } else if (count === 1) {
@@ -856,21 +873,21 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('allows passing an optimistic response to the mutate function', async () => {
+  it("allows passing an optimistic response to the mutate function", async () => {
     const link = mockSingleLink(...mocks);
     const client = new ApolloClient({
       link,
-      cache
+      cache,
     });
 
     const optimisticResponse = {
       createTodo: {
-        id: '99',
-        text: 'This is an optimistic response',
+        id: "99",
+        text: "This is an optimistic response",
         completed: false,
-        __typename: 'Todo'
+        __typename: "Todo",
       },
-      __typename: 'Mutation'
+      __typename: "Mutation",
     };
 
     let count = 0;
@@ -880,7 +897,7 @@ describe('General Mutation testing', () => {
           if (count === 0) {
             createTodo({ optimisticResponse });
             const dataInStore = client.cache.extract(true);
-            expect(dataInStore['Todo:99']).toEqual(
+            expect(dataInStore["Todo:99"]).toEqual(
               optimisticResponse.createTodo
             );
           } else if (count === 2) {
@@ -905,7 +922,7 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('allows a refetchQueries prop', async () => {
+  it("allows a refetchQueries prop", async () => {
     const query = gql`
       query getTodo {
         todo {
@@ -920,30 +937,30 @@ describe('General Mutation testing', () => {
 
     const queryData = {
       todo: {
-        id: '1',
-        text: 'todo from query',
+        id: "1",
+        text: "todo from query",
         completed: false,
-        __typename: 'Todo'
+        __typename: "Todo",
       },
-      __typename: 'Query'
+      __typename: "Query",
     };
 
     const mocksWithQuery = [
       ...mocks,
       {
         request: { query },
-        result: { data: queryData }
+        result: { data: queryData },
       },
       {
         request: { query },
-        result: { data: queryData }
+        result: { data: queryData },
       },
     ];
 
     const refetchQueries = [
       {
-        query
-      }
+        query,
+      },
     ];
 
     let renderCount = 0;
@@ -978,7 +995,7 @@ describe('General Mutation testing', () => {
     );
   });
 
-  it('allows a refetchQueries prop as string and variables have updated', async () => new Promise((resolve, reject) => {
+  it("allows a refetchQueries prop as string and variables have updated", async () => {
     const query = gql`
       query people($first: Int) {
         allPeople(first: $first) {
@@ -991,43 +1008,46 @@ describe('General Mutation testing', () => {
 
     const peopleData1 = {
       allPeople: {
-        people: [{ name: 'Luke Skywalker', __typename: 'Person' }],
-        __typename: 'People'
-      }
+        people: [{ name: "Luke Skywalker", __typename: "Person" }],
+        __typename: "People",
+      },
     };
     const peopleData2 = {
       allPeople: {
-        people: [{ name: 'Han Solo', __typename: 'Person' }],
-        __typename: 'People'
-      }
+        people: [{ name: "Han Solo", __typename: "Person" }],
+        __typename: "People",
+      },
     };
     const peopleData3 = {
       allPeople: {
-        people: [{ name: 'Lord Vader', __typename: 'Person' }],
-        __typename: 'People'
-      }
+        people: [{ name: "Lord Vader", __typename: "Person" }],
+        __typename: "People",
+      },
     };
     const peopleMocks = [
       ...mocks,
       {
         request: { query, variables: { first: 1 } },
-        result: { data: peopleData1 }
+        result: { data: peopleData1 },
       },
       {
         request: { query, variables: { first: 2 } },
-        result: { data: peopleData2 }
+        result: { data: peopleData2 },
       },
       {
         request: { query, variables: { first: 2 } },
-        result: { data: peopleData3 }
-      }
+        result: { data: peopleData3 },
+      },
     ];
 
-    const refetchQueries = ['people'];
+    const refetchQueries = ["people"];
 
     let count = 0;
+    let testFailures: any[] = [];
 
-    const Component: React.FC<PropsWithChildren<PropsWithChildren<any>>> = props => {
+    const Component: React.FC<PropsWithChildren<PropsWithChildren<any>>> = (
+      props
+    ) => {
       const [variables, setVariables] = useState(props.variables);
       return (
         <Mutation mutation={mutation} refetchQueries={refetchQueries}>
@@ -1055,19 +1075,20 @@ describe('General Mutation testing', () => {
                     // mutation loading
                     expect(resultMutation.loading).toBe(true);
                   } else if (count === 5) {
-                    // mutation loaded
-                    expect(resultMutation.loading).toBe(false);
+                    // query refetched or mutation loaded
+                    // or both finished batched together
+                    // hard to make assumptions here
                   } else if (count === 6) {
-                    // query refetched
+                    // both loaded
                     expect(resultQuery.loading).toBe(false);
                     expect(resultMutation.loading).toBe(false);
                     expect(resultQuery.data).toEqual(peopleData3);
                   } else {
-                    reject(`Too many renders (${count})`);
+                    throw new Error(`Too many renders (${count})`);
                   }
                   count++;
                 } catch (err) {
-                  reject(err);
+                  testFailures.push(err);
                 }
                 return null;
               }}
@@ -1083,93 +1104,97 @@ describe('General Mutation testing', () => {
       </MockedProvider>
     );
 
-    waitFor(() => {
-      expect(count).toEqual(IS_REACT_18 ? 6 : 7);
-    }).then(resolve, reject);
-  }));
+    await waitFor(() => {
+      if (testFailures.length > 0) {
+        throw testFailures[0];
+      }
+      expect(count).toEqual(7);
+    });
+  });
 
-  it('allows refetchQueries to be passed to the mutate function', () => new Promise((resolve, reject) => {
-    const query = gql`
-      query getTodo {
-        todo {
-          id
-          text
-          completed
+  it("allows refetchQueries to be passed to the mutate function", () =>
+    new Promise((resolve, reject) => {
+      const query = gql`
+        query getTodo {
+          todo {
+            id
+            text
+            completed
+            __typename
+          }
           __typename
         }
-        __typename
-      }
-    `;
-
-    const queryData = {
-      todo: {
-        id: '1',
-        text: 'todo from query',
-        completed: false,
-        __typename: 'Todo'
-      },
-      __typename: 'Query'
-    };
-
-    const mocksWithQuery = [
-      ...mocks,
-      {
-        request: { query },
-        result: { data: queryData }
-      },
-      {
-        request: { query },
-        result: { data: queryData }
-      },
-    ];
-
-    const refetchQueries = [
-      {
-        query
-      }
-    ];
+      `;
+
+      const queryData = {
+        todo: {
+          id: "1",
+          text: "todo from query",
+          completed: false,
+          __typename: "Todo",
+        },
+        __typename: "Query",
+      };
 
-    let count = 0;
-    const Component = () => (
-      <Mutation mutation={mutation}>
-        {(createTodo: any, resultMutation: any) => (
-          <Query query={query}>
-            {(resultQuery: any) => {
-              try {
-                if (count === 0) {
-                  setTimeout(() => createTodo({ refetchQueries }), 10);
-                } else if (count === 1) {
-                  expect(resultMutation.loading).toBe(false);
-                  expect(resultQuery.loading).toBe(false);
-                } else if (count === 2) {
-                  expect(resultMutation.loading).toBe(true);
-                  expect(resultQuery.data).toEqual(queryData);
-                } else if (count === 3) {
-                  expect(resultMutation.loading).toBe(false);
+      const mocksWithQuery = [
+        ...mocks,
+        {
+          request: { query },
+          result: { data: queryData },
+        },
+        {
+          request: { query },
+          result: { data: queryData },
+        },
+      ];
+
+      const refetchQueries = [
+        {
+          query,
+        },
+      ];
+
+      let count = 0;
+      const Component = () => (
+        <Mutation mutation={mutation}>
+          {(createTodo: any, resultMutation: any) => (
+            <Query query={query}>
+              {(resultQuery: any) => {
+                try {
+                  if (count === 0) {
+                    setTimeout(() => createTodo({ refetchQueries }), 10);
+                  } else if (count === 1) {
+                    expect(resultMutation.loading).toBe(false);
+                    expect(resultQuery.loading).toBe(false);
+                  } else if (count === 2) {
+                    expect(resultMutation.loading).toBe(true);
+                    expect(resultQuery.data).toEqual(queryData);
+                  } else if (count === 3) {
+                    expect(resultMutation.loading).toBe(false);
+                  }
+                  count++;
+                } catch (err) {
+                  reject(err);
                 }
-                count++;
-              } catch (err) {
-                reject(err);
-              }
-              return null;
-            }}
-          </Query>
-        )}
-      </Mutation>
-    );
+                return null;
+              }}
+            </Query>
+          )}
+        </Mutation>
+      );
 
-    render(
-      <MockedProvider mocks={mocksWithQuery}>
-        <Component />
-      </MockedProvider>
-    );
+      render(
+        <MockedProvider mocks={mocksWithQuery}>
+          <Component />
+        </MockedProvider>
+      );
 
-    waitFor(() => {
-      expect(count).toBe(4);
-    }).then(resolve, reject);
-  }));
+      waitFor(() => {
+        expect(count).toBe(4);
+      }).then(resolve, reject);
+    }));
 
-  it('has an update prop for updating the store after the mutation', async () => {
+  it("has an update prop for updating the store after the mutation", async () => {
     const update = (_proxy: DataProxy, response: ExecutionResult) => {
       expect(response.data).toEqual(data);
     };
@@ -1200,7 +1225,7 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('allows update to be passed to the mutate function', async () => {
+  it("allows update to be passed to the mutate function", async () => {
     const update = (_proxy: DataProxy, response: ExecutionResult) => {
       expect(response.data).toEqual(data);
     };
@@ -1227,17 +1252,17 @@ describe('General Mutation testing', () => {
     );
 
     await waitFor(() => {
-      expect(count).toBe(3)
+      expect(count).toBe(3);
     });
   });
 
-  it('allows for overriding the options passed in the props by passing them in the mutate function', async () => {
+  it("allows for overriding the options passed in the props by passing them in the mutate function", async () => {
     const variablesProp = {
-      text: 'play tennis'
+      text: "play tennis",
     };
 
     const variablesMutateFn = {
-      text: 'go swimming'
+      text: "go swimming",
     };
 
     let count = 0;
@@ -1260,12 +1285,12 @@ describe('General Mutation testing', () => {
     const mocks1 = [
       {
         request: { query: mutation, variables: variablesProp },
-        result: { data }
+        result: { data },
       },
       {
         request: { query: mutation, variables: variablesMutateFn },
-        result: { data: data2 }
-      }
+        result: { data: data2 },
+      },
     ];
 
     render(
@@ -1279,40 +1304,40 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('updates if the client changes', async () => {
+  it("updates if the client changes", async () => {
     const link1 = mockSingleLink({
       request: { query: mutation },
-      result: { data }
+      result: { data },
     });
     const client1 = new ApolloClient({
       link: link1,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const data3 = {
       createTodo: {
-        __typename: 'Todo',
-        id: '100',
-        text: 'After updating client.',
-        completed: false
+        __typename: "Todo",
+        id: "100",
+        text: "After updating client.",
+        completed: false,
       },
-      __typename: 'Mutation'
+      __typename: "Mutation",
     };
 
     const link2 = mockSingleLink({
       request: { query: mutation },
-      result: { data: data3 }
+      result: { data: data3 },
     });
 
     const client2 = new ApolloClient({
       link: link2,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
     class Component extends React.Component {
       state = {
-        client: client1
+        client: client1,
       };
 
       render() {
@@ -1328,7 +1353,7 @@ describe('General Mutation testing', () => {
                   expect(result.data).toEqual(data);
                   setTimeout(() => {
                     this.setState({
-                      client: client2
+                      client: client2,
                     });
                   });
                 } else if (count === 3) {
@@ -1353,23 +1378,23 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('uses client from props instead of one provided by context', () => {
+  it("uses client from props instead of one provided by context", () => {
     const link1 = mockSingleLink({
       request: { query: mutation },
-      result: { data }
+      result: { data },
     });
     const client1 = new ApolloClient({
       link: link1,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const link2 = mockSingleLink({
       request: { query: mutation },
-      result: { data: data2 }
+      result: { data: data2 },
     });
     const client2 = new ApolloClient({
       link: link2,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
@@ -1402,7 +1427,7 @@ describe('General Mutation testing', () => {
     });
   });
 
-  it('errors if a query is passed instead of a mutation', () => {
+  it("errors if a query is passed instead of a mutation", () => {
     const query = gql`
       query todos {
         todos {
@@ -1422,14 +1447,14 @@ describe('General Mutation testing', () => {
         </MockedProvider>
       );
     }).toThrowError(
-      'Running a Mutation requires a graphql Mutation, but a Query was used ' +
-        'instead.'
+      "Running a Mutation requires a graphql Mutation, but a Query was used " +
+        "instead."
     );
 
     console.log = errorLogger;
   });
 
-  it('errors when changing from mutation to a query', async () => {
+  it("errors when changing from mutation to a query", async () => {
     let didError = false;
     const query = gql`
       query todos {
@@ -1441,14 +1466,14 @@ describe('General Mutation testing', () => {
 
     class Component extends React.Component {
       state = {
-        query: mutation
+        query: mutation,
       };
 
       componentDidCatch(e: Error) {
         expect(e).toEqual(
           new Error(
-            'Running a Mutation requires a graphql Mutation, but a Query ' +
-              'was used instead.'
+            "Running a Mutation requires a graphql Mutation, but a Query " +
+              "was used instead."
           )
         );
         didError = true;
@@ -1459,7 +1484,7 @@ describe('General Mutation testing', () => {
             {() => {
               setTimeout(() => {
                 this.setState({
-                  query
+                  query,
                 });
               });
               return null;
@@ -1492,7 +1517,7 @@ describe('General Mutation testing', () => {
     console.log = errorLogger;
   });
 
-  it('errors if a subscription is passed instead of a mutation', () => {
+  it("errors if a subscription is passed instead of a mutation", () => {
     const subscription = gql`
       subscription todos {
         todos {
@@ -1512,14 +1537,14 @@ describe('General Mutation testing', () => {
         </MockedProvider>
       );
     }).toThrowError(
-      'Running a Mutation requires a graphql Mutation, but a Subscription ' +
-        'was used instead.'
+      "Running a Mutation requires a graphql Mutation, but a Subscription " +
+        "was used instead."
     );
 
     console.log = errorLogger;
   });
 
-  it('errors when changing from mutation to a subscription', async () => {
+  it("errors when changing from mutation to a subscription", async () => {
     let didError = false;
     const subscription = gql`
       subscription todos {
@@ -1531,14 +1556,14 @@ describe('General Mutation testing', () => {
 
     class Component extends React.Component {
       state = {
-        query: mutation
+        query: mutation,
       };
 
       componentDidCatch(e: Error) {
         expect(e).toEqual(
           new Error(
-            'Running a Mutation requires a graphql Mutation, but a ' +
-              'Subscription was used instead.'
+            "Running a Mutation requires a graphql Mutation, but a " +
+              "Subscription was used instead."
           )
         );
         didError = true;
@@ -1550,7 +1575,7 @@ describe('General Mutation testing', () => {
             {() => {
               setTimeout(() => {
                 this.setState({
-                  query: subscription
+                  query: subscription,
                 });
               });
               return null;
@@ -1582,22 +1607,25 @@ describe('General Mutation testing', () => {
     console.log = errorLogger;
   });
 
-  describe('after it has been unmounted', () => {
-    it('calls the onCompleted prop after the mutation is complete', async () => {
+  describe("after it has been unmounted", () => {
+    it("calls the onCompleted prop after the mutation is complete", async () => {
       let finished = false;
       let success = false;
-      const context = { "foo": "bar" }
+      const context = { foo: "bar" };
       const onCompletedFn = jest.fn();
       const checker = () => {
         setTimeout(() => {
           success = true;
-          expect(onCompletedFn).toHaveBeenCalledWith(data, expect.objectContaining({ context }));
+          expect(onCompletedFn).toHaveBeenCalledWith(
+            data,
+            expect.objectContaining({ context })
+          );
         }, 100);
       };
 
       class Component extends React.Component {
         state = {
-          called: false
+          called: false,
         };
 
         render() {
@@ -1612,7 +1640,7 @@ describe('General Mutation testing', () => {
                     createTodo({ context }).finally(() => {
                       finished = true;
                     });
-                    expect(onCompletedFn).toHaveBeenCalledWith
+                    expect(onCompletedFn).toHaveBeenCalledWith;
                     // eslint-disable-next-line testing-library/await-async-utils
                     this.setState({ called: true }, checker);
                   });
@@ -1630,32 +1658,38 @@ describe('General Mutation testing', () => {
         </MockedProvider>
       );
 
-      await waitFor(() => {
-        // TODO(fixme): The following line fixes the RTL lint rule error:
-        //
-        // expect(waitFor(() => finished)).resolves.toBe(true);
-        //
-        // ...however it also causes the test to fail against React 17.
-        // eslint-disable-next-line testing-library/await-async-utils
-        expect(finished).toBe(true);
-      }, { interval: 1 });
-      await waitFor(() => {
-        // TODO(fixme): The following line fixes the RTL lint rule error:
-        //
-        // expect(waitFor(() => success)).resolves.toBe(true);
-        //
-        // ...however it also causes the test to fail against React 17.
-        // eslint-disable-next-line testing-library/await-async-utils
-        expect(success).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          // TODO(fixme): The following line fixes the RTL lint rule error:
+          //
+          // expect(waitFor(() => finished)).resolves.toBe(true);
+          //
+          // ...however it also causes the test to fail against React 17.
+          // eslint-disable-next-line testing-library/await-async-utils
+          expect(finished).toBe(true);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          // TODO(fixme): The following line fixes the RTL lint rule error:
+          //
+          // expect(waitFor(() => success)).resolves.toBe(true);
+          //
+          // ...however it also causes the test to fail against React 17.
+          // eslint-disable-next-line testing-library/await-async-utils
+          expect(success).toBe(true);
+        },
+        { interval: 1 }
+      );
     });
   });
 
-  it('calls the onError prop if the mutation encounters an error', async () => {
+  it("calls the onError prop if the mutation encounters an error", async () => {
     let finished = false;
     let onErrorCalled = false;
     function onError(error: ApolloError) {
-      expect(error.message).toEqual('error occurred');
+      expect(error.message).toEqual("error occurred");
       onErrorCalled = true;
     }
 
@@ -1677,8 +1711,8 @@ describe('General Mutation testing', () => {
     const mockError = [
       {
         request: { query: mutation },
-        error: new Error('error occurred')
-      }
+        error: new Error("error occurred"),
+      },
     ];
 
     render(
@@ -1687,23 +1721,29 @@ describe('General Mutation testing', () => {
       </MockedProvider>
     );
 
-    await waitFor(() => {
-      // TODO(fixme): The following line fixes the RTL lint rule error:
-      //
-      // expect(waitFor(() => onErrorCalled)).resolves.toBe(true);
-      //
-      // ...however it also causes the test to fail against React 17.
-      // eslint-disable-next-line testing-library/await-async-utils
-      expect(onErrorCalled).toBe(true);
-    }, { interval: 1 });
-    await waitFor(() => {
-      // TODO(fixme): The following line fixes the RTL lint rule error:
-      //
-      // expect(waitFor(() => finished)).resolves.toBe(true);
-      //
-      // ...however it also causes the test to fail against React 17.
-      // eslint-disable-next-line testing-library/await-async-utils
-      expect(finished).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        // TODO(fixme): The following line fixes the RTL lint rule error:
+        //
+        // expect(waitFor(() => onErrorCalled)).resolves.toBe(true);
+        //
+        // ...however it also causes the test to fail against React 17.
+        // eslint-disable-next-line testing-library/await-async-utils
+        expect(onErrorCalled).toBe(true);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        // TODO(fixme): The following line fixes the RTL lint rule error:
+        //
+        // expect(waitFor(() => finished)).resolves.toBe(true);
+        //
+        // ...however it also causes the test to fail against React 17.
+        // eslint-disable-next-line testing-library/await-async-utils
+        expect(finished).toBe(true);
+      },
+      { interval: 1 }
+    );
   });
 });
diff --git a/src/react/components/__tests__/client/Query.test.tsx b/src/react/components/__tests__/client/Query.test.tsx
--- a/src/react/components/__tests__/client/Query.test.tsx
+++ b/src/react/components/__tests__/client/Query.test.tsx
@@ -1,17 +1,15 @@
-import React from 'react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-import { render, screen, waitFor } from '@testing-library/react';
-
-import { ApolloClient, NetworkStatus } from '../../../../core';
-import { ApolloError } from '../../../../errors';
-import { ApolloLink } from '../../../../link/core';
-import { InMemoryCache } from '../../../../cache';
-import { ApolloProvider } from '../../../context';
-import { itAsync, MockedProvider, mockSingleLink } from '../../../../testing';
-import { Query } from '../../Query';
-
-const IS_REACT_18 = React.version.startsWith('18');
+import React from "react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+import { render, screen, waitFor } from "@testing-library/react";
+
+import { ApolloClient, NetworkStatus } from "../../../../core";
+import { ApolloError } from "../../../../errors";
+import { ApolloLink } from "../../../../link/core";
+import { InMemoryCache } from "../../../../cache";
+import { ApolloProvider } from "../../../context";
+import { itAsync, MockedProvider, mockSingleLink } from "../../../../testing";
+import { Query } from "../../Query";
 
 const allPeopleQuery: DocumentNode = gql`
   query people {
@@ -30,7 +28,7 @@ interface Data {
 }
 
 const allPeopleData: Data = {
-  allPeople: { people: [{ name: 'Luke Skywalker' }] },
+  allPeople: { people: [{ name: "Luke Skywalker" }] },
 };
 const allPeopleMocks = [
   {
@@ -41,8 +39,8 @@ const allPeopleMocks = [
 
 const AllPeopleQuery = Query;
 
-describe('Query component', () => {
-  itAsync('calls the children prop', (resolve, reject) => {
+describe("Query component", () => {
+  itAsync("calls the children prop", (resolve, reject) => {
     let finished = false;
     const link = mockSingleLink({
       request: { query: allPeopleQuery },
@@ -113,11 +111,11 @@ describe('Query component', () => {
     );
 
     waitFor(() => {
-      expect(finished).toBe(true)
+      expect(finished).toBe(true);
     }).then(resolve, reject);
   });
 
-  it('renders using the children prop', async () => {
+  it("renders using the children prop", async () => {
     const Component = () => (
       <Query query={allPeopleQuery}>{(_: any) => <div>test</div>}</Query>
     );
@@ -129,11 +127,11 @@ describe('Query component', () => {
     );
 
     await waitFor(() => {
-      expect(screen.getByText('test')).toBeTruthy();
+      expect(screen.getByText("test")).toBeTruthy();
     });
   });
 
-  describe('result provides', () => {
+  describe("result provides", () => {
     let consoleWarn = console.warn;
     beforeAll(() => {
       console.warn = () => null;
@@ -143,7 +141,7 @@ describe('Query component', () => {
       console.warn = consoleWarn;
     });
 
-    itAsync('client', (resolve, reject) => {
+    itAsync("client", (resolve, reject) => {
       let count = 0;
       const queryWithVariables: DocumentNode = gql`
         query people($first: Int) {
@@ -197,12 +195,12 @@ describe('Query component', () => {
       }).then(resolve, reject);
     });
 
-    itAsync('error', (resolve, reject) => {
+    itAsync("error", (resolve, reject) => {
       let finished = false;
       const mockError = [
         {
           request: { query: allPeopleQuery },
-          error: new Error('error occurred'),
+          error: new Error("error occurred"),
         },
       ];
 
@@ -213,9 +211,7 @@ describe('Query component', () => {
               return null;
             }
             try {
-              expect(result.error).toEqual(
-                new Error('error occurred')
-              );
+              expect(result.error).toEqual(new Error("error occurred"));
               finished = true;
             } catch (error) {
               reject(error);
@@ -236,7 +232,7 @@ describe('Query component', () => {
       }).then(resolve, reject);
     });
 
-    itAsync('refetch', (resolve, reject) => {
+    itAsync("refetch", (resolve, reject) => {
       const queryRefetch: DocumentNode = gql`
         query people($first: Int) {
           allPeople(first: $first) {
@@ -247,9 +243,9 @@ describe('Query component', () => {
         }
       `;
 
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
-      const data3 = { allPeople: { people: [{ name: 'Darth Vader' }] } };
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
+      const data3 = { allPeople: { people: [{ name: "Darth Vader" }] } };
 
       const refetchVariables = {
         first: 1,
@@ -293,11 +289,7 @@ describe('Query component', () => {
               }
               if (count === 3) {
                 // second data
-                if (IS_REACT_18) {
-                  expect(data).toEqual(data3);
-                } else {
-                  expect(data).toEqual(data2);
-                }
+                expect(data).toEqual(data2);
               }
               if (count === 5) {
                 // third data
@@ -338,17 +330,13 @@ describe('Query component', () => {
       );
 
       waitFor(() => {
-        if (IS_REACT_18) {
-          expect(count).toBe(4);
-        } else {
-          expect(count).toBe(6);
-        }
+        expect(count).toBe(6);
       }).then(resolve, reject);
     });
 
-    itAsync('fetchMore', (resolve, reject) => {
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
+    itAsync("fetchMore", (resolve, reject) => {
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
 
       const variables = {
         first: 2,
@@ -425,10 +413,10 @@ describe('Query component', () => {
       waitFor(() => expect(count).toBe(2)).then(resolve, reject);
     });
 
-    itAsync('startPolling', (resolve, reject) => {
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
-      const data3 = { allPeople: { people: [{ name: 'Darth Vader' }] } };
+    itAsync("startPolling", (resolve, reject) => {
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
+      const data3 = { allPeople: { people: [{ name: "Darth Vader" }] } };
 
       const mocks = [
         {
@@ -490,10 +478,10 @@ describe('Query component', () => {
       waitFor(() => expect(count).toBe(3)).then(resolve, reject);
     });
 
-    itAsync('stopPolling', (resolve, reject) => {
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
-      const data3 = { allPeople: { people: [{ name: 'Darth Vader' }] } };
+    itAsync("stopPolling", (resolve, reject) => {
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
+      const data3 = { allPeople: { people: [{ name: "Darth Vader" }] } };
 
       const mocks = [
         {
@@ -541,9 +529,9 @@ describe('Query component', () => {
       waitFor(() => expect(count).toBe(POLL_COUNT)).then(resolve, reject);
     });
 
-    itAsync('updateQuery', (resolve, reject) => {
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
+    itAsync("updateQuery", (resolve, reject) => {
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
       const variables = {
         first: 2,
       };
@@ -604,11 +592,11 @@ describe('Query component', () => {
     });
   });
 
-  describe('props allow', () => {
-    it('custom fetch-policy', async () => {
+  describe("props allow", () => {
+    it("custom fetch-policy", async () => {
       let count = 0;
       const Component = () => (
-        <Query query={allPeopleQuery} fetchPolicy={'cache-only'}>
+        <Query query={allPeopleQuery} fetchPolicy={"cache-only"}>
           {(result: any) => {
             if (!result.loading) {
               expect(result.networkStatus).toBe(NetworkStatus.ready);
@@ -630,7 +618,7 @@ describe('Query component', () => {
       });
     });
 
-    it('default fetch-policy', async () => {
+    it("default fetch-policy", async () => {
       let count = 0;
       const Component = () => (
         <Query query={allPeopleQuery}>
@@ -646,7 +634,7 @@ describe('Query component', () => {
 
       render(
         <MockedProvider
-          defaultOptions={{ watchQuery: { fetchPolicy: 'cache-only' } }}
+          defaultOptions={{ watchQuery: { fetchPolicy: "cache-only" } }}
           mocks={allPeopleMocks}
         >
           <Component />
@@ -658,9 +646,9 @@ describe('Query component', () => {
       });
     });
 
-    itAsync('notifyOnNetworkStatusChange', (resolve, reject) => {
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
+    itAsync("notifyOnNetworkStatusChange", (resolve, reject) => {
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
 
       const mocks = [
         {
@@ -688,11 +676,7 @@ describe('Query component', () => {
                 });
               }
               if (count === 2) {
-                if (IS_REACT_18) {
-                  expect(result.loading).toBeFalsy();
-                } else {
-                  expect(result.loading).toBeTruthy();
-                }
+                expect(result.loading).toBeTruthy();
               }
               if (count === 3) {
                 expect(result.loading).toBeFalsy();
@@ -714,18 +698,14 @@ describe('Query component', () => {
       );
 
       waitFor(() => {
-        if (IS_REACT_18) {
-          expect(count).toBe(3)
-        } else {
-          expect(count).toBe(4)
-        }
+        expect(count).toBe(4);
       }).then(resolve, reject);
     });
 
-    itAsync('pollInterval', (resolve, reject) => {
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
-      const data3 = { allPeople: { people: [{ name: 'Darth Vader' }] } };
+    itAsync("pollInterval", (resolve, reject) => {
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
+      const data3 = { allPeople: { people: [{ name: "Darth Vader" }] } };
 
       const mocks = [
         {
@@ -775,7 +755,7 @@ describe('Query component', () => {
       waitFor(() => expect(count).toBe(POLL_COUNT)).then(resolve, reject);
     });
 
-    itAsync('skip', (resolve, reject) => {
+    itAsync("skip", (resolve, reject) => {
       let finished = false;
       const Component = () => (
         <Query query={allPeopleQuery} skip>
@@ -804,7 +784,7 @@ describe('Query component', () => {
       }).then(resolve, reject);
     });
 
-    it('onCompleted with data', async () => {
+    it("onCompleted with data", async () => {
       const query = gql`
         query people($first: Int) {
           allPeople(first: $first) {
@@ -815,8 +795,8 @@ describe('Query component', () => {
         }
       `;
 
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
       const mocks = [
         {
           request: { query, variables: { first: 1 } },
@@ -878,9 +858,9 @@ describe('Query component', () => {
       });
     });
 
-    itAsync('onError with data', (resolve, reject) => {
+    itAsync("onError with data", (resolve, reject) => {
       let finished = false;
-      const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
 
       const mocks = [
         {
@@ -919,8 +899,8 @@ describe('Query component', () => {
     });
   });
 
-  describe('props disallow', () => {
-    it('Mutation provided as query', () => {
+  describe("props disallow", () => {
+    it("Mutation provided as query", () => {
       const mutation = gql`
         mutation submitRepository {
           submitRepository(repoFullName: "apollographql/apollo-client") {
@@ -939,14 +919,14 @@ describe('Query component', () => {
           </MockedProvider>
         );
       }).toThrowError(
-        'Running a Query requires a graphql Query, but a Mutation was used ' +
-          'instead.'
+        "Running a Query requires a graphql Query, but a Mutation was used " +
+          "instead."
       );
 
       console.error = errorLogger;
     });
 
-    it('Subscription provided as query', () => {
+    it("Subscription provided as query", () => {
       const subscription = gql`
         subscription onCommentAdded($repoFullName: String!) {
           commentAdded(repoFullName: $repoFullName) {
@@ -966,19 +946,19 @@ describe('Query component', () => {
           </MockedProvider>
         );
       }).toThrowError(
-        'Running a Query requires a graphql Query, but a Subscription was ' +
-          'used instead.'
+        "Running a Query requires a graphql Query, but a Subscription was " +
+          "used instead."
       );
 
       console.error = errorLogger;
     });
 
-    itAsync('onCompleted with error', (resolve, reject) => {
+    itAsync("onCompleted with error", (resolve, reject) => {
       let finished = false;
       const mockError = [
         {
           request: { query: allPeopleQuery },
-          error: new Error('error occurred'),
+          error: new Error("error occurred"),
         },
       ];
 
@@ -1007,9 +987,9 @@ describe('Query component', () => {
       }).then(resolve, reject);
     });
 
-    it('onError with error', async () => {
+    it("onError with error", async () => {
       let finished = false;
-      const error = new Error('error occurred');
+      const error = new Error("error occurred");
       const mockError = [
         {
           request: { query: allPeopleQuery },
@@ -1042,8 +1022,8 @@ describe('Query component', () => {
     });
   });
 
-  describe('should update', () => {
-    itAsync('if props change', (resolve, reject) => {
+  describe("should update", () => {
+    itAsync("if props change", (resolve, reject) => {
       const query = gql`
         query people($first: Int) {
           allPeople(first: $first) {
@@ -1054,8 +1034,8 @@ describe('Query component', () => {
         }
       `;
 
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
       const mocks = [
         {
           request: { query, variables: { first: 1 } },
@@ -1134,7 +1114,7 @@ describe('Query component', () => {
       waitFor(() => expect(count).toBe(4)).then(resolve, reject);
     });
 
-    itAsync('if the query changes', (resolve, reject) => {
+    itAsync("if the query changes", (resolve, reject) => {
       const query1 = allPeopleQuery;
       const query2 = gql`
         query people {
@@ -1147,8 +1127,8 @@ describe('Query component', () => {
         }
       `;
 
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo', id: '1' }] } };
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo", id: "1" }] } };
       const mocks = [
         {
           request: { query: query1 },
@@ -1211,7 +1191,7 @@ describe('Query component', () => {
       waitFor(() => expect(count).toBe(2)).then(resolve, reject);
     });
 
-    it('with data while loading', async () => {
+    it("with data while loading", async () => {
       const query = gql`
         query people($first: Int) {
           allPeople(first: $first) {
@@ -1224,11 +1204,11 @@ describe('Query component', () => {
 
       const data1 = {
         allPeople: {
-          people: [{ name: 'Luke Skywalker' }],
+          people: [{ name: "Luke Skywalker" }],
         },
       };
       const data2 = {
-        allPeople: { people: [{ name: 'Han Solo' }] },
+        allPeople: { people: [{ name: "Han Solo" }] },
       };
       const mocks = [
         {
@@ -1274,7 +1254,9 @@ describe('Query component', () => {
                     case 3:
                       expect(result.loading).toBe(true);
                       expect(result.data).toBeUndefined();
-                      expect(result.networkStatus).toBe(NetworkStatus.setVariables);
+                      expect(result.networkStatus).toBe(
+                        NetworkStatus.setVariables
+                      );
                       break;
                     case 4:
                       expect(result.loading).toBe(false);
@@ -1299,133 +1281,128 @@ describe('Query component', () => {
       );
     });
 
-    itAsync('should update if a manual `refetch` is triggered after a state change', (resolve, reject) => {
-      const query: DocumentNode = gql`
-        query {
-          allPeople {
-            people {
-              name
+    itAsync(
+      "should update if a manual `refetch` is triggered after a state change",
+      (resolve, reject) => {
+        const query: DocumentNode = gql`
+          query {
+            allPeople {
+              people {
+                name
+              }
             }
           }
-        }
-      `;
+        `;
 
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+        const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
 
-      const link = mockSingleLink(
-        {
-          request: { query },
-          result: { data: data1 },
-        },
-        {
-          request: { query },
-          result: { data: data1 },
-        },
-        {
-          request: { query },
-          result: { data: data1 },
-        }
-      );
+        const link = mockSingleLink(
+          {
+            request: { query },
+            result: { data: data1 },
+          },
+          {
+            request: { query },
+            result: { data: data1 },
+          },
+          {
+            request: { query },
+            result: { data: data1 },
+          }
+        );
 
-      const client = new ApolloClient({
-        link,
-        cache: new InMemoryCache({ addTypename: false }),
-      });
+        const client = new ApolloClient({
+          link,
+          cache: new InMemoryCache({ addTypename: false }),
+        });
 
-      let count = 0;
+        let count = 0;
 
-      class SomeComponent extends React.Component {
-        constructor(props: any) {
-          super(props);
-          this.state = {
-            open: false,
-          };
-          this.toggle = this.toggle.bind(this);
-        }
+        class SomeComponent extends React.Component {
+          constructor(props: any) {
+            super(props);
+            this.state = {
+              open: false,
+            };
+            this.toggle = this.toggle.bind(this);
+          }
 
-        toggle() {
-          this.setState((prevState: any) => ({
-            open: !prevState.open,
-          }));
-        }
+          toggle() {
+            this.setState((prevState: any) => ({
+              open: !prevState.open,
+            }));
+          }
 
-        render() {
-          const { open } = this.state as any;
-          return (
-            <Query client={client} query={query} notifyOnNetworkStatusChange>
-              {(props: any) => {
-                try {
-                  switch (count) {
-                    case 0:
-                      // Loading first response
-                      expect(props.loading).toBe(true);
-                      expect(open).toBe(false);
-                      break;
-                    case 1:
-                      // First response loaded, change state value
-                      expect(props.data).toEqual(data1);
-                      expect(open).toBe(false);
-                      setTimeout(() => {
-                        this.toggle();
-                      });
-                      break;
-                    case 2:
-                      // State value changed, fire a refetch
-                      expect(open).toBe(true);
-                      setTimeout(() => {
-                        props.refetch();
-                      });
-                      break;
-                    case 3:
-                      // Second response loading
-                      if (IS_REACT_18) {
-                        expect(props.loading).toBe(false);
-                      } else {
+          render() {
+            const { open } = this.state as any;
+            return (
+              <Query client={client} query={query} notifyOnNetworkStatusChange>
+                {(props: any) => {
+                  try {
+                    switch (count) {
+                      case 0:
+                        // Loading first response
                         expect(props.loading).toBe(true);
-                      }
-                      break;
-                    case 4:
-                      // Second response received, fire another refetch
-                      expect(props.data).toEqual(data1);
-                      setTimeout(() => {
-                        props.refetch();
-                      });
-                      break;
-                    case 5:
-                      // Third response loading
-                      expect(props.loading).toBe(true);
-                      break;
-                    case 6:
-                      // Third response received
-                      expect(props.data).toEqual(data1);
-                      break;
-                    default:
-                      reject('Unknown count');
+                        expect(open).toBe(false);
+                        break;
+                      case 1:
+                        // First response loaded, change state value
+                        expect(props.data).toEqual(data1);
+                        expect(open).toBe(false);
+                        setTimeout(() => {
+                          this.toggle();
+                        });
+                        break;
+                      case 2:
+                        // State value changed, fire a refetch
+                        expect(open).toBe(true);
+                        setTimeout(() => {
+                          props.refetch();
+                        });
+                        break;
+                      case 3:
+                        // Second response loading
+                        expect(props.loading).toBe(true);
+                        break;
+                      case 4:
+                        // Second response received, fire another refetch
+                        expect(props.data).toEqual(data1);
+                        setTimeout(() => {
+                          props.refetch();
+                        });
+                        break;
+                      case 5:
+                        // Third response loading
+                        expect(props.loading).toBe(true);
+                        break;
+                      case 6:
+                        // Third response received
+                        expect(props.data).toEqual(data1);
+                        break;
+                      default:
+                        reject("Unknown count");
+                    }
+                    count += 1;
+                  } catch (error) {
+                    reject(error);
                   }
-                  count += 1;
-                } catch (error) {
-                  reject(error);
-                }
-                return null;
-              }}
-            </Query>
-          );
+                  return null;
+                }}
+              </Query>
+            );
+          }
         }
-      }
 
-      render(<SomeComponent />);
+        render(<SomeComponent />);
 
-      waitFor(() => {
-        if (IS_REACT_18) {
-          expect(count).toBe(4)
-        } else {
-          expect(count).toBe(7)
-        }
-      }).then(resolve, reject);
-    });
+        waitFor(() => {
+          expect(count).toBe(7);
+        }).then(resolve, reject);
+      }
+    );
   });
 
-  it('should error if the query changes type to a subscription', async () => {
+  it("should error if the query changes type to a subscription", async () => {
     let finished = false;
     const subscription = gql`
       subscription onCommentAdded($repoFullName: String!) {
@@ -1445,8 +1422,8 @@ describe('Query component', () => {
 
       componentDidCatch(error: any) {
         const expectedError = new Error(
-          'Running a Query requires a graphql Query, but a Subscription was ' +
-            'used instead.'
+          "Running a Query requires a graphql Query, but a Subscription was " +
+            "used instead."
         );
         expect(error).toEqual(expectedError);
         finished = true;
@@ -1472,13 +1449,16 @@ describe('Query component', () => {
       </MockedProvider>
     );
 
-    await waitFor(() => {
-      expect(finished).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(finished).toBe(true);
+      },
+      { interval: 1 }
+    );
     console.error = errorLog;
   });
 
-  it('should be able to refetch after there was a network error', async () => {
+  it("should be able to refetch after there was a network error", async () => {
     const query: DocumentNode = gql`
       query somethingelse {
         allPeople(first: 1) {
@@ -1489,12 +1469,12 @@ describe('Query component', () => {
       }
     `;
 
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const dataTwo = { allPeople: { people: [{ name: 'Princess Leia' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+    const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } };
     const link = mockSingleLink(
       { request: { query }, result: { data } },
-      { request: { query }, error: new Error('This is an error!') },
-      { request: { query }, result: { data: dataTwo }, delay: 10 },
+      { request: { query }, error: new Error("This is an error!") },
+      { request: { query }, result: { data: dataTwo }, delay: 10 }
     );
     const client = new ApolloClient({
       link,
@@ -1502,6 +1482,7 @@ describe('Query component', () => {
     });
 
     let count = 0;
+    let testFailures: any[] = [];
     const noop = () => null;
 
     const AllPeopleQuery2 = Query;
@@ -1519,27 +1500,21 @@ describe('Query component', () => {
                 case 1:
                   // First result is loaded, run a refetch to get the second result
                   // which is an error.
-                  expect(result.data.allPeople).toEqual(
-                    data.allPeople
-                  );
+                  expect(result.data.allPeople).toEqual(data.allPeople);
                   setTimeout(() => {
                     result.refetch().then(() => {
-                      fail('Expected error value on first refetch.');
+                      fail("Expected error value on first refetch.");
                     }, noop);
                   }, 0);
                   break;
                 case 2:
                   // Waiting for the second result to load
-                  if (IS_REACT_18) {
-                    expect(result.loading).toBe(false);
-                  } else {
-                    expect(result.loading).toBe(true);
-                  }
+                  expect(result.loading).toBe(true);
                   break;
                 case 3:
                   setTimeout(() => {
                     result.refetch().catch(() => {
-                      fail('Expected good data on second refetch.');
+                      fail("Expected good data on second refetch.");
                     });
                   }, 0);
                   // fallthrough
@@ -1558,10 +1533,13 @@ describe('Query component', () => {
                   expect(result.data.allPeople).toEqual(dataTwo.allPeople);
                   break;
                 default:
-                  throw new Error('Unexpected fall through');
+                  throw new Error("Unexpected fall through");
               }
             } catch (e) {
-              fail(e);
+              // if we throw the error inside the component,
+              // we will get more rerenders in the test, but the `expect` error
+              // might not propagate anyways
+              testFailures.push(e);
             }
             return null;
           }}
@@ -1576,16 +1554,15 @@ describe('Query component', () => {
     );
 
     await waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toBe(3)
-      } else {
-        expect(count).toBe(6)
+      if (testFailures.length > 0) {
+        throw testFailures[0];
       }
+      expect(count).toBe(6);
     });
   });
 
   itAsync(
-    'should not persist previous result errors when a subsequent valid result is received',
+    "should not persist previous result errors when a subsequent valid result is received",
     (resolve, reject) => {
       const query: DocumentNode = gql`
         query somethingelse($variable: Boolean) {
@@ -1597,7 +1574,7 @@ describe('Query component', () => {
         }
       `;
 
-      const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
       const variableGood = { variable: true };
       const variableBad = { variable: false };
 
@@ -1617,7 +1594,7 @@ describe('Query component', () => {
             variables: variableBad,
           },
           result: {
-            errors: [new Error('This is an error!')],
+            errors: [new Error("This is an error!")],
           },
         },
         {
@@ -1649,11 +1626,7 @@ describe('Query component', () => {
               // Change query variables to trigger bad result.
               setTimeout(() => {
                 render(
-                  <Query
-                    client={client}
-                    query={query}
-                    variables={variableBad}
-                  >
+                  <Query client={client} query={query} variables={variableBad}>
                     {(result: any) => {
                       return <DummyComp id="dummyId" {...result} />;
                     }}
@@ -1670,11 +1643,7 @@ describe('Query component', () => {
               // Change query variables to trigger a good result.
               setTimeout(() => {
                 render(
-                  <Query
-                    client={client}
-                    query={query}
-                    variables={variableGood}
-                  >
+                  <Query client={client} query={query} variables={variableGood}>
                     {(result: any) => {
                       return <DummyComp id="dummyId" {...result} />;
                     }}
@@ -1688,7 +1657,7 @@ describe('Query component', () => {
               expect(props.data.allPeople).toBeTruthy();
               break;
             default:
-              reject('Unknown count');
+              reject("Unknown count");
           }
         } catch (error) {
           reject(error);
@@ -1708,7 +1677,7 @@ describe('Query component', () => {
     }
   );
 
-  it('should support mixing setState and onCompleted', async () => {
+  it("should support mixing setState and onCompleted", async () => {
     const query = gql`
       query people($first: Int) {
         allPeople(first: $first) {
@@ -1719,8 +1688,8 @@ describe('Query component', () => {
       }
     `;
 
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+    const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
     const mocks = [
       {
         request: { query, variables: { first: 1 } },
@@ -1808,61 +1777,63 @@ describe('Query component', () => {
     });
   });
 
-  itAsync('should not repeatedly call onError if setState in it', (resolve, reject) => {
-    const mockError = [
-      {
-        request: { query: allPeopleQuery, variables: { first: 1 } },
-        error: new Error('error occurred'),
-      },
-    ];
-
-    let unmount: any;
-    let onErrorCallCount = 0;
-    class Component extends React.Component {
-      state = {
-        variables: {
-          first: 1,
+  itAsync(
+    "should not repeatedly call onError if setState in it",
+    (resolve, reject) => {
+      const mockError = [
+        {
+          request: { query: allPeopleQuery, variables: { first: 1 } },
+          error: new Error("error occurred"),
         },
-      };
-      onError = () => {
-        onErrorCallCount += 1;
-        this.setState({ causeUpdate: true });
-      };
-      render() {
-        return (
-          <Query
-            query={allPeopleQuery}
-            variables={this.state.variables}
-            onError={this.onError}
-          >
-            {({ loading }: any) => {
-              if (!loading) {
-                setTimeout(unmount);
-              }
-              return null;
-            }}
-          </Query>
-        );
+      ];
+
+      let unmount: any;
+      let onErrorCallCount = 0;
+      class Component extends React.Component {
+        state = {
+          variables: {
+            first: 1,
+          },
+        };
+        onError = () => {
+          onErrorCallCount += 1;
+          this.setState({ causeUpdate: true });
+        };
+        render() {
+          return (
+            <Query
+              query={allPeopleQuery}
+              variables={this.state.variables}
+              onError={this.onError}
+            >
+              {({ loading }: any) => {
+                if (!loading) {
+                  setTimeout(unmount);
+                }
+                return null;
+              }}
+            </Query>
+          );
+        }
       }
-    }
 
-    unmount = render(
-      <MockedProvider mocks={mockError} addTypename={false}>
-        <Component />
-      </MockedProvider>
-    ).unmount;
+      unmount = render(
+        <MockedProvider mocks={mockError} addTypename={false}>
+          <Component />
+        </MockedProvider>
+      ).unmount;
 
-    waitFor(() => {
-      expect(onErrorCallCount).toBe(1);
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(onErrorCallCount).toBe(1);
+      }).then(resolve, reject);
+    }
+  );
 
-  describe('Partial refetching', () => {
+  describe("Partial refetching", () => {
     let errorSpy!: ReturnType<typeof jest.spyOn>;
 
     beforeEach(() => {
-      errorSpy = jest.spyOn(console, 'error')
-        .mockImplementation(() => {});
+      errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
     });
 
     afterAll(() => {
@@ -1873,10 +1844,10 @@ describe('Query component', () => {
     // error calls no matter what I try and I do not want to care about it
     // anymore :)
     itAsync.skip(
-      'should attempt a refetch when the query result was marked as being ' +
-        'partial, the returned data was reset to an empty Object by the ' +
-        'Apollo Client QueryManager (due to a cache miss), and the ' +
-        '`partialRefetch` prop is `true`',
+      "should attempt a refetch when the query result was marked as being " +
+        "partial, the returned data was reset to an empty Object by the " +
+        "Apollo Client QueryManager (due to a cache miss), and the " +
+        "`partialRefetch` prop is `true`",
       (resolve, reject) => {
         const allPeopleQuery: DocumentNode = gql`
           query people {
@@ -1890,7 +1861,7 @@ describe('Query component', () => {
 
         let count = 0;
         const allPeopleData = {
-          allPeople: { people: [{ name: 'Luke Skywalker' }] },
+          allPeople: { people: [{ name: "Luke Skywalker" }] },
         };
         const query = allPeopleQuery;
         const link = mockSingleLink(
@@ -1912,7 +1883,7 @@ describe('Query component', () => {
                 if (!loading) {
                   expect(data).toEqual(allPeopleData);
                   expect(errorSpy).toHaveBeenCalledTimes(1);
-                  expect(errorSpy.mock.calls[0][0]).toMatch('Missing field');
+                  expect(errorSpy.mock.calls[0][0]).toMatch("Missing field");
                 }
               } catch (err) {
                 reject(err);
@@ -1935,8 +1906,8 @@ describe('Query component', () => {
     );
 
     itAsync.skip(
-      'should not refetch when an empty partial is returned if the ' +
-        '`partialRefetch` prop is false/not set',
+      "should not refetch when an empty partial is returned if the " +
+        "`partialRefetch` prop is false/not set",
       (resolve, reject) => {
         let finished = false;
         const query = allPeopleQuery;
@@ -1967,16 +1938,16 @@ describe('Query component', () => {
           </ApolloProvider>
         );
 
-       waitFor(() => {
-         expect(finished).toBe(true);
-       }).then(resolve, reject);
+        waitFor(() => {
+          expect(finished).toBe(true);
+        }).then(resolve, reject);
       }
     );
   });
 
   itAsync(
-    'should keep data for a `Query` component using `no-cache` when the ' +
-      'tree is re-rendered',
+    "should keep data for a `Query` component using `no-cache` when the " +
+      "tree is re-rendered",
     (resolve, reject) => {
       const query1 = allPeopleQuery;
 
@@ -1998,14 +1969,14 @@ describe('Query component', () => {
 
       const allThingsData: ThingData = {
         allThings: {
-          thing: [{ description: 'Thing 1' }, { description: 'Thing 2' }],
+          thing: [{ description: "Thing 1" }, { description: "Thing 2" }],
         },
       };
 
       const link = mockSingleLink(
         { request: { query: query1 }, result: { data: allPeopleData } },
         { request: { query: query2 }, result: { data: allThingsData } },
-        { request: { query: query1 }, result: { data: allPeopleData } },
+        { request: { query: query1 }, result: { data: allPeopleData } }
       );
 
       const client = new ApolloClient({
@@ -2056,7 +2027,7 @@ describe('Query component', () => {
     }
   );
 
-  describe('Return partial data', () => {
+  describe("Return partial data", () => {
     const origConsoleWarn = console.warn;
 
     beforeAll(() => {
@@ -2067,7 +2038,7 @@ describe('Query component', () => {
       console.warn = origConsoleWarn;
     });
 
-    it('should not return partial cache data when `returnPartialData` is false', async () => {
+    it("should not return partial cache data when `returnPartialData` is false", async () => {
       let finished = false;
       const cache = new InMemoryCache();
       const client = new ApolloClient({
@@ -2093,15 +2064,15 @@ describe('Query component', () => {
         data: {
           cars: [
             {
-              __typename: 'Car',
-              make: 'Ford',
-              model: 'Mustang',
-              vin: 'PONY123',
+              __typename: "Car",
+              make: "Ford",
+              model: "Mustang",
+              vin: "PONY123",
               repairs: [
                 {
-                  __typename: 'Repair',
-                  date: '2019-05-08',
-                  description: 'Could not get after it.',
+                  __typename: "Repair",
+                  date: "2019-05-08",
+                  description: "Could not get after it.",
                 },
               ],
             },
@@ -2135,11 +2106,11 @@ describe('Query component', () => {
       render(<App />);
 
       await waitFor(() => {
-        expect(finished).toBe(true)
-      })
+        expect(finished).toBe(true);
+      });
     });
 
-    it('should return partial cache data when `returnPartialData` is true', async () => {
+    it("should return partial cache data when `returnPartialData` is true", async () => {
       let finished = false;
       const cache = new InMemoryCache();
       const client = new ApolloClient({
@@ -2165,15 +2136,15 @@ describe('Query component', () => {
         data: {
           cars: [
             {
-              __typename: 'Car',
-              make: 'Ford',
-              model: 'Mustang',
-              vin: 'PONY123',
+              __typename: "Car",
+              make: "Ford",
+              model: "Mustang",
+              vin: "PONY123",
               repairs: [
                 {
-                  __typename: 'Repair',
-                  date: '2019-05-08',
-                  description: 'Could not get after it.',
+                  __typename: "Repair",
+                  date: "2019-05-08",
+                  description: "Could not get after it.",
                 },
               ],
             },
@@ -2199,11 +2170,11 @@ describe('Query component', () => {
               expect(data).toEqual({
                 cars: [
                   {
-                    __typename: 'Car',
+                    __typename: "Car",
                     repairs: [
                       {
-                        __typename: 'Repair',
-                        date: '2019-05-08',
+                        __typename: "Repair",
+                        date: "2019-05-08",
                       },
                     ],
                   },
diff --git a/src/react/components/__tests__/client/Subscription.test.tsx b/src/react/components/__tests__/client/Subscription.test.tsx
--- a/src/react/components/__tests__/client/Subscription.test.tsx
+++ b/src/react/components/__tests__/client/Subscription.test.tsx
@@ -1,21 +1,21 @@
-import React from 'react';
-import gql from 'graphql-tag';
-import { render, waitFor } from '@testing-library/react';
+import React from "react";
+import gql from "graphql-tag";
+import { render, waitFor } from "@testing-library/react";
 
-import { ApolloClient } from '../../../../core';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { ApolloProvider } from '../../../context';
-import { ApolloLink, Operation } from '../../../../link/core';
-import { itAsync, MockSubscriptionLink } from '../../../../testing';
-import { Subscription } from '../../Subscription';
+import { ApolloClient } from "../../../../core";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { ApolloProvider } from "../../../context";
+import { ApolloLink, Operation } from "../../../../link/core";
+import { itAsync, MockSubscriptionLink } from "../../../../testing";
+import { Subscription } from "../../Subscription";
 
 const results = [
-  'Luke Skywalker',
-  'Han Solo',
-  'Darth Vader',
-  'Leia Skywalker'
-].map(name => ({
-  result: { data: { user: { name } } }
+  "Luke Skywalker",
+  "Han Solo",
+  "Darth Vader",
+  "Leia Skywalker",
+].map((name) => ({
+  result: { data: { user: { name } } },
 }));
 
 beforeEach(() => {
@@ -34,10 +34,10 @@ const cache = new Cache({ addTypename: false });
 const link = new MockSubscriptionLink();
 const client = new ApolloClient({
   link,
-  cache
+  cache,
 });
 
-itAsync('executes the subscription', (resolve, reject) => {
+itAsync("executes the subscription", (resolve, reject) => {
   let renderCount = 0;
   const Component = () => (
     <Subscription subscription={subscription}>
@@ -88,7 +88,7 @@ itAsync('executes the subscription', (resolve, reject) => {
   waitFor(() => expect(renderCount).toBe(5)).then(resolve, reject);
 });
 
-it('calls onData if given', async () => {
+it("calls onData if given", async () => {
   let count = 0;
 
   const Component = () => (
@@ -117,8 +117,10 @@ it('calls onData if given', async () => {
   await waitFor(() => expect(count).toBe(4));
 });
 
-it('calls onSubscriptionData with deprecation warning if given', async () => {
-  const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+it("calls onSubscriptionData with deprecation warning if given", async () => {
+  const consoleWarnSpy = jest
+    .spyOn(console, "warn")
+    .mockImplementation(() => {});
   let count = 0;
 
   const Component = () => (
@@ -149,12 +151,12 @@ it('calls onSubscriptionData with deprecation warning if given', async () => {
     if (count >= 3) clearInterval(interval);
   }, 10);
 
-  await waitFor(() => expect(count).toBe(4))
+  await waitFor(() => expect(count).toBe(4));
 
   consoleWarnSpy.mockRestore();
 });
 
-it('should call onComplete if specified', async () => {
+it("should call onComplete if specified", async () => {
   let count = 0;
 
   let done = false;
@@ -184,8 +186,10 @@ it('should call onComplete if specified', async () => {
   await waitFor(() => expect(done).toBeTruthy());
 });
 
-it('should call onSubscriptionComplete with deprecation warning if specified', async () => {
-  const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+it("should call onSubscriptionComplete with deprecation warning if specified", async () => {
+  const consoleWarnSpy = jest
+    .spyOn(console, "warn")
+    .mockImplementation(() => {});
   let count = 0;
 
   let done = false;
@@ -222,73 +226,76 @@ it('should call onSubscriptionComplete with deprecation warning if specified', a
   consoleWarnSpy.mockRestore();
 });
 
-itAsync('executes subscription for the variables passed in the props', (resolve, reject) => {
-  const subscriptionWithVariables = gql`
-    subscription UserInfo($name: String) {
-      user(name: $name) {
-        name
+itAsync(
+  "executes subscription for the variables passed in the props",
+  (resolve, reject) => {
+    const subscriptionWithVariables = gql`
+      subscription UserInfo($name: String) {
+        user(name: $name) {
+          name
+        }
       }
-    }
-  `;
+    `;
 
-  const variables = { name: 'Luke Skywalker' };
+    const variables = { name: "Luke Skywalker" };
 
-  class MockSubscriptionLinkOverride extends MockSubscriptionLink {
-    request(req: Operation) {
-      try {
-        expect(req.variables).toEqual(variables);
-      } catch (error) {
-        reject(error);
+    class MockSubscriptionLinkOverride extends MockSubscriptionLink {
+      request(req: Operation) {
+        try {
+          expect(req.variables).toEqual(variables);
+        } catch (error) {
+          reject(error);
+        }
+        return super.request(req);
       }
-      return super.request(req);
     }
-  }
-
-  const mockLink = new MockSubscriptionLinkOverride();
 
-  const mockClient = new ApolloClient({
-    link: mockLink,
-    cache
-  });
+    const mockLink = new MockSubscriptionLinkOverride();
 
-  let count = 0;
+    const mockClient = new ApolloClient({
+      link: mockLink,
+      cache,
+    });
 
-  const Component = () => (
-    <Subscription
-      subscription={subscriptionWithVariables}
-      variables={variables}
-    >
-      {(result: any) => {
-        const { loading, data } = result;
+    let count = 0;
 
-        try {
-          if (count === 0) {
-            expect(loading).toBe(true);
-          } else if (count === 1) {
-            expect(loading).toBe(false);
-            expect(data).toEqual(results[0].result.data);
+    const Component = () => (
+      <Subscription
+        subscription={subscriptionWithVariables}
+        variables={variables}
+      >
+        {(result: any) => {
+          const { loading, data } = result;
+
+          try {
+            if (count === 0) {
+              expect(loading).toBe(true);
+            } else if (count === 1) {
+              expect(loading).toBe(false);
+              expect(data).toEqual(results[0].result.data);
+            }
+          } catch (error) {
+            reject(error);
           }
-        } catch (error) {
-          reject(error);
-        }
-        count++;
-        return null;
-      }}
-    </Subscription>
-  );
+          count++;
+          return null;
+        }}
+      </Subscription>
+    );
 
-  render(
-    <ApolloProvider client={mockClient}>
-      <Component />
-    </ApolloProvider>
-  );
+    render(
+      <ApolloProvider client={mockClient}>
+        <Component />
+      </ApolloProvider>
+    );
 
-  mockLink.simulateResult(results[0]);
+    mockLink.simulateResult(results[0]);
 
-  waitFor(() => expect(count).toBe(2)).then(resolve, reject);
-});
+    waitFor(() => expect(count).toBe(2)).then(resolve, reject);
+  }
+);
 
-itAsync('does not execute if variables have not changed', (resolve, reject) => {
+itAsync("does not execute if variables have not changed", (resolve, reject) => {
   const subscriptionWithVariables = gql`
     subscription UserInfo($name: String) {
       user(name: $name) {
@@ -297,7 +304,7 @@ itAsync('does not execute if variables have not changed', (resolve, reject) => {
     }
   `;
 
-  const name = 'Luke Skywalker';
+  const name = "Luke Skywalker";
 
   class MockSubscriptionLinkOverride extends MockSubscriptionLink {
     request(req: Operation) {
@@ -314,7 +321,7 @@ itAsync('does not execute if variables have not changed', (resolve, reject) => {
 
   const mockClient = new ApolloClient({
     link: mockLink,
-    cache
+    cache,
   });
 
   let count = 0;
@@ -359,7 +366,7 @@ itAsync('does not execute if variables have not changed', (resolve, reject) => {
   waitFor(() => expect(count).toBe(3)).then(resolve, reject);
 });
 
-itAsync('renders an error', (resolve, reject) => {
+itAsync("renders an error", (resolve, reject) => {
   const subscriptionWithVariables = gql`
     subscription UserInfo($name: String) {
       user(name: $name) {
@@ -369,11 +376,11 @@ itAsync('renders an error', (resolve, reject) => {
   `;
 
   const variables = {
-    name: 'Luke Skywalker'
+    name: "Luke Skywalker",
   };
 
   const subscriptionError = {
-    error: new Error('error occurred')
+    error: new Error("error occurred"),
   };
 
   let count = 0;
@@ -390,7 +397,7 @@ itAsync('renders an error', (resolve, reject) => {
             expect(error).toBeUndefined();
           } else if (count === 1) {
             expect(loading).toBe(false);
-            expect(error).toEqual(new Error('error occurred'));
+            expect(error).toEqual(new Error("error occurred"));
             expect(data).toBeUndefined();
           }
         } catch (error) {
@@ -414,19 +421,20 @@ itAsync('renders an error', (resolve, reject) => {
   waitFor(() => expect(count).toBe(2)).then(resolve, reject);
 });
 
-describe('should update', () => {
-  itAsync('if the client changes', (resolve, reject) => {
+describe("should update", () => {
+  it("if the client changes", async () => {
     const link2 = new MockSubscriptionLink();
     const client2 = new ApolloClient({
       link: link2,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
+    let testFailures: any[] = [];
 
     class Component extends React.Component {
       state = {
-        client: client
+        client: client,
       };
 
       render() {
@@ -436,7 +444,7 @@ describe('should update', () => {
               {(result: any) => {
                 const { loading, data } = result;
                 try {
-                  switch (count) {
+                  switch (count++) {
                     case 0:
                       expect(loading).toBeTruthy();
                       expect(data).toBeUndefined();
@@ -445,14 +453,14 @@ describe('should update', () => {
                       setTimeout(() => {
                         this.setState(
                           {
-                            client: client2
+                            client: client2,
                           },
                           () => {
                             link2.simulateResult(results[1]);
                           }
                         );
                       });
-                      // fallthrough
+                    // fallthrough
                     case 2:
                       expect(loading).toBeFalsy();
                       expect(data).toEqual(results[0].result.data);
@@ -465,12 +473,12 @@ describe('should update', () => {
                       expect(loading).toBeFalsy();
                       expect(data).toEqual(results[1].result.data);
                       break;
+                    default:
+                      throw new Error("too many rerenders");
                   }
                 } catch (error) {
-                  reject(error);
+                  testFailures.push(error);
                 }
-
-                count++;
                 return null;
               }}
             </Subscription>
@@ -483,10 +491,15 @@ describe('should update', () => {
 
     link.simulateResult(results[0]);
 
-    waitFor(() => expect(count).toBe(5)).then(resolve, reject);
+    await waitFor(() => {
+      if (testFailures.length > 0) {
+        throw testFailures[0];
+      }
+      expect(count).toBe(5);
+    });
   });
 
-  itAsync('if the query changes', (resolve, reject) => {
+  itAsync("if the query changes", (resolve, reject) => {
     const subscriptionHero = gql`
       subscription HeroInfo {
         hero {
@@ -499,30 +512,30 @@ describe('should update', () => {
       result: {
         data: {
           hero: {
-            name: 'Chewie'
-          }
-        }
-      }
+            name: "Chewie",
+          },
+        },
+      },
     };
 
     const userLink = new MockSubscriptionLink();
     const heroLink = new MockSubscriptionLink();
     const linkCombined = new ApolloLink((o, f) => (f ? f(o) : null)).split(
-      ({ operationName }) => operationName === 'HeroInfo',
+      ({ operationName }) => operationName === "HeroInfo",
       heroLink,
       userLink
     );
 
     const mockClient = new ApolloClient({
       link: linkCombined,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
 
     class Component extends React.Component {
       state = {
-        subscription
+        subscription,
       };
 
       render() {
@@ -540,14 +553,14 @@ describe('should update', () => {
                     setTimeout(() => {
                       this.setState(
                         {
-                          subscription: subscriptionHero
+                          subscription: subscriptionHero,
                         },
                         () => {
                           heroLink.simulateResult(heroResult);
                         }
                       );
                     });
-                    // fallthrough
+                  // fallthrough
                   case 2:
                     expect(loading).toBeFalsy();
                     expect(data).toEqual(results[0].result.data);
@@ -583,7 +596,7 @@ describe('should update', () => {
     waitFor(() => expect(count).toBe(5)).then(resolve, reject);
   });
 
-  itAsync('if the variables change', (resolve, reject) => {
+  itAsync("if the variables change", (resolve, reject) => {
     const subscriptionWithVariables = gql`
       subscription UserInfo($name: String) {
         user(name: $name) {
@@ -592,33 +605,33 @@ describe('should update', () => {
       }
     `;
 
-    const variablesLuke = { name: 'Luke Skywalker' };
-    const variablesHan = { name: 'Han Solo' };
+    const variablesLuke = { name: "Luke Skywalker" };
+    const variablesHan = { name: "Han Solo" };
 
     const dataLuke = {
       user: {
-        name: 'Luke Skywalker'
-      }
+        name: "Luke Skywalker",
+      },
     };
 
     const dataHan = {
       user: {
-        name: 'Han Solo'
-      }
+        name: "Han Solo",
+      },
     };
 
     const mockLink = new MockSubscriptionLink();
 
     const mockClient = new ApolloClient({
       link: mockLink,
-      cache
+      cache,
     });
 
     let count = 0;
 
     class Component extends React.Component {
       state = {
-        variables: variablesLuke
+        variables: variablesLuke,
       };
 
       render() {
@@ -639,14 +652,16 @@ describe('should update', () => {
                     setTimeout(() => {
                       this.setState(
                         {
-                          variables: variablesHan
+                          variables: variablesHan,
                         },
                         () => {
-                          mockLink.simulateResult({ result: { data: dataHan } });
+                          mockLink.simulateResult({
+                            result: { data: dataHan },
+                          });
                         }
                       );
                     });
-                    // fallthrough
+                  // fallthrough
                   case 2:
                     expect(loading).toBeFalsy();
                     expect(data).toEqual(dataLuke);
@@ -684,20 +699,20 @@ describe('should update', () => {
   });
 });
 
-describe('should not update', () => {
-  const variablesLuke = { name: 'Luke Skywalker' };
-  const variablesHan = { name: 'Han Solo' };
+describe("should not update", () => {
+  const variablesLuke = { name: "Luke Skywalker" };
+  const variablesHan = { name: "Han Solo" };
 
   const dataLuke = {
     user: {
-      name: 'Luke Skywalker'
-    }
+      name: "Luke Skywalker",
+    },
   };
 
   const dataHan = {
     user: {
-      name: 'Han Solo'
-    }
+      name: "Han Solo",
+    },
   };
 
   class MockSubscriptionLinkOverride extends MockSubscriptionLink {
@@ -708,23 +723,23 @@ describe('should not update', () => {
     }
 
     simulateResult() {
-      if (this.variables.name === 'Luke Skywalker') {
+      if (this.variables.name === "Luke Skywalker") {
         return super.simulateResult({
           result: {
-            data: dataLuke
-          }
+            data: dataLuke,
+          },
         });
-      } else if (this.variables.name === 'Han Solo') {
+      } else if (this.variables.name === "Han Solo") {
         return super.simulateResult({
           result: {
-            data: dataHan
-          }
+            data: dataHan,
+          },
         });
       }
     }
   }
 
-  itAsync('if shouldResubscribe is false', (resolve, reject) => {
+  itAsync("if shouldResubscribe is false", (resolve, reject) => {
     const subscriptionWithVariables = gql`
       subscription UserInfo($name: String) {
         user(name: $name) {
@@ -737,14 +752,14 @@ describe('should not update', () => {
 
     const mockClient = new ApolloClient({
       link: mockLink,
-      cache
+      cache,
     });
 
     let count = 0;
 
     class Component extends React.Component {
       state = {
-        variables: variablesLuke
+        variables: variablesLuke,
       };
 
       render() {
@@ -766,7 +781,7 @@ describe('should not update', () => {
                   setTimeout(() => {
                     this.setState(
                       {
-                        variables: variablesHan
+                        variables: variablesHan,
                       },
                       () => {
                         mockLink.simulateResult();
@@ -800,7 +815,7 @@ describe('should not update', () => {
     waitFor(() => expect(count).toBe(4)).then(resolve, reject);
   });
 
-  itAsync('if shouldResubscribe returns false', (resolve, reject) => {
+  itAsync("if shouldResubscribe returns false", (resolve, reject) => {
     const subscriptionWithVariables = gql`
       subscription UserInfo($name: String) {
         user(name: $name) {
@@ -813,14 +828,14 @@ describe('should not update', () => {
 
     const mockClient = new ApolloClient({
       link: mockLink,
-      cache
+      cache,
     });
 
     let count = 0;
 
     class Component extends React.Component {
       state = {
-        variables: variablesLuke
+        variables: variablesLuke,
       };
 
       render() {
@@ -842,7 +857,7 @@ describe('should not update', () => {
                   setTimeout(() => {
                     this.setState(
                       {
-                        variables: variablesHan
+                        variables: variablesHan,
                       },
                       () => {
                         mockLink.simulateResult();
diff --git a/src/react/components/__tests__/ssr/getDataFromTree.test.tsx b/src/react/components/__tests__/ssr/getDataFromTree.test.tsx
--- a/src/react/components/__tests__/ssr/getDataFromTree.test.tsx
+++ b/src/react/components/__tests__/ssr/getDataFromTree.test.tsx
@@ -1,22 +1,28 @@
 /** @jest-environment node */
-import React from 'react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
+import React from "react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
 
-import { ApolloClient } from '../../../../core';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { ApolloProvider, getApolloContext, ApolloContextValue } from '../../../context';
-import { getDataFromTree } from '../../../ssr';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { Query } from '../../Query';
+import { ApolloClient } from "../../../../core";
+import { InMemoryCache as Cache } from "../../../../cache";
+import {
+  ApolloProvider,
+  getApolloContext,
+  ApolloContextValue,
+} from "../../../context";
+import { getDataFromTree } from "../../../ssr";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { Query } from "../../Query";
 
-describe('SSR', () => {
-  describe('`getDataFromTree`', () => {
-    it('should support passing a root context', () => {
-      const apolloContext = getApolloContext() as unknown as React.Context<ApolloContextValue & { text: string }>;
+describe("SSR", () => {
+  describe("`getDataFromTree`", () => {
+    it("should support passing a root context", () => {
+      const apolloContext = getApolloContext() as unknown as React.Context<
+        ApolloContextValue & { text: string }
+      >;
       class Consumer extends React.Component {
         static contextType = apolloContext;
-        declare context: React.ContextType<typeof apolloContext>
+        declare context: React.ContextType<typeof apolloContext>;
 
         render() {
           return <div>{this.context.text}</div>;
@@ -24,13 +30,13 @@ describe('SSR', () => {
       }
 
       return getDataFromTree(<Consumer />, {
-        text: 'oyez'
-      }).then(html => {
-        expect(html).toEqual('<div>oyez</div>');
+        text: "oyez",
+      }).then((html) => {
+        expect(html).toEqual("<div>oyez</div>");
       });
     });
 
-    it('should run through all of the queries (also defined via Query component) that want SSR', () => {
+    it("should run through all of the queries (also defined via Query component) that want SSR", () => {
       const query = gql`
         {
           currentUser {
@@ -38,15 +44,15 @@ describe('SSR', () => {
           }
         }
       `;
-      const data1 = { currentUser: { firstName: 'James' } };
+      const data1 = { currentUser: { firstName: "James" } };
       const link = mockSingleLink({
         request: { query },
         result: { data: data1 },
-        delay: 50
+        delay: 50,
       });
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Data {
@@ -59,7 +65,7 @@ describe('SSR', () => {
         <Query query={query}>
           {({ data, loading }: { data: Data; loading: boolean }) => (
             <div>
-              {loading || !data ? 'loading' : data.currentUser!.firstName}
+              {loading || !data ? "loading" : data.currentUser!.firstName}
             </div>
           )}
         </Query>
@@ -71,57 +77,60 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    itAsync('should pass any GraphQL errors in props along with data during a SSR when errorPolicy="all"', (resolve, reject) => {
-      const query: DocumentNode = gql`
-        query people {
-          allPeople {
-            people {
-              name
+    itAsync(
+      'should pass any GraphQL errors in props along with data during a SSR when errorPolicy="all"',
+      (resolve, reject) => {
+        const query: DocumentNode = gql`
+          query people {
+            allPeople {
+              people {
+                name
+              }
             }
           }
-        }
-      `;
-      const link = mockSingleLink({
-        request: { query },
-        result: {
-          data: {
-            allPeople: {
-              people: null
-            }
+        `;
+        const link = mockSingleLink({
+          request: { query },
+          result: {
+            data: {
+              allPeople: {
+                people: null,
+              },
+            },
+            errors: [new Error("this is an error")],
           },
-          errors: [new Error('this is an error')]
-        }
-      });
+        });
 
-      const client = new ApolloClient({
-        link,
-        cache: new Cache({ addTypename: false })
-      });
+        const client = new ApolloClient({
+          link,
+          cache: new Cache({ addTypename: false }),
+        });
 
-      const app = (
-        <ApolloProvider client={client}>
-          <Query query={query} errorPolicy="all">
-            {({ loading, data, error }: any) => {
-              if (!loading) {
-                expect(data).toMatchObject({ allPeople: { people: null } });
-                expect(error).toBeDefined();
-                expect(error.graphQLErrors[0].message).toEqual(
-                  'this is an error'
-                );
-                resolve();
-              }
-              return null;
-            }}
-          </Query>
-        </ApolloProvider>
-      );
+        const app = (
+          <ApolloProvider client={client}>
+            <Query query={query} errorPolicy="all">
+              {({ loading, data, error }: any) => {
+                if (!loading) {
+                  expect(data).toMatchObject({ allPeople: { people: null } });
+                  expect(error).toBeDefined();
+                  expect(error.graphQLErrors[0].message).toEqual(
+                    "this is an error"
+                  );
+                  resolve();
+                }
+                return null;
+              }}
+            </Query>
+          </ApolloProvider>
+        );
 
-      getDataFromTree(app);
-    });
+        getDataFromTree(app);
+      }
+    );
   });
 });
diff --git a/src/react/components/__tests__/ssr/server.test.tsx b/src/react/components/__tests__/ssr/server.test.tsx
--- a/src/react/components/__tests__/ssr/server.test.tsx
+++ b/src/react/components/__tests__/ssr/server.test.tsx
@@ -1,5 +1,5 @@
 /** @jest-environment node */
-import React from 'react';
+import React from "react";
 import {
   print,
   graphql as execute,
@@ -7,111 +7,111 @@ import {
   GraphQLObjectType,
   GraphQLList,
   GraphQLString,
-  GraphQLID
-} from 'graphql';
-import gql from 'graphql-tag';
+  GraphQLID,
+} from "graphql";
+import gql from "graphql-tag";
 
-import { ApolloClient } from '../../../../core';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { ApolloProvider } from '../../../context';
-import { ApolloLink } from '../../../../link/core';
-import { Observable } from '../../../../utilities';
-import { renderToStringWithData } from '../../../ssr';
-import { Query } from '../../Query';
+import { ApolloClient } from "../../../../core";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { ApolloProvider } from "../../../context";
+import { ApolloLink } from "../../../../link/core";
+import { Observable } from "../../../../utilities";
+import { renderToStringWithData } from "../../../ssr";
+import { Query } from "../../Query";
 
-const planetMap = new Map([['Planet:1', { id: 'Planet:1', name: 'Tatooine' }]]);
+const planetMap = new Map([["Planet:1", { id: "Planet:1", name: "Tatooine" }]]);
 
 const shipMap = new Map([
   [
-    'Ship:2',
+    "Ship:2",
     {
-      id: 'Ship:2',
-      name: 'CR90 corvette',
-      films: ['Film:4', 'Film:6', 'Film:3']
-    }
+      id: "Ship:2",
+      name: "CR90 corvette",
+      films: ["Film:4", "Film:6", "Film:3"],
+    },
   ],
   [
-    'Ship:3',
+    "Ship:3",
     {
-      id: 'Ship:3',
-      name: 'Star Destroyer',
-      films: ['Film:4', 'Film:5', 'Film:6']
-    }
-  ]
+      id: "Ship:3",
+      name: "Star Destroyer",
+      films: ["Film:4", "Film:5", "Film:6"],
+    },
+  ],
 ]);
 
 const filmMap = new Map([
-  ['Film:3', { id: 'Film:3', title: 'Revenge of the Sith' }],
-  ['Film:4', { id: 'Film:4', title: 'A New Hope' }],
-  ['Film:5', { id: 'Film:5', title: 'the Empire Strikes Back' }],
-  ['Film:6', { id: 'Film:6', title: 'Return of the Jedi' }]
+  ["Film:3", { id: "Film:3", title: "Revenge of the Sith" }],
+  ["Film:4", { id: "Film:4", title: "A New Hope" }],
+  ["Film:5", { id: "Film:5", title: "the Empire Strikes Back" }],
+  ["Film:6", { id: "Film:6", title: "Return of the Jedi" }],
 ]);
 
 const PlanetType = new GraphQLObjectType({
-  name: 'Planet',
+  name: "Planet",
   fields: {
     id: { type: GraphQLID },
-    name: { type: GraphQLString }
-  }
+    name: { type: GraphQLString },
+  },
 });
 
 const FilmType = new GraphQLObjectType({
-  name: 'Film',
+  name: "Film",
   fields: {
     id: { type: GraphQLID },
-    title: { type: GraphQLString }
-  }
+    title: { type: GraphQLString },
+  },
 });
 
 const ShipType = new GraphQLObjectType({
-  name: 'Ship',
+  name: "Ship",
   fields: {
     id: { type: GraphQLID },
     name: { type: GraphQLString },
     films: {
       type: new GraphQLList(FilmType),
-      resolve: ({ films }) => films.map((id: string) => filmMap.get(id))
-    }
-  }
+      resolve: ({ films }) => films.map((id: string) => filmMap.get(id)),
+    },
+  },
 });
 
 const QueryType = new GraphQLObjectType({
-  name: 'Query',
+  name: "Query",
   fields: {
     allPlanets: {
       type: new GraphQLList(PlanetType),
-      resolve: () => Array.from(planetMap.values())
+      resolve: () => Array.from(planetMap.values()),
     },
     allShips: {
       type: new GraphQLList(ShipType),
-      resolve: () => Array.from(shipMap.values())
+      resolve: () => Array.from(shipMap.values()),
     },
     ship: {
       type: ShipType,
       args: { id: { type: GraphQLID } },
-      resolve: (_, { id }) => shipMap.get(id)
+      resolve: (_, { id }) => shipMap.get(id),
     },
     film: {
       type: FilmType,
       args: { id: { type: GraphQLID } },
-      resolve: (_, { id }) => filmMap.get(id)
-    }
-  }
+      resolve: (_, { id }) => filmMap.get(id),
+    },
+  },
 });
 
 const Schema = new GraphQLSchema({ query: QueryType });
 
-describe('SSR', () => {
-  it('should work with React.createContext', async () => {
-    let defaultValue = 'default';
+describe("SSR", () => {
+  it("should work with React.createContext", async () => {
+    let defaultValue = "default";
     let Context = React.createContext(defaultValue);
-    let providerValue = 'provider';
+    let providerValue = "provider";
     expect(
       await renderToStringWithData(
         <React.Fragment>
           <Context.Provider value={providerValue} />
           <Context.Consumer>
-            {val => {
+            {(val) => {
               expect(val).toBe(defaultValue);
               return val;
             }}
@@ -123,7 +123,7 @@ describe('SSR', () => {
       await renderToStringWithData(
         <Context.Provider value={providerValue}>
           <Context.Consumer>
-            {val => {
+            {(val) => {
               expect(val).toBe(providerValue);
               return val;
             }}
@@ -134,7 +134,7 @@ describe('SSR', () => {
     expect(
       await renderToStringWithData(
         <Context.Consumer>
-          {val => {
+          {(val) => {
             expect(val).toBe(defaultValue);
             return val;
           }}
@@ -147,34 +147,34 @@ describe('SSR', () => {
       await renderToStringWithData(
         <ContextForUndefined.Provider value={undefined}>
           <ContextForUndefined.Consumer>
-            {val => {
+            {(val) => {
               expect(val).toBeUndefined();
-              return val === undefined ? 'works' : 'broken';
+              return val === undefined ? "works" : "broken";
             }}
           </ContextForUndefined.Consumer>
         </ContextForUndefined.Provider>
       )
-    ).toBe('works');
+    ).toBe("works");
 
     const apolloClient = new ApolloClient({
-      link: new ApolloLink(config => {
-        return new Observable(observer => {
+      link: new ApolloLink((config) => {
+        return new Observable((observer) => {
           execute({
             schema: Schema,
             source: print(config.query),
             variableValues: config.variables,
             operationName: config.operationName,
           })
-            .then(result => {
+            .then((result) => {
               observer.next(result);
               observer.complete();
             })
-            .catch(e => {
+            .catch((e) => {
               observer.error(e);
             });
         });
       }),
-      cache: new Cache()
+      cache: new Cache(),
     });
 
     expect(
@@ -192,7 +192,7 @@ describe('SSR', () => {
             >
               {() => (
                 <Context.Consumer>
-                  {val => {
+                  {(val) => {
                     expect(val).toBe(providerValue);
                     return val;
                   }}
diff --git a/src/react/context/__tests__/ApolloConsumer.test.tsx b/src/react/context/__tests__/ApolloConsumer.test.tsx
--- a/src/react/context/__tests__/ApolloConsumer.test.tsx
+++ b/src/react/context/__tests__/ApolloConsumer.test.tsx
@@ -1,25 +1,25 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
+import React from "react";
+import { render, screen } from "@testing-library/react";
 
-import { ApolloLink } from '../../../link/core';
-import { ApolloClient } from '../../../core';
-import { InMemoryCache as Cache } from '../../../cache';
-import { ApolloProvider } from '../ApolloProvider';
-import { ApolloConsumer } from '../ApolloConsumer';
-import { getApolloContext } from '../ApolloContext';
-import { itAsync } from '../../../testing';
+import { ApolloLink } from "../../../link/core";
+import { ApolloClient } from "../../../core";
+import { InMemoryCache as Cache } from "../../../cache";
+import { ApolloProvider } from "../ApolloProvider";
+import { ApolloConsumer } from "../ApolloConsumer";
+import { getApolloContext } from "../ApolloContext";
+import { itAsync } from "../../../testing";
 
 const client = new ApolloClient({
   cache: new Cache(),
-  link: new ApolloLink((o, f) => (f ? f(o) : null))
+  link: new ApolloLink((o, f) => (f ? f(o) : null)),
 });
 
-describe('<ApolloConsumer /> component', () => {
-  itAsync('has a render prop', (resolve, reject) => {
+describe("<ApolloConsumer /> component", () => {
+  itAsync("has a render prop", (resolve, reject) => {
     render(
       <ApolloProvider client={client}>
         <ApolloConsumer>
-          {clientRender => {
+          {(clientRender) => {
             try {
               expect(clientRender).toBe(client);
               resolve();
@@ -33,17 +33,17 @@ describe('<ApolloConsumer /> component', () => {
     );
   });
 
-  it('renders the content in the children prop', () => {
+  it("renders the content in the children prop", () => {
     render(
       <ApolloProvider client={client}>
         <ApolloConsumer>{() => <div>Test</div>}</ApolloConsumer>
       </ApolloProvider>
     );
 
-    expect(screen.getByText('Test')).toBeTruthy();
+    expect(screen.getByText("Test")).toBeTruthy();
   });
 
-  it('errors if there is no client in the context', () => {
+  it("errors if there is no client in the context", () => {
     // Prevent Error about missing context type from appearing in the console.
     const errorLogger = console.error;
     console.error = () => {};
diff --git a/src/react/context/__tests__/ApolloProvider.test.tsx b/src/react/context/__tests__/ApolloProvider.test.tsx
--- a/src/react/context/__tests__/ApolloProvider.test.tsx
+++ b/src/react/context/__tests__/ApolloProvider.test.tsx
@@ -1,39 +1,44 @@
-import React, { useContext } from 'react';
-import { render, screen } from '@testing-library/react';
+import React, { useContext } from "react";
+import { render, screen } from "@testing-library/react";
 
-import { ApolloLink } from '../../../link/core';
-import { ApolloClient } from '../../../core';
-import { InMemoryCache as Cache } from '../../../cache';
-import { ApolloProvider } from '../ApolloProvider';
-import { getApolloContext } from '../ApolloContext';
+import { ApolloLink } from "../../../link/core";
+import { ApolloClient } from "../../../core";
+import { InMemoryCache as Cache } from "../../../cache";
+import { ApolloProvider, ApolloProviderProps } from "../ApolloProvider";
+import { ApolloContextValue, getApolloContext } from "../ApolloContext";
 
-describe('<ApolloProvider /> Component', () => {
+describe("<ApolloProvider /> Component", () => {
   const client = new ApolloClient({
     cache: new Cache(),
-    link: new ApolloLink((o, f) => (f ? f(o) : null))
+    link: new ApolloLink((o, f) => (f ? f(o) : null)),
   });
 
-  it('should render children components', () => {
+  const anotherClient = new ApolloClient({
+    cache: new Cache(),
+    link: new ApolloLink((o, f) => (f ? f(o) : null)),
+  });
+
+  it("should render children components", () => {
     render(
       <ApolloProvider client={client}>
         <div className="unique">Test</div>
       </ApolloProvider>
     );
 
-    expect(screen.getByText('Test')).toBeTruthy();
+    expect(screen.getByText("Test")).toBeTruthy();
   });
 
-  it('should support the 2.0', () => {
+  it("should support the 2.0", () => {
     render(
       <ApolloProvider client={{} as ApolloClient<any>}>
         <div className="unique">Test</div>
       </ApolloProvider>
     );
 
-    expect(screen.getByText('Test')).toBeTruthy();
+    expect(screen.getByText("Test")).toBeTruthy();
   });
 
-  it('should require a client', () => {
+  it("should require a client", () => {
     const originalConsoleError = console.error;
     console.error = () => {
       /* noop */
@@ -50,22 +55,22 @@ describe('<ApolloProvider /> Component', () => {
         </ApolloContext.Provider>
       );
     }).toThrowError(
-      'ApolloProvider was not passed a client instance. Make ' +
+      "ApolloProvider was not passed a client instance. Make " +
         'sure you pass in your client via the "client" prop.'
     );
     console.error = originalConsoleError;
   });
 
-  it('should not require a store', () => {
+  it("should not require a store", () => {
     render(
       <ApolloProvider client={client}>
         <div className="unique">Test</div>
       </ApolloProvider>
     );
-    expect(screen.getByText('Test')).toBeTruthy();
+    expect(screen.getByText("Test")).toBeTruthy();
   });
 
-  it('should add the client to the children context', () => {
+  it("should add the client to the children context", () => {
     const TestChild = () => {
       const context = useContext(getApolloContext());
       expect(context.client).toEqual(client);
@@ -79,7 +84,7 @@ describe('<ApolloProvider /> Component', () => {
     );
   });
 
-  it('should update props when the client changes', () => {
+  it("should update props when the client changes", () => {
     let clientToCheck = client;
 
     const TestChild = () => {
@@ -95,7 +100,7 @@ describe('<ApolloProvider /> Component', () => {
 
     const newClient = new ApolloClient({
       cache: new Cache(),
-      link: new ApolloLink((o, f) => (f ? f(o) : null))
+      link: new ApolloLink((o, f) => (f ? f(o) : null)),
     });
     clientToCheck = newClient;
     rerender(
@@ -104,4 +109,69 @@ describe('<ApolloProvider /> Component', () => {
       </ApolloProvider>
     );
   });
+
+  describe.each<
+    [
+      string,
+      Omit<ApolloProviderProps<any>, "children">,
+      Omit<ApolloProviderProps<any>, "children">,
+    ]
+  >([["client", { client }, { client: anotherClient }]])(
+    "context value stability, %s prop",
+    (prop, value, childValue) => {
+      it(`should not recreate the context value if the ${prop} prop didn't change`, () => {
+        let lastContext: ApolloContextValue | undefined;
+
+        const TestChild = () => {
+          lastContext = useContext(getApolloContext());
+          return null;
+        };
+
+        const { rerender } = render(
+          <ApolloProvider {...value}>
+            <TestChild />
+          </ApolloProvider>
+        );
+
+        const firstContextValue = lastContext;
+
+        rerender(
+          <ApolloProvider {...value}>
+            <TestChild />
+          </ApolloProvider>
+        );
+
+        expect(lastContext).toBe(firstContextValue);
+      });
+
+      it(`should not recreate the context if the parent context value differs, but the ${prop} prop didn't change`, () => {
+        let lastContext: ApolloContextValue | undefined;
+
+        const TestChild = () => {
+          lastContext = useContext(getApolloContext());
+          return null;
+        };
+
+        const { rerender } = render(
+          <ApolloProvider {...value}>
+            <ApolloProvider {...childValue}>
+              <TestChild />
+            </ApolloProvider>
+          </ApolloProvider>
+        );
+
+        const firstContextValue = lastContext;
+
+        rerender(
+          <ApolloProvider {...value}>
+            <ApolloProvider {...childValue}>
+              <TestChild />
+            </ApolloProvider>
+          </ApolloProvider>
+        );
+
+        expect(lastContext).toBe(firstContextValue);
+      });
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/client-option.test.tsx b/src/react/hoc/__tests__/client-option.test.tsx
--- a/src/react/hoc/__tests__/client-option.test.tsx
+++ b/src/react/hoc/__tests__/client-option.test.tsx
@@ -1,17 +1,17 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../core';
-import { ApolloProvider } from '../../context';
-import { InMemoryCache as Cache } from '../../../cache';
-import { itAsync, mockSingleLink } from '../../../testing';
-import { graphql } from '../graphql';
-import { ChildProps } from '../types';
-
-describe('client option', () => {
-  it('renders with client from options', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../core";
+import { ApolloProvider } from "../../context";
+import { InMemoryCache as Cache } from "../../../cache";
+import { itAsync, mockSingleLink } from "../../../testing";
+import { graphql } from "../graphql";
+import { ChildProps } from "../types";
+
+describe("client option", () => {
+  it("renders with client from options", () => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -21,22 +21,22 @@ describe('client option', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
 
     type Data = typeof data;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     const config = {
       options: {
-        client
-      }
+        client,
+      },
     };
     const ContainerWithData = graphql<{}, Data>(query, config)(() => null);
     const { unmount } = render(
@@ -44,7 +44,7 @@ describe('client option', () => {
         client={
           new ApolloClient({
             link,
-            cache: new Cache({ addTypename: false })
+            cache: new Cache({ addTypename: false }),
           })
         }
       >
@@ -54,7 +54,7 @@ describe('client option', () => {
     unmount();
   });
 
-  itAsync('doesnt require a recycler', (resolve, reject) => {
+  itAsync("doesnt require a recycler", (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -64,24 +64,27 @@ describe('client option', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     const config = {
       options: {
-        client
-      }
+        client,
+      },
     };
-    let renderCount = 0
-    const ContainerWithData = graphql<{}, Data>(query, config)(() => {
+    let renderCount = 0;
+    const ContainerWithData = graphql<{}, Data>(
+      query,
+      config
+    )(() => {
       renderCount += 1;
       return null;
     });
@@ -92,72 +95,77 @@ describe('client option', () => {
     }).then(resolve, reject);
   });
 
-  itAsync('ignores client from context if client from options is present', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "ignores client from context if client from options is present",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
+      `;
+      const dataProvider = {
+        allPeople: { people: [{ name: "Leia Organa Solo" }] },
+      };
+
+      type Data = typeof dataProvider;
+      const linkProvider = mockSingleLink({
+        request: { query },
+        result: { data: dataProvider },
+      });
+      const clientProvider = new ApolloClient({
+        link: linkProvider,
+        cache: new Cache({ addTypename: false }),
+      });
+      const dataOptions = {
+        allPeople: { people: [{ name: "Luke Skywalker" }] },
+      };
+      const linkOptions = mockSingleLink({
+        request: { query },
+        result: { data: dataOptions },
+      });
+      const clientOptions = new ApolloClient({
+        link: linkOptions,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      const config = {
+        options: {
+          client: clientOptions,
+        },
+      };
+
+      class Container extends React.Component<ChildProps<{}, Data>> {
+        componentDidUpdate() {
+          const { data } = this.props;
+          expect(data!.loading).toBeFalsy(); // first data
+          expect(data!.allPeople).toEqual({
+            people: [{ name: "Luke Skywalker" }],
+          });
+          done = true;
+        }
+        render() {
+          return null;
+        }
       }
-    `;
-    const dataProvider = {
-      allPeople: { people: [{ name: 'Leia Organa Solo' }] }
-    };
-
-    type Data = typeof dataProvider;
-    const linkProvider = mockSingleLink({
-      request: { query },
-      result: { data: dataProvider }
-    });
-    const clientProvider = new ApolloClient({
-      link: linkProvider,
-      cache: new Cache({ addTypename: false })
-    });
-    const dataOptions = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const linkOptions = mockSingleLink({
-      request: { query },
-      result: { data: dataOptions }
-    });
-    const clientOptions = new ApolloClient({
-      link: linkOptions,
-      cache: new Cache({ addTypename: false })
-    });
-
-    const config = {
-      options: {
-        client: clientOptions
-      }
-    };
-
-    class Container extends React.Component<ChildProps<{}, Data>> {
-      componentDidUpdate() {
-        const { data } = this.props;
-        expect(data!.loading).toBeFalsy(); // first data
-        expect(data!.allPeople).toEqual({
-          people: [{ name: 'Luke Skywalker' }]
-        });
-        done = true;
-      }
-      render() {
-        return null;
-      }
+      const ContainerWithData = graphql<{}, Data>(query, config)(Container);
+      render(
+        <ApolloProvider client={clientProvider}>
+          <ContainerWithData />
+        </ApolloProvider>
+      );
+
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
     }
-    const ContainerWithData = graphql<{}, Data>(query, config)(Container);
-    render(
-      <ApolloProvider client={clientProvider}>
-        <ContainerWithData />
-      </ApolloProvider>
-    );
-
-    waitFor(() => {
-      expect(done).toBe(true);
-    }).then(resolve, reject);
-  });
+  );
 
-  itAsync('exposes refetch as part of the props api', (resolve, reject) => {
+  itAsync("exposes refetch as part of the props api", (resolve, reject) => {
     let done = false;
     const query: DocumentNode = gql`
       query people($first: Int) {
@@ -171,16 +179,16 @@ describe('client option', () => {
     const variables = { first: 1 };
     type Variables = typeof variables;
 
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data1;
 
     const link = mockSingleLink({
       request: { query, variables },
-      result: { data: data1 }
+      result: { data: data1 },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<Variables, Data, Variables>(query)(
diff --git a/src/react/hoc/__tests__/fragments.test.tsx b/src/react/hoc/__tests__/fragments.test.tsx
--- a/src/react/hoc/__tests__/fragments.test.tsx
+++ b/src/react/hoc/__tests__/fragments.test.tsx
@@ -1,18 +1,18 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
 
-import { ApolloClient } from '../../../core';
-import { ApolloProvider } from '../../context';
-import { InMemoryCache as Cache } from '../../../cache';
-import { itAsync, mockSingleLink } from '../../../testing';
-import { graphql } from '../graphql';
-import { ChildProps } from '../types';
+import { ApolloClient } from "../../../core";
+import { ApolloProvider } from "../../context";
+import { InMemoryCache as Cache } from "../../../cache";
+import { itAsync, mockSingleLink } from "../../../testing";
+import { graphql } from "../graphql";
+import { ChildProps } from "../types";
 
-describe('fragments', () => {
+describe("fragments", () => {
   // XXX in a later version, we should support this for composition
-  it('throws if you only pass a fragment', () => {
+  it("throws if you only pass a fragment", () => {
     const query: DocumentNode = gql`
       fragment Failure on PeopleConnection {
         people {
@@ -21,17 +21,17 @@ describe('fragments', () => {
       }
     `;
     const expectedData = {
-      allPeople: { people: [{ name: 'Luke Skywalker' }] }
+      allPeople: { people: [{ name: "Luke Skywalker" }] },
     };
     type Data = typeof expectedData;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data: expectedData }
+      result: { data: expectedData },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     try {
@@ -40,9 +40,7 @@ describe('fragments', () => {
           componentDidUpdate() {
             const { props } = this;
             expect(props.data!.loading).toBeFalsy();
-            expect(props.data!.allPeople).toEqual(
-              expectedData.allPeople
-            );
+            expect(props.data!.allPeople).toEqual(expectedData.allPeople);
           }
           render() {
             return null;
@@ -61,63 +59,64 @@ describe('fragments', () => {
     }
   });
 
-  itAsync('correctly fetches a query with inline fragments', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          __typename
-          ...person
+  itAsync(
+    "correctly fetches a query with inline fragments",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            __typename
+            ...person
+          }
         }
-      }
 
-      fragment person on PeopleConnection {
-        people {
-          name
+        fragment person on PeopleConnection {
+          people {
+            name
+          }
         }
-      }
-    `;
-    const data = {
-      allPeople: {
-        __typename: 'PeopleConnection',
-        people: [{ name: 'Luke Skywalker' }]
-      }
-    };
+      `;
+      const data = {
+        allPeople: {
+          __typename: "PeopleConnection",
+          people: [{ name: "Luke Skywalker" }],
+        },
+      };
 
-    type Data = typeof data;
+      type Data = typeof data;
 
-    const link = mockSingleLink({
-      request: { query },
-      result: { data }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      const link = mockSingleLink({
+        request: { query },
+        result: { data },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    const Container = graphql<{}, Data>(query)(
-      class extends React.Component<ChildProps<{}, Data>> {
-        componentDidUpdate() {
-          expect(this.props.data!.loading).toBeFalsy();
-          expect(this.props.data!.allPeople).toEqual(
-            data.allPeople
-          );
-          done = true;
-        }
-        render() {
-          return null;
+      const Container = graphql<{}, Data>(query)(
+        class extends React.Component<ChildProps<{}, Data>> {
+          componentDidUpdate() {
+            expect(this.props.data!.loading).toBeFalsy();
+            expect(this.props.data!.allPeople).toEqual(data.allPeople);
+            done = true;
+          }
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true);
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/mutations/index.test.tsx b/src/react/hoc/__tests__/mutations/index.test.tsx
--- a/src/react/hoc/__tests__/mutations/index.test.tsx
+++ b/src/react/hoc/__tests__/mutations/index.test.tsx
@@ -1,18 +1,14 @@
-import React from 'react';
-import { render } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import {
-  createMockClient,
-  itAsync,
-  MockedProvider,
-} from '../../../../testing';
-import { NormalizedCacheObject } from '../../../../cache';
-import { ApolloProvider } from '../../../context';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
+import React from "react";
+import { render } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { createMockClient, itAsync, MockedProvider } from "../../../../testing";
+import { NormalizedCacheObject } from "../../../../cache";
+import { ApolloProvider } from "../../../context";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
 
 const query: DocumentNode = gql`
   mutation addPerson {
@@ -35,10 +31,10 @@ interface Variables {
 }
 
 const expectedData = {
-  allPeople: { people: [{ name: 'Luke Skywalker' }] }
+  allPeople: { people: [{ name: "Luke Skywalker" }] },
 };
 
-describe('graphql(mutation)', () => {
+describe("graphql(mutation)", () => {
   let error: typeof console.error;
   let client: ApolloClient<NormalizedCacheObject>;
   beforeEach(() => {
@@ -51,12 +47,12 @@ describe('graphql(mutation)', () => {
     console.error = error;
   });
 
-  it('binds a mutation to props', () => {
+  it("binds a mutation to props", () => {
     const ContainerWithData = graphql(query)(({ mutate, result }) => {
       expect(mutate).toBeTruthy();
       expect(result).toBeTruthy();
-      expect(typeof mutate).toBe('function');
-      expect(typeof result).toBe('object');
+      expect(typeof mutate).toBe("function");
+      expect(typeof result).toBe("object");
       return null;
     });
 
@@ -67,7 +63,7 @@ describe('graphql(mutation)', () => {
     );
   });
 
-  it('binds a mutation result to props', () => {
+  it("binds a mutation result to props", () => {
     type InjectedProps = {
       result: any;
     };
@@ -77,7 +73,7 @@ describe('graphql(mutation)', () => {
     )(({ result }) => {
       const { loading, error } = result;
       expect(result).toBeTruthy();
-      expect(typeof loading).toBe('boolean');
+      expect(typeof loading).toBe("boolean");
       expect(error).toBeFalsy();
 
       return null;
@@ -90,7 +86,7 @@ describe('graphql(mutation)', () => {
     );
   });
 
-  it('binds a mutation to props with a custom name', () => {
+  it("binds a mutation to props with a custom name", () => {
     interface Props {}
 
     type InjectedProps = {
@@ -100,12 +96,12 @@ describe('graphql(mutation)', () => {
 
     const ContainerWithData = graphql<Props, Data, Variables, InjectedProps>(
       query,
-      { name: 'customMutation' }
+      { name: "customMutation" }
     )(({ customMutation, customMutationResult }) => {
       expect(customMutation).toBeTruthy();
       expect(customMutationResult).toBeTruthy();
-      expect(typeof customMutation).toBe('function');
-      expect(typeof customMutationResult).toBe('object');
+      expect(typeof customMutation).toBe("function");
+      expect(typeof customMutationResult).toBe("object");
       return null;
     });
 
@@ -116,7 +112,7 @@ describe('graphql(mutation)', () => {
     );
   });
 
-  it('binds a mutation to custom props', () => {
+  it("binds a mutation to custom props", () => {
     interface Props {
       methodName: string;
     }
@@ -128,12 +124,12 @@ describe('graphql(mutation)', () => {
       {
         props: ({ ownProps, mutate: addPerson }) => ({
           [ownProps.methodName]: (name: string) =>
-            addPerson!({ variables: { name } })
-        })
+            addPerson!({ variables: { name } }),
+        }),
       }
     )(({ myInjectedMutationMethod }) => {
       expect(myInjectedMutationMethod).toBeTruthy();
-      expect(typeof myInjectedMutationMethod).toBe('function');
+      expect(typeof myInjectedMutationMethod).toBe("function");
       return null;
     });
 
@@ -144,7 +140,7 @@ describe('graphql(mutation)', () => {
     );
   });
 
-  itAsync('does not swallow children errors', (resolve, reject) => {
+  itAsync("does not swallow children errors", (resolve, reject) => {
     let bar: any;
     const ContainerWithData = graphql(query)(() => {
       bar(); // this will throw
@@ -173,11 +169,11 @@ describe('graphql(mutation)', () => {
     );
   });
 
-  itAsync('can execute a mutation', (resolve, reject) => {
+  itAsync("can execute a mutation", (resolve, reject) => {
     const Container = graphql(query)(
       class extends React.Component<ChildProps> {
         componentDidMount() {
-          this.props.mutate!().then(result => {
+          this.props.mutate!().then((result) => {
             expect(result && result.data).toEqual(expectedData);
             resolve();
           });
@@ -195,95 +191,101 @@ describe('graphql(mutation)', () => {
     );
   });
 
-  itAsync('can execute a mutation with variables from props', (resolve, reject) => {
-    const queryWithVariables = gql`
-      mutation addPerson($first: Int) {
-        allPeople(first: $first) {
-          people {
-            name
+  itAsync(
+    "can execute a mutation with variables from props",
+    (resolve, reject) => {
+      const queryWithVariables = gql`
+        mutation addPerson($first: Int) {
+          allPeople(first: $first) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    client = createMockClient(expectedData, queryWithVariables, {
-      first: 1
-    });
-
-    interface Props {
-      first: number;
-    }
+      `;
+      client = createMockClient(expectedData, queryWithVariables, {
+        first: 1,
+      });
 
-    const Container = graphql<Props>(queryWithVariables)(
-      class extends React.Component<ChildProps<Props>> {
-        componentDidMount() {
-          this.props.mutate!().then(result => {
-            expect(result && result.data).toEqual(expectedData);
-            resolve();
-          });
-        }
-        render() {
-          return null;
-        }
+      interface Props {
+        first: number;
       }
-    );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container first={1} />
-      </ApolloProvider>
-    );
-  });
-
-  itAsync('can execute a mutation with variables from BOTH options and arguments', (resolve, reject) => {
-    const queryWithVariables = gql`
-      mutation addPerson($first: Int!, $second: Int!) {
-        allPeople(first: $first) {
-          people {
-            name
+      const Container = graphql<Props>(queryWithVariables)(
+        class extends React.Component<ChildProps<Props>> {
+          componentDidMount() {
+            this.props.mutate!().then((result) => {
+              expect(result && result.data).toEqual(expectedData);
+              resolve();
+            });
+          }
+          render() {
+            return null;
           }
         }
-      }
-    `;
+      );
 
-    const mocks = [
-      {
-        request: {
-          query: queryWithVariables,
-          variables: {
-            first: 1,
-            second: 2
+      render(
+        <ApolloProvider client={client}>
+          <Container first={1} />
+        </ApolloProvider>
+      );
+    }
+  );
+
+  itAsync(
+    "can execute a mutation with variables from BOTH options and arguments",
+    (resolve, reject) => {
+      const queryWithVariables = gql`
+        mutation addPerson($first: Int!, $second: Int!) {
+          allPeople(first: $first) {
+            people {
+              name
+            }
           }
-        },
-        result: { data: expectedData },
-      }
-    ];
-
-    interface Props {}
-
-    const Container = graphql<Props>(queryWithVariables, {
-      options: () => ({
-        variables: { first: 1 }
-      })
-    })(
-      class extends React.Component<ChildProps<Props>> {
-        componentDidMount() {
-          this.props.mutate!({
-            variables: { second: 2 }
-          }).then(result => {
-            expect(result && result.data).toEqual(expectedData);
-            resolve();
-          });
         }
-        render() {
-          return null;
+      `;
+
+      const mocks = [
+        {
+          request: {
+            query: queryWithVariables,
+            variables: {
+              first: 1,
+              second: 2,
+            },
+          },
+          result: { data: expectedData },
+        },
+      ];
+
+      interface Props {}
+
+      const Container = graphql<Props>(queryWithVariables, {
+        options: () => ({
+          variables: { first: 1 },
+        }),
+      })(
+        class extends React.Component<ChildProps<Props>> {
+          componentDidMount() {
+            this.props.mutate!({
+              variables: { second: 2 },
+            }).then((result) => {
+              expect(result && result.data).toEqual(expectedData);
+              resolve();
+            });
+          }
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <MockedProvider mocks={mocks}>
-        <Container />
-      </MockedProvider>
-    );
-  });
+      render(
+        <MockedProvider mocks={mocks}>
+          <Container />
+        </MockedProvider>
+      );
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/mutations/lifecycle.test.tsx b/src/react/hoc/__tests__/mutations/lifecycle.test.tsx
--- a/src/react/hoc/__tests__/mutations/lifecycle.test.tsx
+++ b/src/react/hoc/__tests__/mutations/lifecycle.test.tsx
@@ -1,11 +1,11 @@
-import React from 'react';
-import { render, cleanup } from '@testing-library/react';
-import gql from 'graphql-tag';
+import React from "react";
+import { render, cleanup } from "@testing-library/react";
+import gql from "graphql-tag";
 
-import { ApolloProvider } from '../../../context/ApolloProvider';
-import { itAsync, createMockClient } from '../../../../testing/core';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
+import { ApolloProvider } from "../../../context/ApolloProvider";
+import { itAsync, createMockClient } from "../../../../testing/core";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
 
 const query = gql`
   mutation addPerson($id: Int) {
@@ -17,40 +17,43 @@ const query = gql`
   }
 `;
 const expectedData = {
-  allPeople: { people: [{ name: 'Luke Skywalker' }] }
+  allPeople: { people: [{ name: "Luke Skywalker" }] },
 };
 
-describe('graphql(mutation) lifecycle', () => {
+describe("graphql(mutation) lifecycle", () => {
   afterEach(cleanup);
 
-  itAsync('allows falsy values in the mapped variables from props', (resolve, reject) => {
-    const client = createMockClient(expectedData, query, { id: null });
+  itAsync(
+    "allows falsy values in the mapped variables from props",
+    (resolve, reject) => {
+      const client = createMockClient(expectedData, query, { id: null });
 
-    interface Props {
-      id: string | null;
-    }
-
-    const Container = graphql<Props>(query)(
-      class extends React.Component<ChildProps<Props>> {
-        componentDidMount() {
-          this.props.mutate!().then(result => {
-            expect(result && result.data).toEqual(expectedData);
-            resolve();
-          });
-        }
+      interface Props {
+        id: string | null;
+      }
 
-        render() {
-          return null;
+      const Container = graphql<Props>(query)(
+        class extends React.Component<ChildProps<Props>> {
+          componentDidMount() {
+            this.props.mutate!().then((result) => {
+              expect(result && result.data).toEqual(expectedData);
+              resolve();
+            });
+          }
+
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container id={null} />
-      </ApolloProvider>
-    );
-  });
+      render(
+        <ApolloProvider client={client}>
+          <Container id={null} />
+        </ApolloProvider>
+      );
+    }
+  );
 
   it("errors if the passed props don't contain the needed variables", () => {
     const client = createMockClient(expectedData, query, { first: 1 });
@@ -69,53 +72,56 @@ describe('graphql(mutation) lifecycle', () => {
     }
   });
 
-  itAsync('rebuilds the mutation on prop change when using `options`', (resolve, reject) => {
-    const client = createMockClient(expectedData, query, {
-      id: 2
-    });
+  itAsync(
+    "rebuilds the mutation on prop change when using `options`",
+    (resolve, reject) => {
+      const client = createMockClient(expectedData, query, {
+        id: 2,
+      });
 
-    interface Props {
-      listId: number;
-    }
+      interface Props {
+        listId: number;
+      }
 
-    function options(props: Props) {
-      return {
-        variables: {
-          id: props.listId
-        }
-      };
-    }
+      function options(props: Props) {
+        return {
+          variables: {
+            id: props.listId,
+          },
+        };
+      }
 
-    class Container extends React.Component<ChildProps<Props>> {
-      render() {
-        if (this.props.listId !== 2) return null;
-        this.props.mutate!().then(() => resolve());
-        return null;
+      class Container extends React.Component<ChildProps<Props>> {
+        render() {
+          if (this.props.listId !== 2) return null;
+          this.props.mutate!().then(() => resolve());
+          return null;
+        }
       }
-    }
 
-    const ContainerWithMutate = graphql<Props>(query, { options })(Container);
+      const ContainerWithMutate = graphql<Props>(query, { options })(Container);
 
-    class ChangingProps extends React.Component<{}, { listId: number }> {
-      state = { listId: 1 };
+      class ChangingProps extends React.Component<{}, { listId: number }> {
+        state = { listId: 1 };
 
-      componentDidMount() {
-        setTimeout(() => this.setState({ listId: 2 }), 50);
-      }
+        componentDidMount() {
+          setTimeout(() => this.setState({ listId: 2 }), 50);
+        }
 
-      render() {
-        return <ContainerWithMutate listId={this.state.listId} />;
+        render() {
+          return <ContainerWithMutate listId={this.state.listId} />;
+        }
       }
-    }
 
-    render(
-      <ApolloProvider client={client}>
-        <ChangingProps />
-      </ApolloProvider>
-    );
-  });
+      render(
+        <ApolloProvider client={client}>
+          <ChangingProps />
+        </ApolloProvider>
+      );
+    }
+  );
 
-  itAsync('can execute a mutation with custom variables', (resolve, reject) => {
+  itAsync("can execute a mutation with custom variables", (resolve, reject) => {
     const client = createMockClient(expectedData, query, { id: 1 });
     interface Variables {
       id: number;
@@ -124,7 +130,7 @@ describe('graphql(mutation) lifecycle', () => {
     const Container = graphql<{}, {}, Variables>(query)(
       class extends React.Component<ChildProps<{}, {}, Variables>> {
         componentDidMount() {
-          this.props.mutate!({ variables: { id: 1 } }).then(result => {
+          this.props.mutate!({ variables: { id: 1 } }).then((result) => {
             expect(result && result.data).toEqual(expectedData);
             resolve();
           });
diff --git a/src/react/hoc/__tests__/mutations/queries.test.tsx b/src/react/hoc/__tests__/mutations/queries.test.tsx
--- a/src/react/hoc/__tests__/mutations/queries.test.tsx
+++ b/src/react/hoc/__tests__/mutations/queries.test.tsx
@@ -1,90 +1,91 @@
-import React from 'react';
-import { act, render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient, MutationUpdaterFunction, ApolloCache } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import {
-  itAsync,
-  createMockClient,
-  mockSingleLink,
-} from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-const IS_REACT_18 = React.version.startsWith('18');
+import React from "react";
+import { act, render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
 
-describe('graphql(mutation) query integration', () => {
-  itAsync('allows for passing optimisticResponse for a mutation', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      mutation createTodo {
-        createTodo {
-          id
-          text
-          completed
+import {
+  ApolloClient,
+  MutationUpdaterFunction,
+  ApolloCache,
+} from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, createMockClient, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("graphql(mutation) query integration", () => {
+  itAsync(
+    "allows for passing optimisticResponse for a mutation",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        mutation createTodo {
+          createTodo {
+            id
+            text
+            completed
+            __typename
+          }
           __typename
         }
-        __typename
-      }
-    `;
-
-    const data = {
-      __typename: 'Mutation',
-      createTodo: {
-        __typename: 'Todo',
-        id: '99',
-        text: 'This one was created with a mutation.',
-        completed: true
-      }
-    };
-
-    type Data = typeof data;
-
-    let mutateFired = false;
-    const client = createMockClient(data, query);
-    const Container = graphql<{}, Data>(query)(
-      class extends React.Component<ChildProps<{}, Data>> {
-        componentDidMount() {
-          const optimisticResponse = {
-            __typename: 'Mutation',
-            createTodo: {
-              __typename: 'Todo',
-              id: '99',
-              text: 'Optimistically generated',
-              completed: true
-            }
-          };
+      `;
+
+      const data = {
+        __typename: "Mutation",
+        createTodo: {
+          __typename: "Todo",
+          id: "99",
+          text: "This one was created with a mutation.",
+          completed: true,
+        },
+      };
 
-          this.props.mutate!({ optimisticResponse }).then(result => {
-            expect(result && result.data).toEqual(data);
-            mutateFired = true;
-          });
+      type Data = typeof data;
+
+      let mutateFired = false;
+      const client = createMockClient(data, query);
+      const Container = graphql<{}, Data>(query)(
+        class extends React.Component<ChildProps<{}, Data>> {
+          componentDidMount() {
+            const optimisticResponse = {
+              __typename: "Mutation",
+              createTodo: {
+                __typename: "Todo",
+                id: "99",
+                text: "Optimistically generated",
+                completed: true,
+              },
+            };
+
+            this.props.mutate!({ optimisticResponse }).then((result) => {
+              expect(result && result.data).toEqual(data);
+              mutateFired = true;
+            });
 
-          const dataInStore = client.cache.extract(true);
-          expect(dataInStore['Todo:99']).toEqual(
-            optimisticResponse.createTodo
-          );
-        }
-        render() {
-          return null;
+            const dataInStore = client.cache.extract(true);
+            expect(dataInStore["Todo:99"]).toEqual(
+              optimisticResponse.createTodo
+            );
+          }
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(mutateFired).toBeTruthy();
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(mutateFired).toBeTruthy();
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('allows for updating queries from a mutation', (resolve, reject) => {
+  itAsync("allows for updating queries from a mutation", (resolve, reject) => {
     const query: DocumentNode = gql`
       query todos {
         todo_list {
@@ -111,20 +112,20 @@ describe('graphql(mutation) query integration', () => {
 
     const mutationData = {
       createTodo: {
-        id: '99',
-        text: 'This one was created with a mutation.',
-        completed: true
-      }
+        id: "99",
+        text: "This one was created with a mutation.",
+        completed: true,
+      },
     };
 
     type MutationData = typeof mutationData;
 
     const optimisticResponse = {
       createTodo: {
-        id: '99',
-        text: 'Optimistically generated',
-        completed: true
-      }
+        id: "99",
+        text: "Optimistically generated",
+        completed: true,
+      },
     };
     interface QueryData {
       todo_list: {
@@ -148,13 +149,13 @@ describe('graphql(mutation) query integration', () => {
     };
 
     const expectedData = {
-      todo_list: { id: '123', title: 'how to apollo', tasks: [] }
+      todo_list: { id: "123", title: "how to apollo", tasks: [] },
     };
 
     const link = mockSingleLink(
       {
         request: { query },
-        result: { data: expectedData }
+        result: { data: expectedData },
       },
       { request: { query: mutation }, result: { data: mutationData } }
     );
@@ -165,7 +166,7 @@ describe('graphql(mutation) query integration', () => {
 
     type WithQueryChildProps = ChildProps<{}, QueryData>;
     const withMutation = graphql<WithQueryChildProps, MutationData>(mutation, {
-      options: () => ({ optimisticResponse, update })
+      options: () => ({ optimisticResponse, update }),
     });
 
     let renderCount = 0;
@@ -176,7 +177,7 @@ describe('graphql(mutation) query integration', () => {
         if (!this.props.data || !this.props.data.todo_list) return null;
         if (!this.props.data.todo_list.tasks.length) {
           act(() => {
-            this.props.mutate!().then(result => {
+            this.props.mutate!().then((result) => {
               expect(result && result.data).toEqual(mutationData);
             });
           });
@@ -186,23 +187,14 @@ describe('graphql(mutation) query integration', () => {
         switch (++renderCount) {
           case 1:
             expect(this.props.data.todo_list.tasks).toEqual([
-              optimisticResponse.createTodo
+              optimisticResponse.createTodo,
             ]);
             break;
           case 2:
             expect(this.props.data.todo_list.tasks).toEqual([
-              mutationData.createTodo
+              mutationData.createTodo,
             ]);
             break;
-          case 3:
-            if (IS_REACT_18) {
-              expect(this.props.data.todo_list.tasks).toEqual([
-                mutationData.createTodo
-              ]);
-            } else {
-              reject(`too many renders (${renderCount})`);
-            }
-            break;
           default:
             reject(`too many renders (${renderCount})`);
         }
@@ -220,151 +212,138 @@ describe('graphql(mutation) query integration', () => {
     );
 
     waitFor(() => {
-      if (IS_REACT_18) {
-        expect(renderCount).toBe(3);
-      } else {
-        expect(renderCount).toBe(2);
-      }
+      expect(renderCount).toBe(2);
     }).then(resolve, reject);
   });
 
-  itAsync('allows for updating queries from a mutation automatically', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query getMini($id: ID!) {
-        mini(id: $id) {
-          __typename
-          id
-          cover(maxWidth: 600, maxHeight: 400)
+  itAsync(
+    "allows for updating queries from a mutation automatically",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query getMini($id: ID!) {
+          mini(id: $id) {
+            __typename
+            id
+            cover(maxWidth: 600, maxHeight: 400)
+          }
         }
-      }
-    `;
-
-    const queryData = {
-      mini: {
-        id: 1,
-        __typename: 'Mini',
-        cover: 'image1'
-      }
-    };
+      `;
+
+      const queryData = {
+        mini: {
+          id: 1,
+          __typename: "Mini",
+          cover: "image1",
+        },
+      };
 
-    type Data = typeof queryData;
+      type Data = typeof queryData;
 
-    const variables = { id: 1 };
+      const variables = { id: 1 };
 
-    type Variables = typeof variables;
+      type Variables = typeof variables;
 
-    const mutation: DocumentNode = gql`
-      mutation($signature: String!) {
-        mini: submitMiniCoverS3DirectUpload(signature: $signature) {
-          __typename
-          id
-          cover(maxWidth: 600, maxHeight: 400)
+      const mutation: DocumentNode = gql`
+        mutation ($signature: String!) {
+          mini: submitMiniCoverS3DirectUpload(signature: $signature) {
+            __typename
+            id
+            cover(maxWidth: 600, maxHeight: 400)
+          }
         }
-      }
-    `;
-
-    const mutationData = {
-      mini: {
-        id: 1,
-        cover: 'image2',
-        __typename: 'Mini'
-      }
-    };
-
-    type MutationData = typeof mutationData;
+      `;
+
+      const mutationData = {
+        mini: {
+          id: 1,
+          cover: "image2",
+          __typename: "Mini",
+        },
+      };
 
-    interface MutationVariables {
-      signature: string;
-    }
+      type MutationData = typeof mutationData;
 
-    const link = mockSingleLink(
-      { request: { query, variables }, result: { data: queryData } },
-      {
-        request: { query: mutation, variables: { signature: '1233' } },
-        result: { data: mutationData }
+      interface MutationVariables {
+        signature: string;
       }
-    );
-    const cache = new Cache({ addTypename: false });
-    const client = new ApolloClient({ link, cache });
 
-    class Boundary extends React.Component<React.PropsWithChildren> {
-      componentDidCatch(e: any) {
-        reject(e);
-      }
-      render() {
-        // eslint-disable-next-line testing-library/no-node-access
-        return this.props.children;
-      }
-    }
+      const link = mockSingleLink(
+        { request: { query, variables }, result: { data: queryData } },
+        {
+          request: { query: mutation, variables: { signature: "1233" } },
+          result: { data: mutationData },
+        }
+      );
+      const cache = new Cache({ addTypename: false });
+      const client = new ApolloClient({ link, cache });
 
-    let count = 0;
-    const MutationContainer = graphql<MutationVariables, MutationData>(
-      mutation
-    )(
-      class extends React.Component<
-        ChildProps<MutationVariables, MutationData>
-      > {
+      class Boundary extends React.Component<React.PropsWithChildren> {
+        componentDidCatch(e: any) {
+          reject(e);
+        }
         render() {
-          if (count === 1) {
-            setTimeout(() => {
-              this.props.mutate!()
-                .then(result => {
-                  expect(result && result.data).toEqual(
-                    mutationData
-                  );
-                })
-                .catch(reject);
-            });
-          }
-          return null;
+          // eslint-disable-next-line testing-library/no-node-access
+          return this.props.children;
         }
       }
-    );
 
-    const Container = graphql<Variables, Data>(query)(
-      class extends React.Component<ChildProps<Variables, Data>> {
-        render() {
-          if (count === 1) {
-            if (IS_REACT_18) {
-              expect(this.props.data!.mini).toEqual(mutationData.mini);
-            } else {
-              expect(this.props.data!.mini).toEqual(queryData.mini);
+      let count = 0;
+      const MutationContainer = graphql<MutationVariables, MutationData>(
+        mutation
+      )(
+        class extends React.Component<
+          ChildProps<MutationVariables, MutationData>
+        > {
+          render() {
+            if (count === 1) {
+              setTimeout(() => {
+                this.props.mutate!()
+                  .then((result) => {
+                    expect(result && result.data).toEqual(mutationData);
+                  })
+                  .catch(reject);
+              });
             }
+            return null;
           }
-          if (count === 2) {
-            expect(this.props.data!.mini).toEqual(
-              mutationData.mini
+        }
+      );
+
+      const Container = graphql<Variables, Data>(query)(
+        class extends React.Component<ChildProps<Variables, Data>> {
+          render() {
+            if (count === 1) {
+              expect(this.props.data!.mini).toEqual(queryData.mini);
+            }
+            if (count === 2) {
+              expect(this.props.data!.mini).toEqual(mutationData.mini);
+            }
+            count++;
+
+            return (
+              <MutationContainer {...this.props.data!.mini} signature="1233" />
             );
           }
-          count++;
-
-          return (
-            <MutationContainer {...this.props.data!.mini} signature="1233" />
-          );
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Boundary>
-          <Container id={1} />
-        </Boundary>
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Boundary>
+            <Container id={1} />
+          </Boundary>
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toBe(2);
-      } else {
+      waitFor(() => {
         expect(count).toBe(3);
-      }
-    }).then(resolve, reject);
-  });
+      }).then(resolve, reject);
+    }
+  );
 
-  it('should be able to override the internal `ignoreResults` setting', async () => {
+  it("should be able to override the internal `ignoreResults` setting", async () => {
     const mutation: DocumentNode = gql`
-      mutation($signature: String!) {
+      mutation ($signature: String!) {
         mini: submitMiniCoverS3DirectUpload(signature: $signature) {
           __typename
           id
@@ -376,9 +355,9 @@ describe('graphql(mutation) query integration', () => {
     const mutationData = {
       mini: {
         id: 1,
-        cover: 'image2',
-        __typename: 'Mini'
-      }
+        cover: "image2",
+        __typename: "Mini",
+      },
     };
 
     type MutationData = typeof mutationData;
@@ -388,8 +367,8 @@ describe('graphql(mutation) query integration', () => {
     }
 
     const link = mockSingleLink({
-      request: { query: mutation, variables: { signature: '1233' } },
-      result: { data: mutationData }
+      request: { query: mutation, variables: { signature: "1233" } },
+      result: { data: mutationData },
     });
 
     const cache = new Cache({ addTypename: false });
@@ -408,10 +387,8 @@ describe('graphql(mutation) query integration', () => {
             case 0:
               expect(this.props.result!.loading).toBeFalsy();
               setTimeout(() => {
-                this.props.mutate!().then(result => {
-                  expect(result && result.data).toEqual(
-                    mutationData
-                  );
+                this.props.mutate!().then((result) => {
+                  expect(result && result.data).toEqual(mutationData);
                 });
               });
               break;
diff --git a/src/react/hoc/__tests__/mutations/recycled-queries.test.tsx b/src/react/hoc/__tests__/mutations/recycled-queries.test.tsx
--- a/src/react/hoc/__tests__/mutations/recycled-queries.test.tsx
+++ b/src/react/hoc/__tests__/mutations/recycled-queries.test.tsx
@@ -1,19 +1,21 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloCache, ApolloClient, MutationUpdaterFunction } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { MutationFunction } from '../../../types/types';
-import { mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-const IS_REACT_18 = React.version.startsWith('18')
-
-describe('graphql(mutation) update queries', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import {
+  ApolloCache,
+  ApolloClient,
+  MutationUpdaterFunction,
+} from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { MutationFunction } from "../../../types/types";
+import { mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("graphql(mutation) update queries", () => {
   // This is a long test that keeps track of a lot of stuff. It is testing
   // whether or not the `options.update` reducers will run even when a given
   // container component is unmounted.
@@ -33,7 +35,7 @@ describe('graphql(mutation) update queries', () => {
   //
   // There are also a lot more assertions on the way to make sure everything is
   // going as smoothly as planned.
-  it('will run `update` for a previously mounted component', async () => {
+  it("will run `update` for a previously mounted component", async () => {
     const query: DocumentNode = gql`
       query todos {
         todo_list {
@@ -68,10 +70,10 @@ describe('graphql(mutation) update queries', () => {
 
     const mutationData = {
       createTodo: {
-        id: '99',
-        text: 'This one was created with a mutation.',
-        completed: true
-      }
+        id: "99",
+        text: "This one was created with a mutation.",
+        completed: true,
+      },
     };
     type MutationData = typeof mutationData;
 
@@ -84,35 +86,33 @@ describe('graphql(mutation) update queries', () => {
     > = (proxy, result) => {
       todoUpdateQueryCount++;
       const data = JSON.parse(
-        JSON.stringify(
-          proxy.readQuery<QueryData>({ query })
-        ) // read from cache
+        JSON.stringify(proxy.readQuery<QueryData>({ query })) // read from cache
       );
       data!.todo_list.tasks.push(result.data!.createTodo); // update value
       proxy.writeQuery({ query, data }); // write to cache
     };
 
     const expectedData = {
-      todo_list: { id: '123', title: 'how to apollo', tasks: [] }
+      todo_list: { id: "123", title: "how to apollo", tasks: [] },
     };
 
     const link = mockSingleLink(
       {
         request: { query },
-        result: { data: expectedData }
+        result: { data: expectedData },
       },
       { request: { query: mutation }, result: { data: mutationData } },
       { request: { query: mutation }, result: { data: mutationData } }
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let mutate: MutationFunction<MutationData>;
 
     const MyMutation = graphql<{}, MutationData>(mutation, {
-      options: () => ({ update })
+      options: () => ({ update }),
     })(
       class extends React.Component<ChildProps<{}, MutationData>> {
         componentDidMount() {
@@ -128,6 +128,7 @@ describe('graphql(mutation) update queries', () => {
     let queryMountCount = 0;
     let queryUnmountCount = 0;
     let queryRenderCount = 0;
+    const testFailures: any[] = [];
 
     const MyQuery = graphql<{}, QueryData>(query)(
       class extends React.Component<ChildProps<{}, QueryData>> {
@@ -140,55 +141,60 @@ describe('graphql(mutation) update queries', () => {
         }
 
         render() {
-          switch (queryRenderCount) {
-            case 0:
-              expect(this.props.data!.loading).toBeTruthy();
-              expect(this.props.data!.todo_list).toBeFalsy();
-              break;
-            case 1:
-              expect(this.props.data!.loading).toBeFalsy();
-              if (!IS_REACT_18) {
+          try {
+            switch (queryRenderCount) {
+              case 0:
+                expect(this.props.data!.loading).toBeTruthy();
+                expect(this.props.data!.todo_list).toBeFalsy();
+                break;
+              case 1:
+                expect(this.props.data!.loading).toBeFalsy();
                 expect(this.props.data!.todo_list).toEqual({
-                  id: '123',
-                  title: 'how to apollo',
-                  tasks: []
+                  id: "123",
+                  title: "how to apollo",
+                  tasks: [],
                 });
-              }
-              break;
-            case 2:
-              expect(this.props.data!.loading).toBeFalsy();
-              expect(queryMountCount).toBe(1);
-              expect(this.props.data!.todo_list).toEqual({
-                id: '123',
-                title: 'how to apollo',
-                tasks: [
-                  {
-                    id: '99',
-                    text: 'This one was created with a mutation.',
-                    completed: true
-                  }
-                ]
-              });
-              break;
-            case 3:
-              expect(this.props.data!.todo_list).toEqual({
-                id: '123',
-                title: 'how to apollo',
-                tasks: [
-                  {
-                    id: '99',
-                    text: 'This one was created with a mutation.',
-                    completed: true
-                  },
-                  {
-                    id: '99',
-                    text: 'This one was created with a mutation.',
-                    completed: true
-                  }
-                ]
-              });
-              break;
-            default:
+                break;
+              case 2:
+                expect(this.props.data!.loading).toBeFalsy();
+                expect(queryMountCount).toBe(1);
+                expect(this.props.data!.todo_list).toEqual({
+                  id: "123",
+                  title: "how to apollo",
+                  tasks: [
+                    {
+                      id: "99",
+                      text: "This one was created with a mutation.",
+                      completed: true,
+                    },
+                  ],
+                });
+                break;
+              case 3:
+                expect(this.props.data!.loading).toBeFalsy();
+                expect(queryMountCount).toBe(1);
+                expect(this.props.data!.todo_list).toEqual({
+                  id: "123",
+                  title: "how to apollo",
+                  tasks: [
+                    {
+                      id: "99",
+                      text: "This one was created with a mutation.",
+                      completed: true,
+                    },
+                    {
+                      id: "99",
+                      text: "This one was created with a mutation.",
+                      completed: true,
+                    },
+                  ],
+                });
+                break;
+              default:
+                throw new Error("too many rerenders");
+            }
+          } catch (e) {
+            testFailures.push(e);
           }
 
           queryRenderCount += 1;
@@ -209,35 +215,47 @@ describe('graphql(mutation) update queries', () => {
       </ApolloProvider>
     );
 
-    setTimeout(() => {
-      mutate();
+    let resolveLastTimeout: () => void;
+    const allTimeoutsFinished = new Promise<void>((r) => {
+      resolveLastTimeout = r;
+    });
 
-      setTimeout(() => {
-        if (IS_REACT_18) {
-          expect(queryUnmountCount).toBe(1);
-        } else {
-          expect(queryUnmountCount).toBe(0);
+    const catchingSetTimeout = (cb: (args: void) => void, ms: number) => {
+      return setTimeout(() => {
+        try {
+          cb();
+        } catch (e) {
+          testFailures.push(e);
         }
+      }, ms);
+    };
+
+    catchingSetTimeout(() => {
+      mutate();
+
+      catchingSetTimeout(() => {
+        expect(queryUnmountCount).toBe(0);
         query1Unmount();
         expect(queryUnmountCount).toBe(1);
 
-        setTimeout(() => {
+        catchingSetTimeout(() => {
           mutate();
 
-          setTimeout(() => {
+          catchingSetTimeout(() => {
             const { unmount: query2Unmount } = render(
               <ApolloProvider client={client}>
                 <MyQuery />
               </ApolloProvider>
             );
 
-            setTimeout(() => {
+            catchingSetTimeout(() => {
               mutationUnmount();
               query2Unmount();
 
               expect(todoUpdateQueryCount).toBe(2);
               expect(queryMountCount).toBe(2);
               expect(queryUnmountCount).toBe(2);
+              resolveLastTimeout!();
             }, 5);
           }, 5);
         }, 5);
@@ -245,13 +263,15 @@ describe('graphql(mutation) update queries', () => {
     }, 5);
 
     await waitFor(() => {
-      if (!IS_REACT_18) {
-        expect(queryRenderCount).toBe(4);
-      }
+      expect(queryRenderCount).toBe(4);
     });
+    await allTimeoutsFinished;
+    if (testFailures.length > 0) {
+      throw testFailures[0];
+    }
   });
 
-  it('will run `refetchQueries` for a recycled queries', async () => {
+  it("will run `refetchQueries` for a recycled queries", async () => {
     const mutation: DocumentNode = gql`
       mutation createTodo {
         createTodo {
@@ -264,10 +284,10 @@ describe('graphql(mutation) update queries', () => {
 
     const mutationData = {
       createTodo: {
-        id: '99',
-        text: 'This one was created with a mutation.',
-        completed: true
-      }
+        id: "99",
+        text: "This one was created with a mutation.",
+        completed: true,
+      },
     };
 
     type MutationData = typeof mutationData;
@@ -299,28 +319,28 @@ describe('graphql(mutation) update queries', () => {
     }
 
     const data = {
-      todo_list: { id: '123', title: 'how to apollo', tasks: [] }
+      todo_list: { id: "123", title: "how to apollo", tasks: [] },
     };
 
     const updatedData = {
       todo_list: {
-        id: '123',
-        title: 'how to apollo',
-        tasks: [mutationData.createTodo]
-      }
+        id: "123",
+        title: "how to apollo",
+        tasks: [mutationData.createTodo],
+      },
     };
 
     const link = mockSingleLink(
-      { request: { query, variables: { id: '123' } }, result: { data } },
+      { request: { query, variables: { id: "123" } }, result: { data } },
       { request: { query: mutation }, result: { data: mutationData } },
       {
-        request: { query, variables: { id: '123' } },
-        result: { data: updatedData }
+        request: { query, variables: { id: "123" } },
+        result: { data: updatedData },
       }
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let mutate: MutationFunction<MutationData>;
@@ -357,23 +377,19 @@ describe('graphql(mutation) update queries', () => {
             case 1:
               expect(this.props.data!.loading).toBeFalsy();
               expect(this.props.data!.todo_list).toEqual({
-                id: '123',
-                title: 'how to apollo',
-                tasks: []
+                id: "123",
+                title: "how to apollo",
+                tasks: [],
               });
               break;
             case 2:
               expect(this.props.data!.loading).toBeFalsy();
               expect(queryMountCount).toBe(1);
-              expect(this.props.data!.todo_list).toEqual(
-                updatedData.todo_list
-              );
+              expect(this.props.data!.todo_list).toEqual(updatedData.todo_list);
               break;
             case 3:
               expect(this.props.data!.loading).toBeFalsy();
-              expect(this.props.data!.todo_list).toEqual(
-                updatedData.todo_list
-              );
+              expect(this.props.data!.todo_list).toEqual(updatedData.todo_list);
               break;
             default:
           }
@@ -396,7 +412,7 @@ describe('graphql(mutation) update queries', () => {
     );
 
     setTimeout(() => {
-      mutate({ refetchQueries: [{ query, variables: { id: '123' } }] }).then(
+      mutate({ refetchQueries: [{ query, variables: { id: "123" } }] }).then(
         () => {
           setTimeout(() => {
             render(
diff --git a/src/react/hoc/__tests__/queries/api.test.tsx b/src/react/hoc/__tests__/queries/api.test.tsx
--- a/src/react/hoc/__tests__/queries/api.test.tsx
+++ b/src/react/hoc/__tests__/queries/api.test.tsx
@@ -1,16 +1,16 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-describe('[queries] api', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("[queries] api", () => {
   const consoleWarn = console.warn;
   beforeAll(() => {
     console.warn = () => null;
@@ -20,7 +20,7 @@ describe('[queries] api', () => {
     console.warn = consoleWarn;
   });
 
-  itAsync('exposes refetch as part of the props api', (resolve, reject) => {
+  itAsync("exposes refetch as part of the props api", (resolve, reject) => {
     const query: DocumentNode = gql`
       query people($first: Int) {
         allPeople(first: $first) {
@@ -31,7 +31,7 @@ describe('[queries] api', () => {
       }
     `;
     const variables = { first: 1 };
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     const link = mockSingleLink(
       { request: { query, variables }, result: { data: data1 } },
       { request: { query, variables }, result: { data: data1 } },
@@ -39,7 +39,7 @@ describe('[queries] api', () => {
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let hasRefetched = false,
@@ -76,9 +76,7 @@ describe('[queries] api', () => {
                   .refetch({ first: 2 }) // new variables
                   .then((response: any) => {
                     expect(response.data).toEqual(data1);
-                    expect(data!.allPeople).toEqual(
-                      data1.allPeople
-                    );
+                    expect(data!.allPeople).toEqual(data1.allPeople);
                     done = true;
                   });
               })
@@ -105,51 +103,56 @@ describe('[queries] api', () => {
     waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
   });
 
-  itAsync('exposes subscribeToMore as part of the props api', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "exposes subscribeToMore as part of the props api",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const link = mockSingleLink({
-      request: { query },
-      result: { data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } } }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    // example of loose typing
-    const Container = graphql(query)(
-      class extends React.Component<ChildProps> {
-        render() {
-          const { data } = this.props;
-          if (data && !data.loading) {
-            expect(data!.subscribeToMore).toBeTruthy();
-            expect(data!.subscribeToMore instanceof Function).toBeTruthy();
-            done = true;
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: {
+          data: { allPeople: { people: [{ name: "Luke Skywalker" }] } },
+        },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      // example of loose typing
+      const Container = graphql(query)(
+        class extends React.Component<ChildProps> {
+          render() {
+            const { data } = this.props;
+            if (data && !data.loading) {
+              expect(data!.subscribeToMore).toBeTruthy();
+              expect(data!.subscribeToMore instanceof Function).toBeTruthy();
+              done = true;
+            }
+            return null;
           }
-          return null;
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
+    }
+  );
 
-  itAsync('exposes fetchMore as part of the props api', (resolve, reject) => {
+  itAsync("exposes fetchMore as part of the props api", (resolve, reject) => {
     const query: DocumentNode = gql`
       query people($skip: Int, $first: Int) {
         allPeople(first: $first, skip: $skip) {
@@ -159,8 +162,8 @@ describe('[queries] api', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const data1 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+    const data1 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
 
     type Data = typeof data;
 
@@ -175,12 +178,12 @@ describe('[queries] api', () => {
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
     const Container = graphql<{}, Data, Variables>(query, {
-      options: () => ({ variables })
+      options: () => ({ variables }),
     })(
       class extends React.Component<ChildProps<{}, Data, Variables>> {
         componentDidUpdate() {
@@ -188,16 +191,16 @@ describe('[queries] api', () => {
           if (count === 0) {
             expect(props.data!.fetchMore).toBeTruthy();
             expect(props.data!.fetchMore instanceof Function).toBeTruthy();
-            props.data!
-              .fetchMore({
+            props
+              .data!.fetchMore({
                 variables: { skip: 2 },
                 updateQuery: (prev: any, { fetchMoreResult }) => ({
                   allPeople: {
                     people: prev.allPeople.people.concat(
                       fetchMoreResult!.allPeople.people
-                    )
-                  }
-                })
+                    ),
+                  },
+                }),
               })
               .then((result: any) => {
                 expect(result.data.allPeople.people).toEqual(
@@ -214,7 +217,7 @@ describe('[queries] api', () => {
             // This ends the test (passing).
             setTimeout(() => resolve(), 20);
           } else {
-            throw new Error('should not reach this point');
+            throw new Error("should not reach this point");
           }
           count++;
         }
@@ -234,100 +237,101 @@ describe('[queries] api', () => {
     );
   });
 
-  itAsync('reruns props function after query results change via fetchMore', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people($cursor: Int) {
-        allPeople(cursor: $cursor) {
-          cursor
-          people {
-            name
+  itAsync(
+    "reruns props function after query results change via fetchMore",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people($cursor: Int) {
+          allPeople(cursor: $cursor) {
+            cursor
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const vars1 = { cursor: undefined };
-    const data1 = {
-      allPeople: { cursor: 1, people: [{ name: 'Luke Skywalker' }] }
-    };
-    const vars2 = { cursor: 1 };
-    const data2 = {
-      allPeople: { cursor: 2, people: [{ name: 'Leia Skywalker' }] }
-    };
-
-    type Data = typeof data1;
-    type Variables = { cursor: number | undefined };
-
-    const link = mockSingleLink(
-      { request: { query, variables: vars1 }, result: { data: data1 } },
-      { request: { query, variables: vars2 }, result: { data: data2 } }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    let isUpdated = false;
-
-    type FinalProps = {
-      loading: boolean;
-      people?: { name: string }[];
-      getMorePeople?: () => void;
-    };
-
-    let done = false;
-    const Container = graphql<{}, Data, Variables, FinalProps>(query, {
-      props({ data }) {
-        const { loading, allPeople, fetchMore } = data!;
-
-        if (loading) return { loading };
-        const { cursor, people } = allPeople!;
-        return {
-          loading: false,
-          people,
-          getMorePeople: () =>
-            fetchMore({
-              variables: { cursor },
-              updateQuery(prev, { fetchMoreResult }) {
-                const {
-                  allPeople: { cursor, people }
-                } = fetchMoreResult!;
-                return {
-                  allPeople: {
-                    cursor,
-                    people: [...people, ...prev.allPeople.people]
-                  }
-                };
+      `;
+      const vars1 = { cursor: undefined };
+      const data1 = {
+        allPeople: { cursor: 1, people: [{ name: "Luke Skywalker" }] },
+      };
+      const vars2 = { cursor: 1 };
+      const data2 = {
+        allPeople: { cursor: 2, people: [{ name: "Leia Skywalker" }] },
+      };
+
+      type Data = typeof data1;
+      type Variables = { cursor: number | undefined };
+
+      const link = mockSingleLink(
+        { request: { query, variables: vars1 }, result: { data: data1 } },
+        { request: { query, variables: vars2 }, result: { data: data2 } }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      let isUpdated = false;
+
+      type FinalProps = {
+        loading: boolean;
+        people?: { name: string }[];
+        getMorePeople?: () => void;
+      };
+
+      let done = false;
+      const Container = graphql<{}, Data, Variables, FinalProps>(query, {
+        props({ data }) {
+          const { loading, allPeople, fetchMore } = data!;
+
+          if (loading) return { loading };
+          const { cursor, people } = allPeople!;
+          return {
+            loading: false,
+            people,
+            getMorePeople: () =>
+              fetchMore({
+                variables: { cursor },
+                updateQuery(prev, { fetchMoreResult }) {
+                  const {
+                    allPeople: { cursor, people },
+                  } = fetchMoreResult!;
+                  return {
+                    allPeople: {
+                      cursor,
+                      people: [...people, ...prev.allPeople.people],
+                    },
+                  };
+                },
+              }),
+          };
+        },
+      })(
+        class extends React.Component<FinalProps> {
+          render() {
+            if (!this.props.loading) {
+              if (isUpdated) {
+                expect(this.props.people!.length).toBe(2);
+                done = true;
+              } else {
+                isUpdated = true;
+                expect(this.props.people).toEqual(data1.allPeople.people);
+                this.props.getMorePeople!();
               }
-            })
-        };
-      }
-    })(
-      class extends React.Component<FinalProps> {
-        render() {
-          if (!this.props.loading) {
-            if (isUpdated) {
-              expect(this.props.people!.length).toBe(2);
-              done = true;
-            } else {
-              isUpdated = true;
-              expect(this.props.people).toEqual(
-                data1.allPeople.people
-              );
-              this.props.getMorePeople!();
             }
-          }
 
-          return null;
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(done).toBe(true)).then(resolve, reject);
-  });
+      waitFor(() => expect(done).toBe(true)).then(resolve, reject);
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/queries/errors.test.tsx b/src/react/hoc/__tests__/queries/errors.test.tsx
--- a/src/react/hoc/__tests__/queries/errors.test.tsx
+++ b/src/react/hoc/__tests__/queries/errors.test.tsx
@@ -1,21 +1,19 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { withState } from './recomposeWithState';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { QueryResult } from '../../../types/types';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { Query } from '../../../components/Query';
-import { graphql } from '../../graphql';
-import { ChildProps, DataValue } from '../../types';
-
-const IS_REACT_18 = React.version.startsWith('18');
-
-describe('[queries] errors', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { withState } from "./recomposeWithState";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { QueryResult } from "../../../types/types";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { Query } from "../../../components/Query";
+import { graphql } from "../../graphql";
+import { ChildProps, DataValue } from "../../types";
+
+describe("[queries] errors", () => {
   let error: typeof console.error;
   beforeEach(() => {
     error = console.error;
@@ -26,7 +24,7 @@ describe('[queries] errors', () => {
   });
 
   // errors
-  itAsync('does not swallow children errors', (resolve, reject) => {
+  itAsync("does not swallow children errors", (resolve, reject) => {
     let done = false;
     const query: DocumentNode = gql`
       query people {
@@ -37,14 +35,14 @@ describe('[queries] errors', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     class ErrorBoundary extends React.Component<React.PropsWithChildren> {
@@ -77,7 +75,7 @@ describe('[queries] errors', () => {
     }).then(resolve, reject);
   });
 
-  it('can unmount without error', () => {
+  it("can unmount without error", () => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -87,14 +85,14 @@ describe('[queries] errors', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const ContainerWithData = graphql(query)(() => null);
@@ -112,8 +110,8 @@ describe('[queries] errors', () => {
     }
   });
 
-  itAsync('passes any GraphQL errors in props', (resolve, reject) => {
-    let done = false
+  itAsync("passes any GraphQL errors in props", (resolve, reject) => {
+    let done = false;
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -125,11 +123,11 @@ describe('[queries] errors', () => {
     `;
     const link = mockSingleLink({
       request: { query },
-      error: new Error('boo')
+      error: new Error("boo"),
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const ErrorContainer = graphql(query)(
@@ -138,7 +136,7 @@ describe('[queries] errors', () => {
           const { data } = this.props;
           expect(data!.error).toBeTruthy();
           expect(data!.error!.networkError).toBeTruthy();
-          done = true
+          done = true;
         }
         render() {
           return null;
@@ -157,7 +155,7 @@ describe('[queries] errors', () => {
     }).then(resolve, reject);
   });
 
-  describe('uncaught exceptions', () => {
+  describe("uncaught exceptions", () => {
     const consoleWarn = console.warn;
     beforeAll(() => {
       console.warn = () => null;
@@ -173,13 +171,13 @@ describe('[queries] errors', () => {
     }
     beforeEach(() => {
       unhandled = [];
-      process.on('unhandledRejection', handle);
+      process.on("unhandledRejection", handle);
     });
     afterEach(() => {
-      process.removeListener('unhandledRejection', handle);
+      process.removeListener("unhandledRejection", handle);
     });
 
-    it('does not log when you change variables resulting in an error', async () => {
+    it("does not log when you change variables resulting in an error", async () => {
       const query: DocumentNode = gql`
         query people($var: Int) {
           allPeople(first: $var) {
@@ -190,21 +188,21 @@ describe('[queries] errors', () => {
         }
       `;
       const var1 = { var: 1 };
-      const data = { allPeople: { people: { name: 'Luke Skywalker' } } };
+      const data = { allPeople: { people: { name: "Luke Skywalker" } } };
       const var2 = { var: 2 };
       const link = mockSingleLink(
         {
           request: { query, variables: var1 },
-          result: { data }
+          result: { data },
         },
         {
           request: { query, variables: var2 },
-          error: new Error('boo')
+          error: new Error("boo"),
         }
       );
       const client = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       type Data = typeof data;
@@ -217,7 +215,11 @@ describe('[queries] errors', () => {
 
       let iteration = 0;
       let done = false;
-      const ErrorContainer = withState('var', 'setVar', 1)(
+      const ErrorContainer = withState(
+        "var",
+        "setVar",
+        1
+      )(
         graphql<Props, Data, Vars>(query)(
           class extends React.Component<ChildProps<Props, Data, Vars>> {
             componentDidUpdate() {
@@ -260,14 +262,14 @@ describe('[queries] errors', () => {
 
       await waitFor(() => {
         expect(iteration).toBe(3);
-      })
+      });
       await waitFor(() => {
         expect(done).toBeTruthy();
       });
     });
   });
 
-  it('will not log a warning when there is an error that is not caught in the render method when using query', () =>
+  it("will not log a warning when there is an error that is not caught in the render method when using query", () =>
     new Promise<void>((resolve, reject) => {
       const query: DocumentNode = gql`
         query people {
@@ -287,11 +289,11 @@ describe('[queries] errors', () => {
 
       const link = mockSingleLink({
         request: { query },
-        error: new Error('oops')
+        error: new Error("oops"),
       });
       const client = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       const origError = console.error;
@@ -315,7 +317,7 @@ describe('[queries] errors', () => {
                 expect(errorMock.mock.calls.length).toBe(0);
                 break;
               default:
-                throw new Error('Too many renders.');
+                throw new Error("Too many renders.");
             }
           } catch (error) {
             reject(error);
@@ -337,91 +339,80 @@ describe('[queries] errors', () => {
       }).then(resolve, reject);
     }));
 
-  itAsync('passes any cached data when there is a GraphQL error', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "passes any cached data when there is a GraphQL error",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    type Data = typeof data;
-    const link = mockSingleLink(
-      { request: { query }, result: { data } },
-      { request: { query }, error: new Error('No Network Connection') }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      type Data = typeof data;
+      const link = mockSingleLink(
+        { request: { query }, result: { data } },
+        { request: { query }, error: new Error("No Network Connection") }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let count = 0;
-    const Container = graphql<{}, Data>(query, {
-      options: { notifyOnNetworkStatusChange: true }
-    })(
-      class extends React.Component<ChildProps<{}, Data>> {
-        componentDidUpdate() {
-          const { props } = this;
-          try {
-            switch (count++) {
-              case 0:
-                expect(props.data!.allPeople).toEqual(
-                  data.allPeople
-                );
-                setTimeout(() => {
-                  props.data!.refetch().catch(() => null);
-                });
-                break;
-              case 1:
-                if (IS_REACT_18) {
-                  expect(props.data!.loading).toBeFalsy();
-                } else {
+      let count = 0;
+      const Container = graphql<{}, Data>(query, {
+        options: { notifyOnNetworkStatusChange: true },
+      })(
+        class extends React.Component<ChildProps<{}, Data>> {
+          componentDidUpdate() {
+            const { props } = this;
+            try {
+              switch (count++) {
+                case 0:
+                  expect(props.data!.allPeople).toEqual(data.allPeople);
+                  setTimeout(() => {
+                    props.data!.refetch().catch(() => null);
+                  });
+                  break;
+                case 1:
                   expect(props.data!.loading).toBeTruthy();
-                }
-                expect(props.data!.allPeople).toEqual(
-                  data.allPeople
-                );
-                break;
-              case 2:
-                expect(props.data!.loading).toBeFalsy();
-                expect(props.data!.error).toBeTruthy();
-                expect(props.data!.allPeople).toEqual(
-                  data.allPeople
-                );
-                break;
-              default:
-                throw new Error('Unexpected fall through');
+                  expect(props.data!.allPeople).toEqual(data.allPeople);
+                  break;
+                case 2:
+                  expect(props.data!.loading).toBeFalsy();
+                  expect(props.data!.error).toBeTruthy();
+                  expect(props.data!.allPeople).toEqual(data.allPeople);
+                  break;
+                default:
+                  throw new Error("Unexpected fall through");
+              }
+            } catch (e) {
+              reject(e);
             }
-          } catch (e) {
-            reject(e);
           }
-        }
 
-        render() {
-          return null;
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toBe(2);
-      } else {
-        expect(count).toBe(3)
-      }
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(count).toBe(3);
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('can refetch after there was a network error', (resolve, reject) => {
+  itAsync("can refetch after there was a network error", (resolve, reject) => {
     const query: DocumentNode = gql`
       query somethingelse {
         allPeople(first: 1) {
@@ -431,24 +422,24 @@ describe('[queries] errors', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const dataTwo = { allPeople: { people: [{ name: 'Princess Leia' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+    const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } };
 
     type Data = typeof data;
     const link = mockSingleLink(
       { request: { query }, result: { data } },
-      { request: { query }, error: new Error('This is an error!') },
+      { request: { query }, error: new Error("This is an error!") },
       { request: { query }, result: { data: dataTwo } }
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
     const noop = () => null;
     const Container = graphql<{}, Data>(query, {
-      options: { notifyOnNetworkStatusChange: true }
+      options: { notifyOnNetworkStatusChange: true },
     })(
       class extends React.Component<ChildProps<{}, Data>> {
         componentDidUpdate() {
@@ -459,16 +450,12 @@ describe('[queries] errors', () => {
                 props
                   .data!.refetch()
                   .then(() => {
-                    reject('Expected error value on first refetch.');
+                    reject("Expected error value on first refetch.");
                   })
                   .catch(noop);
                 break;
               case 1:
-                if (IS_REACT_18) {
-                  expect(props.data!.loading).toBeFalsy();
-                } else {
-                  expect(props.data!.loading).toBeTruthy();
-                }
+                expect(props.data!.loading).toBeTruthy();
                 break;
               case 2:
                 expect(props.data!.loading).toBeFalsy();
@@ -477,7 +464,7 @@ describe('[queries] errors', () => {
                   .data!.refetch()
                   .then(noop)
                   .catch(() => {
-                    reject('Expected good data on second refetch.');
+                    reject("Expected good data on second refetch.");
                   });
                 break;
               case 3:
@@ -486,12 +473,10 @@ describe('[queries] errors', () => {
               case 4:
                 expect(props.data!.loading).toBeFalsy();
                 expect(props.data!.error).toBeFalsy();
-                expect(props.data!.allPeople).toEqual(
-                  dataTwo.allPeople
-                );
+                expect(props.data!.allPeople).toEqual(dataTwo.allPeople);
                 break;
               default:
-                throw new Error('Unexpected fall through');
+                throw new Error("Unexpected fall through");
             }
           } catch (e) {
             reject(e);
@@ -511,232 +496,139 @@ describe('[queries] errors', () => {
     );
 
     waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toBe(2)
-      } else {
-        expect(count).toBe(5)
-      }
+      expect(count).toBe(5);
     }).then(resolve, reject);
   });
 
-  itAsync('does not throw/console.err an error after a component that received a network error is unmounted', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query somethingelse {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "does not throw/console.err an error after a component that received a network error is unmounted",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query somethingelse {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
 
-    type Data = typeof data;
-    const link = mockSingleLink(
-      { request: { query }, result: { data } },
-      { request: { query }, error: new Error('This is an error!') }
-    );
+      type Data = typeof data;
+      const link = mockSingleLink(
+        { request: { query }, result: { data } },
+        { request: { query }, error: new Error("This is an error!") }
+      );
 
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-    let count = 0;
-    const noop = () => null;
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+      let count = 0;
+      const noop = () => null;
 
-    interface ContainerOwnProps {
-      hideContainer: Function;
-    }
+      interface ContainerOwnProps {
+        hideContainer: Function;
+      }
 
-    interface QueryChildProps {
-      data: DataValue<Data>;
-      hideContainer: Function;
-    }
+      interface QueryChildProps {
+        data: DataValue<Data>;
+        hideContainer: Function;
+      }
 
-    let done = false;
-    const Container = graphql<ContainerOwnProps, Data, {}, QueryChildProps>(
-      query,
-      {
-        options: { notifyOnNetworkStatusChange: true },
-        props: something => {
-          return {
-            data: something.data!,
-            hideContainer: something!.ownProps.hideContainer
-          };
+      let done = false;
+      const Container = graphql<ContainerOwnProps, Data, {}, QueryChildProps>(
+        query,
+        {
+          options: { notifyOnNetworkStatusChange: true },
+          props: (something) => {
+            return {
+              data: something.data!,
+              hideContainer: something!.ownProps.hideContainer,
+            };
+          },
         }
-      }
-    )(
-      class extends React.Component<ChildProps<QueryChildProps, Data>> {
-        componentDidUpdate() {
-          const { props } = this;
-          try {
-            switch (count++) {
-              case 0:
-                props
-                  .data!.refetch()
-                  .then(() => {
-                    reject('Expected error value on first refetch.');
-                  })
-                  .catch(noop);
-                break;
-              case 2:
-                expect(props.data!.loading).toBeFalsy();
-                expect(props.data!.error).toBeTruthy();
-                const origError = console.error;
-                const errorMock = jest.fn();
-                console.error = errorMock;
-                props.hideContainer();
-                setTimeout(() => {
-                  expect(errorMock.mock.calls.length).toEqual(0);
-                  console.error = origError;
-                  done = true;
-                }, 100);
-                break;
-              default:
-                if (count < 2) {
-                  throw new Error('Unexpected fall through');
-                }
+      )(
+        class extends React.Component<ChildProps<QueryChildProps, Data>> {
+          componentDidUpdate() {
+            const { props } = this;
+            try {
+              switch (count++) {
+                case 0:
+                  props
+                    .data!.refetch()
+                    .then(() => {
+                      reject("Expected error value on first refetch.");
+                    })
+                    .catch(noop);
+                  break;
+                case 2:
+                  expect(props.data!.loading).toBeFalsy();
+                  expect(props.data!.error).toBeTruthy();
+                  const origError = console.error;
+                  const errorMock = jest.fn();
+                  console.error = errorMock;
+                  props.hideContainer();
+                  setTimeout(() => {
+                    expect(errorMock.mock.calls.length).toEqual(0);
+                    console.error = origError;
+                    done = true;
+                  }, 100);
+                  break;
+                default:
+                  if (count < 2) {
+                    throw new Error("Unexpected fall through");
+                  }
+              }
+            } catch (err) {
+              reject(err);
             }
-          } catch (err) {
-            reject(err);
           }
-        }
-        render() {
-          return null;
-        }
-      }
-    );
-
-    class Switcher extends React.Component<any, any> {
-      constructor(props: any) {
-        super(props);
-        this.state = {
-          showContainer: true
-        };
-      }
-      render() {
-        const {
-          state: { showContainer }
-        } = this;
-        if (showContainer) {
-          return (
-            <Container
-              hideContainer={() => this.setState({ showContainer: false })}
-            />
-          );
-        }
-        return null;
-      }
-    }
-
-    render(
-      <ApolloProvider client={client}>
-        <Switcher />
-      </ApolloProvider>
-    );
-
-    waitFor(() => {
-      if (!IS_REACT_18) {
-        expect(done).toBeTruthy()
-      }
-    }).then(resolve, reject);
-  });
-
-  itAsync('correctly sets loading state on remount after a network error', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query somethingelse {
-        allPeople(first: 1) {
-          people {
-            name
+          render() {
+            return null;
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const dataTwo = { allPeople: { people: [{ name: 'Princess Leia' }] } };
-
-    type Data = typeof data;
-    const link = mockSingleLink(
-      { request: { query }, error: new Error('This is an error!') },
-      { request: { query }, result: { data: dataTwo } }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      );
 
-    let count = 0;
-    type ContainerOwnProps = { toggle: () => void };
-    const Container = graphql<ContainerOwnProps, Data>(query, {
-      options: { notifyOnNetworkStatusChange: true }
-    })(
-      class extends React.Component<ChildProps<ContainerOwnProps, Data>> {
+      class Switcher extends React.Component<any, any> {
+        constructor(props: any) {
+          super(props);
+          this.state = {
+            showContainer: true,
+          };
+        }
         render() {
-          switch (count) {
-            case 0:
-              expect(this.props.data!.loading).toBe(true);
-              break;
-            case 1:
-              expect(this.props.data!.loading).toBe(false);
-              expect(this.props.data!.error!.networkError!.message).toMatch(
-                /This is an error/
-              );
-              // unmount this component
-              setTimeout(() => {
-                this.props.toggle();
-              }, 0);
-              setTimeout(() => {
-                // remount after 50 ms
-                this.props.toggle();
-              }, 50);
-              break;
-            case 2:
-              expect(this.props.data!.loading).toBe(true);
-              break;
-            case 3:
-              expect(this.props.data!.loading).toBe(false);
-              expect(this.props.data!.allPeople).toEqual(dataTwo.allPeople);
-              break;
-            default:
-              throw new Error('Too many renders.');
+          const {
+            state: { showContainer },
+          } = this;
+          if (showContainer) {
+            return (
+              <Container
+                hideContainer={() => this.setState({ showContainer: false })}
+              />
+            );
           }
-          count += 1;
-
           return null;
         }
       }
-    );
 
-    type Toggle = () => void;
-    type OwnProps = { children: (toggle: Toggle) => any };
-    class Manager extends React.Component<OwnProps, { show: boolean }> {
-      constructor(props: any) {
-        super(props);
-        this.state = { show: true };
-      }
-      render() {
-        if (!this.state.show) return null;
-        // eslint-disable-next-line testing-library/no-node-access
-        return this.props.children(() =>
-          this.setState(({ show }) => ({ show: !show }))
-        );
-      }
-    }
-
-    render(
-      <ApolloProvider client={client}>
-        <Manager>{(toggle: Toggle) => <Container toggle={toggle} />}</Manager>
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Switcher />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(count).toBe(4)).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBeTruthy();
+      }).then(resolve, reject);
+    }
+  );
 
-  describe('errorPolicy', () => {
-    itAsync('passes any GraphQL errors in props along with data', (resolve, reject) => {
-      let done = false;
+  itAsync(
+    "correctly sets loading state on remount after a network error",
+    (resolve, reject) => {
       const query: DocumentNode = gql`
-        query people {
+        query somethingelse {
           allPeople(first: 1) {
             people {
               name
@@ -744,107 +636,206 @@ describe('[queries] errors', () => {
           }
         }
       `;
-      const link = mockSingleLink({
-        request: { query },
-        result: {
-          data: {
-            allPeople: {
-              people: null
-            }
-          },
-          errors: [new Error('this is an error')]
-        }
-      });
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } };
 
+      type Data = typeof data;
+      const link = mockSingleLink(
+        { request: { query }, error: new Error("This is an error!") },
+        { request: { query }, result: { data: dataTwo } }
+      );
       const client = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
-      const ErrorContainer = graphql(query, {
-        options: { errorPolicy: 'all' }
+      let count = 0;
+      type ContainerOwnProps = { toggle: () => void };
+      const Container = graphql<ContainerOwnProps, Data>(query, {
+        options: { notifyOnNetworkStatusChange: true },
       })(
-        class extends React.Component<ChildProps> {
-          componentDidUpdate() {
-            const { data } = this.props;
-            expect(data!.error).toBeTruthy();
-            expect(data!.error!.graphQLErrors[0].message).toEqual(
-              'this is an error'
-            );
-            expect(data).toMatchObject({ allPeople: { people: null } });
-            done = true;
-          }
+        class extends React.Component<ChildProps<ContainerOwnProps, Data>> {
           render() {
+            switch (count) {
+              case 0:
+                expect(this.props.data!.loading).toBe(true);
+                break;
+              case 1:
+                expect(this.props.data!.loading).toBe(false);
+                expect(this.props.data!.error!.networkError!.message).toMatch(
+                  /This is an error/
+                );
+                // unmount this component
+                setTimeout(() => {
+                  this.props.toggle();
+                }, 0);
+                setTimeout(() => {
+                  // remount after 50 ms
+                  this.props.toggle();
+                }, 50);
+                break;
+              case 2:
+                expect(this.props.data!.loading).toBe(true);
+                break;
+              case 3:
+                expect(this.props.data!.loading).toBe(false);
+                expect(this.props.data!.allPeople).toEqual(dataTwo.allPeople);
+                break;
+              default:
+                throw new Error("Too many renders.");
+            }
+            count += 1;
+
             return null;
           }
         }
       );
 
+      type Toggle = () => void;
+      type OwnProps = { children: (toggle: Toggle) => any };
+      class Manager extends React.Component<OwnProps, { show: boolean }> {
+        constructor(props: any) {
+          super(props);
+          this.state = { show: true };
+        }
+        render() {
+          if (!this.state.show) return null;
+          // eslint-disable-next-line testing-library/no-node-access
+          return this.props.children(() =>
+            this.setState(({ show }) => ({ show: !show }))
+          );
+        }
+      }
+
       render(
         <ApolloProvider client={client}>
-          <ErrorContainer />
+          <Manager>{(toggle: Toggle) => <Container toggle={toggle} />}</Manager>
         </ApolloProvider>
       );
 
-      waitFor(() => {
-        expect(done).toBe(true);
-      }).then(resolve, reject);
-    });
+      waitFor(() => expect(count).toBe(4)).then(resolve, reject);
+    }
+  );
+
+  describe("errorPolicy", () => {
+    itAsync(
+      "passes any GraphQL errors in props along with data",
+      (resolve, reject) => {
+        let done = false;
+        const query: DocumentNode = gql`
+          query people {
+            allPeople(first: 1) {
+              people {
+                name
+              }
+            }
+          }
+        `;
+        const link = mockSingleLink({
+          request: { query },
+          result: {
+            data: {
+              allPeople: {
+                people: null,
+              },
+            },
+            errors: [new Error("this is an error")],
+          },
+        });
 
-    itAsync('passes any GraphQL errors in props along with data [component]', (resolve, reject) => {
-      let done = false;
-      const query: DocumentNode = gql`
-        query people {
-          allPeople(first: 1) {
-            people {
-              name
+        const client = new ApolloClient({
+          link,
+          cache: new Cache({ addTypename: false }),
+        });
+
+        const ErrorContainer = graphql(query, {
+          options: { errorPolicy: "all" },
+        })(
+          class extends React.Component<ChildProps> {
+            componentDidUpdate() {
+              const { data } = this.props;
+              expect(data!.error).toBeTruthy();
+              expect(data!.error!.graphQLErrors[0].message).toEqual(
+                "this is an error"
+              );
+              expect(data).toMatchObject({ allPeople: { people: null } });
+              done = true;
+            }
+            render() {
+              return null;
             }
           }
-        }
-      `;
-      const link = mockSingleLink({
-        request: { query },
-        result: {
-          data: {
-            allPeople: {
-              people: null
+        );
+
+        render(
+          <ApolloProvider client={client}>
+            <ErrorContainer />
+          </ApolloProvider>
+        );
+
+        waitFor(() => {
+          expect(done).toBe(true);
+        }).then(resolve, reject);
+      }
+    );
+
+    itAsync(
+      "passes any GraphQL errors in props along with data [component]",
+      (resolve, reject) => {
+        let done = false;
+        const query: DocumentNode = gql`
+          query people {
+            allPeople(first: 1) {
+              people {
+                name
+              }
             }
+          }
+        `;
+        const link = mockSingleLink({
+          request: { query },
+          result: {
+            data: {
+              allPeople: {
+                people: null,
+              },
+            },
+            errors: [new Error("this is an error")],
           },
-          errors: [new Error('this is an error')]
-        }
-      });
+        });
 
-      const client = new ApolloClient({
-        link,
-        cache: new Cache({ addTypename: false })
-      });
+        const client = new ApolloClient({
+          link,
+          cache: new Cache({ addTypename: false }),
+        });
 
-      class ErrorContainer extends React.Component<QueryResult> {
-        componentDidUpdate() {
-          const { props } = this;
-          expect(props.error).toBeTruthy();
-          expect(props.error!.graphQLErrors[0].message).toEqual(
-            'this is an error'
-          );
-          expect(props.data!.allPeople!).toMatchObject({ people: null });
-          done = true;
-        }
-        render() {
-          return null;
+        class ErrorContainer extends React.Component<QueryResult> {
+          componentDidUpdate() {
+            const { props } = this;
+            expect(props.error).toBeTruthy();
+            expect(props.error!.graphQLErrors[0].message).toEqual(
+              "this is an error"
+            );
+            expect(props.data!.allPeople!).toMatchObject({ people: null });
+            done = true;
+          }
+          render() {
+            return null;
+          }
         }
-      }
 
-      render(
-        <ApolloProvider client={client}>
-          <Query query={query} errorPolicy="all">
-            {(props: any) => <ErrorContainer {...props} />}
-          </Query>
-        </ApolloProvider>
-      );
+        render(
+          <ApolloProvider client={client}>
+            <Query query={query} errorPolicy="all">
+              {(props: any) => <ErrorContainer {...props} />}
+            </Query>
+          </ApolloProvider>
+        );
 
-      waitFor(() => {
-        expect(done).toBe(true);
-      }).then(resolve, reject);
-    });
+        waitFor(() => {
+          expect(done).toBe(true);
+        }).then(resolve, reject);
+      }
+    );
   });
 });
diff --git a/src/react/hoc/__tests__/queries/index.test.tsx b/src/react/hoc/__tests__/queries/index.test.tsx
--- a/src/react/hoc/__tests__/queries/index.test.tsx
+++ b/src/react/hoc/__tests__/queries/index.test.tsx
@@ -1,18 +1,18 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { ApolloLink } from '../../../../link/core';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps, DataProps } from '../../types';
-
-describe('queries', () => {
+import React from "react";
+import PropTypes from "prop-types";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { ApolloLink } from "../../../../link/core";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps, DataProps } from "../../types";
+
+describe("queries", () => {
   let error: typeof console.error;
   beforeEach(() => {
     error = console.error;
@@ -24,7 +24,7 @@ describe('queries', () => {
   });
 
   // general api
-  it('binds a query to props', async() => {
+  it("binds a query to props", async () => {
     let done = false;
     const query: DocumentNode = gql`
       query people {
@@ -37,11 +37,11 @@ describe('queries', () => {
     `;
     const link = mockSingleLink({
       request: { query },
-      result: { data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } } }
+      result: { data: { allPeople: { people: [{ name: "Luke Skywalker" }] } } },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Data {
@@ -50,13 +50,13 @@ describe('queries', () => {
       };
     }
 
-    const ContainerWithData = graphql<any, Data>(query)(
-      ({ data }: DataProps<Data>) => {
-        expect(data).toBeTruthy();
-        done = true;
-        return null;
-      }
-    );
+    const ContainerWithData = graphql<any, Data>(query)(({
+      data,
+    }: DataProps<Data>) => {
+      expect(data).toBeTruthy();
+      done = true;
+      return null;
+    });
 
     render(
       <ApolloProvider client={client}>
@@ -69,7 +69,7 @@ describe('queries', () => {
     });
   });
 
-  itAsync('includes the variables in the props', (resolve, reject) => {
+  itAsync("includes the variables in the props", (resolve, reject) => {
     const TIME_SCALE = 5000;
     let renderCount = 0;
     const query: DocumentNode = gql`
@@ -85,11 +85,11 @@ describe('queries', () => {
     const variables = { first: 1 };
     const link = mockSingleLink({
       request: { query, variables },
-      result: { data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } } }
+      result: { data: { allPeople: { people: [{ name: "Luke Skywalker" }] } } },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     // Ensure variable types work correctly here
@@ -106,14 +106,14 @@ describe('queries', () => {
       first: number;
     }
 
-    const ContainerWithData = graphql<Variables, Data, Variables>(query)(
-      ({ data }: ChildProps<Variables, Data, Variables>) => {
-        expect(data).toBeTruthy();
-        expect(data!.variables).toEqual(variables);
-        renderCount += 1;
-        return null;
-      }
-    );
+    const ContainerWithData = graphql<Variables, Data, Variables>(query)(({
+      data,
+    }: ChildProps<Variables, Data, Variables>) => {
+      expect(data).toBeTruthy();
+      expect(data!.variables).toEqual(variables);
+      renderCount += 1;
+      return null;
+    });
 
     render(
       <ApolloProvider client={client}>
@@ -121,96 +121,104 @@ describe('queries', () => {
       </ApolloProvider>
     );
 
-    waitFor(() => {
-      expect(renderCount).toBe(2);
-    }, {timeout: TIME_SCALE}).then(resolve, reject);
+    waitFor(
+      () => {
+        expect(renderCount).toBe(2);
+      },
+      { timeout: TIME_SCALE }
+    ).then(resolve, reject);
   });
 
-  itAsync('should update query variables when props change', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people($someId: ID) {
-        allPeople(someId: $someId) {
-          people {
-            name
+  itAsync(
+    "should update query variables when props change",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people($someId: ID) {
+          allPeople(someId: $someId) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
+      `;
 
-    const link = mockSingleLink(
-      {
-        request: { query, variables: { someId: 1 } },
-        result: {
-          data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } }
+      const link = mockSingleLink(
+        {
+          request: { query, variables: { someId: 1 } },
+          result: {
+            data: { allPeople: { people: [{ name: "Luke Skywalker" }] } },
+          },
+        },
+        {
+          request: { query, variables: { someId: 2 } },
+          result: {
+            data: { allPeople: { people: [{ name: "Darth Vader" }] } },
+          },
         }
-      },
-      {
-        request: { query, variables: { someId: 2 } },
-        result: { data: { allPeople: { people: [{ name: 'Darth Vader' }] } } }
-      }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    interface Data {
-      allPeople: {
-        people: {
-          name: string;
+      interface Data {
+        allPeople: {
+          people: {
+            name: string;
+          };
         };
-      };
-    }
+      }
 
-    interface Variables {
-      someId: number;
-    }
+      interface Variables {
+        someId: number;
+      }
 
-    const options = {
-      options: {}
-    };
+      const options = {
+        options: {},
+      };
 
-    let count = 0;
-    const ContainerWithData = graphql<Variables, Data, Variables>(
-      query,
-      options
-    )(({ data }: ChildProps<Variables, Data, Variables>) => {
-      expect(data).toBeTruthy();
-      switch (++count) {
-        case 1:
-          expect(data!.loading).toBe(true);
-          expect(data!.variables).toEqual({ someId: 1 });
-          break;
-        case 2:
-          expect(data!.loading).toBe(true);
-          expect(data!.variables).toEqual({ someId: 2 });
-          break;
-        case 3:
-          expect(data!.loading).toBe(false);
-          expect(data!.variables).toEqual({ someId: 2 });
-          break;
-        default:
-          reject(`too many renders (${count})`);
-      }
+      let count = 0;
+      const ContainerWithData = graphql<Variables, Data, Variables>(
+        query,
+        options
+      )(({ data }: ChildProps<Variables, Data, Variables>) => {
+        expect(data).toBeTruthy();
+        switch (++count) {
+          case 1:
+            expect(data!.loading).toBe(true);
+            expect(data!.variables).toEqual({ someId: 1 });
+            break;
+          case 2:
+            expect(data!.loading).toBe(true);
+            expect(data!.variables).toEqual({ someId: 2 });
+            break;
+          case 3:
+            expect(data!.loading).toBe(false);
+            expect(data!.variables).toEqual({ someId: 2 });
+            break;
+          default:
+            reject(`too many renders (${count})`);
+        }
 
-      return null;
-    });
+        return null;
+      });
 
-    const { rerender } = render(
-      <ApolloProvider client={client}>
-        <ContainerWithData someId={1} />
-      </ApolloProvider>
-    );
-    rerender(
-      <ApolloProvider client={client}>
-        <ContainerWithData someId={2} />
-      </ApolloProvider>
-    );
+      const { rerender } = render(
+        <ApolloProvider client={client}>
+          <ContainerWithData someId={1} />
+        </ApolloProvider>
+      );
+      rerender(
+        <ApolloProvider client={client}>
+          <ContainerWithData someId={2} />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(count).toBe(3);
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(count).toBe(3);
+      }).then(resolve, reject);
+    }
+  );
 
   it("shouldn't warn about fragments", () => {
     const oldWarn = console.warn;
@@ -218,20 +226,18 @@ describe('queries', () => {
     console.warn = (str: any) => warnings.push(str);
 
     try {
-      graphql(
-        gql`
-          query foo {
-            bar
-          }
-        `
-      );
+      graphql(gql`
+        query foo {
+          bar
+        }
+      `);
       expect(warnings.length).toEqual(0);
     } finally {
       console.warn = oldWarn;
     }
   });
 
-  itAsync('executes a query', (resolve, reject) => {
+  itAsync("executes a query", (resolve, reject) => {
     let done = false;
     const query: DocumentNode = gql`
       query people {
@@ -242,16 +248,16 @@ describe('queries', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<{}, Data>(query)(
@@ -279,7 +285,7 @@ describe('queries', () => {
     }).then(resolve, reject);
   });
 
-  itAsync('executes a query with two root fields', (resolve, reject) => {
+  itAsync("executes a query with two root fields", (resolve, reject) => {
     let done = false;
     const query: DocumentNode = gql`
       query people {
@@ -296,18 +302,18 @@ describe('queries', () => {
       }
     `;
     const data = {
-      allPeople: { people: [{ name: 'Luke Skywalker' }] },
-      otherPeople: { people: [{ name: 'Luke Skywalker' }] }
+      allPeople: { people: [{ name: "Luke Skywalker" }] },
+      otherPeople: { people: [{ name: "Luke Skywalker" }] },
     };
     type Data = typeof data;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<{}, Data>(query)(
@@ -316,9 +322,7 @@ describe('queries', () => {
           const { props } = this;
           expect(props.data!.loading).toBeFalsy();
           expect(props.data!.allPeople).toEqual(data.allPeople);
-          expect(props.data!.otherPeople).toEqual(
-            data.otherPeople
-          );
+          expect(props.data!.otherPeople).toEqual(data.otherPeople);
           done = true;
         }
         render() {
@@ -338,7 +342,7 @@ describe('queries', () => {
     }).then(resolve, reject);
   });
 
-  itAsync('maps props as variables if they match', (resolve, reject) => {
+  itAsync("maps props as variables if they match", (resolve, reject) => {
     let done = false;
     const query: DocumentNode = gql`
       query people($first: Int) {
@@ -349,7 +353,7 @@ describe('queries', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const variables = { first: 1 };
@@ -357,11 +361,11 @@ describe('queries', () => {
 
     const link = mockSingleLink({
       request: { query, variables },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<Vars, Data, Vars>(query)(
@@ -370,9 +374,7 @@ describe('queries', () => {
           const { props } = this;
           expect(props.data!.loading).toBeFalsy();
           expect(props.data!.allPeople).toEqual(data.allPeople);
-          expect(props.data!.variables).toEqual(
-            this.props.data!.variables
-          );
+          expect(props.data!.variables).toEqual(this.props.data!.variables);
           done = true;
         }
         render() {
@@ -392,129 +394,135 @@ describe('queries', () => {
     }).then(resolve, reject);
   });
 
-  itAsync("doesn't care about the order of variables in a request", (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people($first: Int, $jedi: Boolean) {
-        allPeople(first: $first, jedi: $jedi) {
-          people {
-            name
+  itAsync(
+    "doesn't care about the order of variables in a request",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people($first: Int, $jedi: Boolean) {
+          allPeople(first: $first, jedi: $jedi) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    type Data = typeof data;
-    const variables = { jedi: true, first: 1 };
-    type Vars = typeof variables;
-
-    const mocks = [
-      {
-        request: {
-          query,
-          variables
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      type Data = typeof data;
+      const variables = { jedi: true, first: 1 };
+      type Vars = typeof variables;
+
+      const mocks = [
+        {
+          request: {
+            query,
+            variables,
+          },
+          result: {
+            data,
+          },
         },
-        result: {
-          data
-        }
-      }
-    ];
-    const link = mockSingleLink.apply(null, mocks);
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-    const options = {
-      options: {
-        variables: {
-          jedi: true,
-          first: 1
-        }
-      }
-    };
+      ];
+      const link = mockSingleLink.apply(null, mocks);
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+      const options = {
+        options: {
+          variables: {
+            jedi: true,
+            first: 1,
+          },
+        },
+      };
 
-    const Container = graphql<{}, Data, Vars>(
-      query,
-      options
-    )(
-      class extends React.Component<ChildProps<{}, Data, Vars>> {
-        componentDidUpdate() {
-          const { props } = this;
-          try {
-            expect(props.data!.loading).toBeFalsy();
-            expect(props.data!.allPeople).toEqual(data.allPeople);
-            done = true;
-          } catch (error) {
-            reject(error);
+      const Container = graphql<{}, Data, Vars>(
+        query,
+        options
+      )(
+        class extends React.Component<ChildProps<{}, Data, Vars>> {
+          componentDidUpdate() {
+            const { props } = this;
+            try {
+              expect(props.data!.loading).toBeFalsy();
+              expect(props.data!.allPeople).toEqual(data.allPeople);
+              done = true;
+            } catch (error) {
+              reject(error);
+            }
+          }
+          render() {
+            return null;
           }
         }
-        render() {
-          return null;
-        }
-      }
-    );
-
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true);
-    }).then(resolve, reject);
-  });
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-  itAsync('allows falsy values in the mapped variables from props', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people($first: Int) {
-        allPeople(first: $first) {
-          people {
-            name
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
+
+  itAsync(
+    "allows falsy values in the mapped variables from props",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people($first: Int) {
+          allPeople(first: $first) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    type Data = typeof data;
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      type Data = typeof data;
 
-    const variables = { first: null };
-    type Vars = typeof variables;
+      const variables = { first: null };
+      type Vars = typeof variables;
 
-    const link = mockSingleLink({
-      request: { query, variables },
-      result: { data }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      const link = mockSingleLink({
+        request: { query, variables },
+        result: { data },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    const Container = graphql<Partial<Vars>, Data, Vars>(query)(
-      class extends React.Component<ChildProps<Partial<Vars>, Data, Vars>> {
-        componentDidUpdate() {
-          const { props } = this;
-          expect(props.data!.loading).toBeFalsy();
-          expect(props.data!.allPeople).toEqual(data.allPeople);
-          done = true;
-        }
-        render() {
-          return null;
+      const Container = graphql<Partial<Vars>, Data, Vars>(query)(
+        class extends React.Component<ChildProps<Partial<Vars>, Data, Vars>> {
+          componentDidUpdate() {
+            const { props } = this;
+            expect(props.data!.loading).toBeFalsy();
+            expect(props.data!.allPeople).toEqual(data.allPeople);
+            done = true;
+          }
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container first={null} />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container first={null} />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true);
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
 
   it("doesn't error on optional required props", () => {
     const query: DocumentNode = gql`
@@ -526,7 +534,7 @@ describe('queries', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const variables = { first: 1 };
@@ -534,11 +542,11 @@ describe('queries', () => {
 
     const link = mockSingleLink({
       request: { query, variables },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     const Container = graphql<Vars, Data>(query)(() => null);
 
@@ -549,7 +557,7 @@ describe('queries', () => {
           <Container first={1} />
         </ApolloProvider>
       );
-      unmount()
+      unmount();
     } catch (e) {
       errorCaught = e;
     }
@@ -558,7 +566,7 @@ describe('queries', () => {
   });
 
   // context
-  itAsync('allows context through updates', (resolve, reject) => {
+  itAsync("allows context through updates", (resolve, reject) => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -568,16 +576,16 @@ describe('queries', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<{}, Data>(query)(
@@ -594,10 +602,13 @@ describe('queries', () => {
       }
     );
 
-    class ContextContainer extends React.Component<React.PropsWithChildren, { color: string }> {
+    class ContextContainer extends React.Component<
+      React.PropsWithChildren,
+      { color: string }
+    > {
       constructor(props: {}) {
         super(props);
-        this.state = { color: 'purple' };
+        this.state = { color: "purple" };
       }
 
       getChildContext() {
@@ -606,7 +617,7 @@ describe('queries', () => {
 
       componentDidMount() {
         setTimeout(() => {
-          this.setState({ color: 'green' });
+          this.setState({ color: "green" });
         }, 50);
       }
 
@@ -617,7 +628,7 @@ describe('queries', () => {
     }
 
     (ContextContainer as any).childContextTypes = {
-      color: PropTypes.string
+      color: PropTypes.string,
     };
 
     let count = 0;
@@ -625,9 +636,9 @@ describe('queries', () => {
     class ChildContextContainer extends React.Component<React.PropsWithChildren> {
       render() {
         const { color } = this.context as any;
-        if (count === 0) expect(color).toBe('purple');
+        if (count === 0) expect(color).toBe("purple");
         if (count === 1) {
-          expect(color).toBe('green');
+          expect(color).toBe("green");
           done = true;
         }
 
@@ -638,7 +649,7 @@ describe('queries', () => {
     }
 
     (ChildContextContainer as any).contextTypes = {
-      color: PropTypes.string
+      color: PropTypes.string,
     };
 
     render(
@@ -655,7 +666,7 @@ describe('queries', () => {
   });
 
   // meta
-  it('stores the component name', () => {
+  it("stores the component name", () => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -665,7 +676,7 @@ describe('queries', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
     const Container = graphql<{}, Data>(query)(
       class Container extends React.Component<ChildProps<{}, Data>> {
@@ -675,7 +686,7 @@ describe('queries', () => {
       }
     );
 
-    expect(Container.displayName).toEqual('Apollo(Container)');
+    expect(Container.displayName).toEqual("Apollo(Container)");
   });
 
   it("uses a custom wrapped component name when 'alias' is specified", () => {
@@ -689,7 +700,7 @@ describe('queries', () => {
       }
     `;
     @graphql(query, {
-      alias: 'withFoo'
+      alias: "withFoo",
     })
     class Container extends React.Component {
       render(): React.ReactNode {
@@ -699,10 +710,10 @@ describe('queries', () => {
     // );
 
     // Not sure why I have to cast Container to any
-    expect((Container as any).displayName).toEqual('withFoo(Container)');
+    expect((Container as any).displayName).toEqual("withFoo(Container)");
   });
 
-  itAsync('passes context to the link', (resolve, reject) => {
+  itAsync("passes context to the link", (resolve, reject) => {
     let done = false;
     const query: DocumentNode = gql`
       query people {
@@ -721,13 +732,13 @@ describe('queries', () => {
       mockSingleLink({
         request: { query },
         result: {
-          data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } }
-        }
+          data: { allPeople: { people: [{ name: "Luke Skywalker" }] } },
+        },
       })
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Data {
@@ -737,7 +748,7 @@ describe('queries', () => {
     }
 
     const ContainerWithData = graphql<any, Data>(query, {
-      options: props => ({ context: { fromProps: props.context } })
+      options: (props) => ({ context: { fromProps: props.context } }),
     })(() => null);
 
     render(
@@ -751,21 +762,21 @@ describe('queries', () => {
     }).then(resolve, reject);
   });
 
-  describe('Return partial data', () => {
+  describe("Return partial data", () => {
     const consoleWarn = console.warn;
     beforeAll(() => {
-      console.warn = () => null
+      console.warn = () => null;
     });
 
     afterAll(() => {
       console.warn = consoleWarn;
     });
 
-    it('should not return partial cache data when `returnPartialData` is false', () => {
+    it("should not return partial cache data when `returnPartialData` is false", () => {
       const cache = new Cache();
       const client = new ApolloClient({
         cache,
-        link: ApolloLink.empty()
+        link: ApolloLink.empty(),
       });
 
       const fullQuery = gql`
@@ -786,20 +797,20 @@ describe('queries', () => {
         data: {
           cars: [
             {
-              __typename: 'Car',
-              make: 'Ford',
-              model: 'Mustang',
-              vin: 'PONY123',
+              __typename: "Car",
+              make: "Ford",
+              model: "Mustang",
+              vin: "PONY123",
               repairs: [
                 {
-                  __typename: 'Repair',
-                  date: '2019-05-08',
-                  description: 'Could not get after it.'
-                }
-              ]
-            }
-          ]
-        }
+                  __typename: "Repair",
+                  date: "2019-05-08",
+                  description: "Could not get after it.",
+                },
+              ],
+            },
+          ],
+        },
       });
 
       const partialQuery = gql`
@@ -831,11 +842,11 @@ describe('queries', () => {
       render(<App />);
     });
 
-    it('should return partial cache data when `returnPartialData` is true', () => {
+    it("should return partial cache data when `returnPartialData` is true", () => {
       const cache = new Cache();
       const client = new ApolloClient({
         cache,
-        link: ApolloLink.empty()
+        link: ApolloLink.empty(),
       });
 
       const fullQuery = gql`
@@ -856,20 +867,20 @@ describe('queries', () => {
         data: {
           cars: [
             {
-              __typename: 'Car',
-              make: 'Ford',
-              model: 'Mustang',
-              vin: 'PONY123',
+              __typename: "Car",
+              make: "Ford",
+              model: "Mustang",
+              vin: "PONY123",
               repairs: [
                 {
-                  __typename: 'Repair',
-                  date: '2019-05-08',
-                  description: 'Could not get after it.'
-                }
-              ]
-            }
-          ]
-        }
+                  __typename: "Repair",
+                  date: "2019-05-08",
+                  description: "Could not get after it.",
+                },
+              ],
+            },
+          ],
+        },
       });
 
       const partialQuery = gql`
@@ -885,22 +896,22 @@ describe('queries', () => {
 
       const ComponentWithData = graphql<any, any>(partialQuery, {
         options: {
-          returnPartialData: true
-        }
+          returnPartialData: true,
+        },
       })(
         class Compnent extends React.Component<any> {
           render() {
             if (!this.props.data.loading) {
               expect(this.props.data.cars).toEqual([
                 {
-                  __typename: 'Car',
+                  __typename: "Car",
                   repairs: [
                     {
-                      __typename: 'Repair',
-                      date: '2019-05-08'
-                    }
-                  ]
-                }
+                      __typename: "Repair",
+                      date: "2019-05-08",
+                    },
+                  ],
+                },
               ]);
             }
             return null;
diff --git a/src/react/hoc/__tests__/queries/lifecycle.test.tsx b/src/react/hoc/__tests__/queries/lifecycle.test.tsx
--- a/src/react/hoc/__tests__/queries/lifecycle.test.tsx
+++ b/src/react/hoc/__tests__/queries/lifecycle.test.tsx
@@ -1,21 +1,19 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { mockSingleLink } from '../../../../testing';
-import { Query as QueryComponent } from '../../../components';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-const IS_REACT_18 = React.version.startsWith('18');
-
-describe('[queries] lifecycle', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { mockSingleLink } from "../../../../testing";
+import { Query as QueryComponent } from "../../../components";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("[queries] lifecycle", () => {
   // lifecycle
-  it('reruns the query if it changes', async () => {
+  it("reruns the query if it changes", async () => {
     let count = 0;
     const query: DocumentNode = gql`
       query people($first: Int) {
@@ -27,12 +25,12 @@ describe('[queries] lifecycle', () => {
       }
     `;
 
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data1;
     const variables1 = { first: 1 };
     type Vars = typeof variables1;
 
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+    const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
     const variables2 = { first: 2 };
 
     const link = mockSingleLink(
@@ -42,14 +40,14 @@ describe('[queries] lifecycle', () => {
 
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<Vars, Data, Vars>(query, {
-      options: props => ({
+      options: (props) => ({
         variables: props,
-        fetchPolicy: count === 0 ? 'cache-and-network' : 'cache-first'
-      })
+        fetchPolicy: count === 0 ? "cache-and-network" : "cache-first",
+      }),
     })(
       class extends React.Component<ChildProps<Vars, Data, Vars>> {
         componentDidUpdate(prevProps: ChildProps<Vars, Data, Vars>) {
@@ -110,7 +108,7 @@ describe('[queries] lifecycle', () => {
     await waitFor(() => expect(count).toBe(3));
   });
 
-  it('rebuilds the queries on prop change when using `options`', async () => {
+  it("rebuilds the queries on prop change when using `options`", async () => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -120,16 +118,16 @@ describe('[queries] lifecycle', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let firstRun = true;
@@ -176,7 +174,7 @@ describe('[queries] lifecycle', () => {
     });
   });
 
-  it('reruns the query if just the variables change', async () => {
+  it("reruns the query if just the variables change", async () => {
     let count = 0;
     const query: DocumentNode = gql`
       query people($first: Int) {
@@ -188,13 +186,13 @@ describe('[queries] lifecycle', () => {
       }
     `;
 
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data1;
 
     const variables1 = { first: 1 };
     type Vars = typeof variables1;
 
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+    const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
     const variables2 = { first: 2 };
 
     const link = mockSingleLink(
@@ -204,11 +202,11 @@ describe('[queries] lifecycle', () => {
 
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<Vars, Data, Vars>(query, {
-      options: props => ({ variables: props })
+      options: (props) => ({ variables: props }),
     })(
       class extends React.Component<ChildProps<Vars, Data, Vars>> {
         componentDidUpdate(prevProps: ChildProps<Vars, Data, Vars>) {
@@ -270,7 +268,7 @@ describe('[queries] lifecycle', () => {
     await waitFor(() => expect(count).toBe(3));
   });
 
-  it('reruns the queries on prop change when using passed props', async () => {
+  it("reruns the queries on prop change when using passed props", async () => {
     let count = 0;
     const query: DocumentNode = gql`
       query people($first: Int) {
@@ -282,13 +280,13 @@ describe('[queries] lifecycle', () => {
       }
     `;
 
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data1;
 
     const variables1 = { first: 1 };
     type Vars = typeof variables1;
 
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+    const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
     const variables2 = { first: 2 };
 
     const link = mockSingleLink(
@@ -298,7 +296,7 @@ describe('[queries] lifecycle', () => {
 
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<Vars, Data, Vars>(query)(
@@ -359,7 +357,7 @@ describe('[queries] lifecycle', () => {
     await waitFor(() => expect(count).toBe(3));
   });
 
-  it('stays subscribed to updates after irrelevant prop changes', async () => {
+  it("stays subscribed to updates after irrelevant prop changes", async () => {
     const query: DocumentNode = gql`
       query people($first: Int) {
         allPeople(first: $first) {
@@ -371,17 +369,17 @@ describe('[queries] lifecycle', () => {
     `;
     const variables = { first: 1 };
     type Vars = typeof variables;
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data1;
 
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+    const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
     const link = mockSingleLink(
       { request: { query, variables }, result: { data: data1 } },
       { request: { query, variables }, result: { data: data2 } }
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Props {
@@ -391,7 +389,7 @@ describe('[queries] lifecycle', () => {
 
     let count = 0;
     const Container = graphql<Props, Data, Vars>(query, {
-      options: { variables, notifyOnNetworkStatusChange: false }
+      options: { variables, notifyOnNetworkStatusChange: false },
     })(
       class extends React.Component<ChildProps<Props, Data, Vars>> {
         componentDidUpdate() {
@@ -401,23 +399,17 @@ describe('[queries] lifecycle', () => {
             if (count === 1) {
               expect(props.foo).toEqual(42);
               expect(props.data!.loading).toEqual(false);
-              expect(props.data!.allPeople).toEqual(
-                data1.allPeople
-              );
+              expect(props.data!.allPeople).toEqual(data1.allPeople);
               props.changeState();
             } else if (count === 2) {
               expect(props.foo).toEqual(43);
               expect(props.data!.loading).toEqual(false);
-              expect(props.data!.allPeople).toEqual(
-                data1.allPeople
-              );
+              expect(props.data!.allPeople).toEqual(data1.allPeople);
               props.data!.refetch();
             } else if (count === 3) {
               expect(props.foo).toEqual(43);
               expect(props.data!.loading).toEqual(false);
-              expect(props.data!.allPeople).toEqual(
-                data2.allPeople
-              );
+              expect(props.data!.allPeople).toEqual(data2.allPeople);
             }
           } catch (e) {
             fail(e);
@@ -450,7 +442,7 @@ describe('[queries] lifecycle', () => {
     await waitFor(() => expect(count).toBe(3));
   });
 
-  it('correctly rebuilds props on remount', async () => {
+  it("correctly rebuilds props on remount", async () => {
     const query: DocumentNode = gql`
       query pollingPeople {
         allPeople(first: 1) {
@@ -460,7 +452,7 @@ describe('[queries] lifecycle', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Darth Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Darth Skywalker" }] } };
     type Data = typeof data;
     const link = mockSingleLink({
       request: { query },
@@ -468,14 +460,14 @@ describe('[queries] lifecycle', () => {
       newData: () => ({
         data: {
           allPeople: {
-            people: [{ name: `Darth Skywalker - ${Math.random()}` }]
-          }
-        }
-      })
+            people: [{ name: `Darth Skywalker - ${Math.random()}` }],
+          },
+        },
+      }),
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     let app: React.ReactElement<any>,
       count = 0;
@@ -483,7 +475,7 @@ describe('[queries] lifecycle', () => {
     let done = false;
     let rerender: any;
     const Container = graphql<{}, Data>(query, {
-      options: { pollInterval: 10, notifyOnNetworkStatusChange: false }
+      options: { pollInterval: 10, notifyOnNetworkStatusChange: false },
     })(
       class extends React.Component<ChildProps<{}, Data>> {
         componentDidUpdate() {
@@ -512,13 +504,11 @@ describe('[queries] lifecycle', () => {
     rerender = render(app).rerender;
 
     await waitFor(() => {
-      if (!IS_REACT_18) {
-        expect(done).toBeTruthy()
-      }
+      expect(done).toBeTruthy();
     });
   });
 
-  it('will re-execute a query when the client changes', async () => {
+  it("will re-execute a query when the client changes", async () => {
     const query: DocumentNode = gql`
       {
         a
@@ -529,38 +519,38 @@ describe('[queries] lifecycle', () => {
     const link1 = mockSingleLink(
       {
         request: { query },
-        result: { data: { a: 1, b: 2, c: 3 } }
+        result: { data: { a: 1, b: 2, c: 3 } },
       },
       {
         request: { query },
-        result: { data: { a: 1, b: 2, c: 3 } }
+        result: { data: { a: 1, b: 2, c: 3 } },
       }
     );
     const link2 = mockSingleLink(
       {
         request: { query },
-        result: { data: { a: 4, b: 5, c: 6 } }
+        result: { data: { a: 4, b: 5, c: 6 } },
       },
       {
         request: { query },
-        result: { data: { a: 4, b: 5, c: 6 } }
+        result: { data: { a: 4, b: 5, c: 6 } },
       }
     );
     const link3 = mockSingleLink({
       request: { query },
-      result: { data: { a: 7, b: 8, c: 9 } }
+      result: { data: { a: 7, b: 8, c: 9 } },
     });
     const client1 = new ApolloClient({
       link: link1,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     const client2 = new ApolloClient({
       link: link2,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     const client3 = new ApolloClient({
       link: link3,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Data {
@@ -571,9 +561,10 @@ describe('[queries] lifecycle', () => {
     let switchClient: (client: ApolloClient<any>) => void;
     let refetchQuery: () => void;
     let count = 0;
+    let testFailures: any[] = [];
 
     const Query = graphql<{}, Data>(query, {
-      options: { notifyOnNetworkStatusChange: true }
+      options: { notifyOnNetworkStatusChange: true },
     })(
       class extends React.Component<ChildProps<{}, Data>> {
         componentDidMount() {
@@ -597,20 +588,16 @@ describe('[queries] lifecycle', () => {
                   loading: false,
                   a: 1,
                   b: 2,
-                  c: 3
+                  c: 3,
                 });
                 refetchQuery!();
                 break;
               case 3:
-                if (IS_REACT_18) {
-                  expect({ loading }).toEqual({ loading: false });
-                } else {
-                  expect({ loading }).toEqual({ loading: true });
-                }
+                expect({ loading }).toEqual({ loading: true });
                 expect({ a, b, c }).toEqual({
                   a: 1,
                   b: 2,
-                  c: 3
+                  c: 3,
                 });
                 break;
               case 4:
@@ -618,7 +605,7 @@ describe('[queries] lifecycle', () => {
                   loading: false,
                   a: 1,
                   b: 2,
-                  c: 3
+                  c: 3,
                 });
                 setTimeout(() => {
                   switchClient!(client2);
@@ -637,7 +624,7 @@ describe('[queries] lifecycle', () => {
                   loading: false,
                   a: 4,
                   b: 5,
-                  c: 6
+                  c: 6,
                 });
                 refetchQuery!();
                 break;
@@ -646,7 +633,7 @@ describe('[queries] lifecycle', () => {
                   loading: true,
                   a: 4,
                   b: 5,
-                  c: 6
+                  c: 6,
                 });
                 break;
               case 8:
@@ -654,7 +641,7 @@ describe('[queries] lifecycle', () => {
                   loading: false,
                   a: 4,
                   b: 5,
-                  c: 6
+                  c: 6,
                 });
                 setTimeout(() => {
                   switchClient!(client3);
@@ -673,7 +660,7 @@ describe('[queries] lifecycle', () => {
                   loading: false,
                   a: 7,
                   b: 8,
-                  c: 9
+                  c: 9,
                 });
                 setTimeout(() => {
                   switchClient!(client1);
@@ -684,7 +671,7 @@ describe('[queries] lifecycle', () => {
                   loading: false,
                   a: 1,
                   b: 2,
-                  c: 3
+                  c: 3,
                 });
                 setTimeout(() => {
                   switchClient!(client3);
@@ -702,7 +689,7 @@ describe('[queries] lifecycle', () => {
                 fail(`Unexpectedly many renders (${count})`);
             }
           } catch (err) {
-            fail(err);
+            testFailures.push(err);
           }
 
           return null;
@@ -712,11 +699,11 @@ describe('[queries] lifecycle', () => {
 
     class ClientSwitcher extends React.Component<any, any> {
       state = {
-        client: client1
+        client: client1,
       };
 
       componentDidMount() {
-        switchClient = newClient => {
+        switchClient = (newClient) => {
           this.setState({ client: newClient });
         };
       }
@@ -733,15 +720,14 @@ describe('[queries] lifecycle', () => {
     render(<ClientSwitcher />);
 
     await waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toBe(3)
-      } else {
-        expect(count).toBe(12)
+      if (testFailures.length > 0) {
+        throw testFailures[0];
       }
+      expect(count).toBe(12);
     });
   });
 
-  it('handles synchronous racecondition with prefilled data from the server', async () => {
+  it("handles synchronous racecondition with prefilled data from the server", async () => {
     const query: DocumentNode = gql`
       query GetUser($first: Int) {
         user(first: $first) {
@@ -751,29 +737,29 @@ describe('[queries] lifecycle', () => {
     `;
     const variables = { first: 1 };
     type Vars = typeof variables;
-    const data2 = { user: { name: 'Luke Skywalker' } };
+    const data2 = { user: { name: "Luke Skywalker" } };
     type Data = typeof data2;
 
     const link = mockSingleLink({
       request: { query, variables },
       result: { data: data2 },
-      delay: 10
+      delay: 10,
     });
     const initialState = {
       apollo: {
         data: {
           ROOT_QUERY: {
-            'user({"first":1})': null
-          }
-        }
-      }
+            'user({"first":1})': null,
+          },
+        },
+      },
     };
 
     const client = new ApolloClient({
       link,
       // prefill the store (like SSR would)
       // @see https://github.com/zeit/next.js/blob/master/examples/with-apollo/lib/initApollo.js
-      cache: new Cache({ addTypename: false }).restore(initialState)
+      cache: new Cache({ addTypename: false }).restore(initialState),
     });
 
     let count = 0;
@@ -781,8 +767,8 @@ describe('[queries] lifecycle', () => {
     const Container = graphql<Vars, Data>(query)(
       class extends React.Component<ChildProps<Vars, Data>> {
         componentDidMount() {
-          this.props.data!.refetch().then(result => {
-            expect(result.data!.user.name).toBe('Luke Skywalker');
+          this.props.data!.refetch().then((result) => {
+            expect(result.data!.user.name).toBe("Luke Skywalker");
             done = true;
           });
         }
@@ -790,9 +776,9 @@ describe('[queries] lifecycle', () => {
         render() {
           count++;
           const user = this.props.data!.user;
-          const name = user ? user.name : '';
+          const name = user ? user.name : "";
           if (count === 2) {
-            expect(name).toBe('Luke Skywalker');
+            expect(name).toBe("Luke Skywalker");
           }
           return null;
         }
@@ -808,7 +794,7 @@ describe('[queries] lifecycle', () => {
     await waitFor(() => expect(done).toBeTruthy());
   });
 
-  it('handles asynchronous racecondition with prefilled data from the server', async () => {
+  it("handles asynchronous racecondition with prefilled data from the server", async () => {
     const query: DocumentNode = gql`
       query Q {
         books {
@@ -821,42 +807,42 @@ describe('[queries] lifecycle', () => {
     const ssrResult = {
       books: [
         {
-          name: 'ssrfirst',
-          __typename: 'Book'
-        }
-      ]
+          name: "ssrfirst",
+          __typename: "Book",
+        },
+      ],
     };
 
     const result = {
       books: [
         {
-          name: 'first',
-          __typename: 'Book'
-        }
-      ]
+          name: "first",
+          __typename: "Book",
+        },
+      ],
     };
 
     const ssrLink = mockSingleLink({
       request: { query } as any,
-      result: { data: ssrResult }
+      result: { data: ssrResult },
     });
 
     const link = mockSingleLink({
       request: { query } as any,
-      result: { data: result }
+      result: { data: result },
     });
 
     const ssrClient = new ApolloClient({
       cache: new Cache(),
-      link: ssrLink
+      link: ssrLink,
     });
     await ssrClient.query({
       query,
-      variables: {}
+      variables: {},
     });
     const client = new ApolloClient({
       cache: new Cache().restore(ssrClient.extract()), // --- this is the "SSR" bit
-      link
+      link,
     });
 
     //try to render the app / call refetch / etc
@@ -869,7 +855,7 @@ describe('[queries] lifecycle', () => {
           {({ loading, data, refetch }: any) => {
             if (!loading) {
               if (!refetched) {
-                expect(data.books[0].name).toEqual('ssrfirst');
+                expect(data.books[0].name).toEqual("ssrfirst");
                 //setTimeout allows component to mount, which often happens
                 //when waiting  ideally we should be able to call refetch
                 //immediately However the subscription needs to start before
@@ -879,13 +865,13 @@ describe('[queries] lifecycle', () => {
                 //data you get is fresh, so one would wait for an interaction
                 setTimeout(() => {
                   refetch().then((refetchResult: any) => {
-                    expect(refetchResult.data.books[0].name).toEqual('first');
+                    expect(refetchResult.data.books[0].name).toEqual("first");
                     done = true;
                   });
                 });
                 refetched = true;
               } else {
-                expect(data.books[0].name).toEqual('first');
+                expect(data.books[0].name).toEqual("first");
               }
             }
             return <p> stub </p>;
diff --git a/src/react/hoc/__tests__/queries/loading.test.tsx b/src/react/hoc/__tests__/queries/loading.test.tsx
--- a/src/react/hoc/__tests__/queries/loading.test.tsx
+++ b/src/react/hoc/__tests__/queries/loading.test.tsx
@@ -1,64 +1,71 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient, NetworkStatus, WatchQueryFetchPolicy } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-const IS_REACT_18 = React.version.startsWith('18');
-
-describe('[queries] loading', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import {
+  ApolloClient,
+  NetworkStatus,
+  WatchQueryFetchPolicy,
+} from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("[queries] loading", () => {
   // networkStatus / loading
-  itAsync('exposes networkStatus as a part of the props api', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "exposes networkStatus as a part of the props api",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const link = mockSingleLink({
-      request: { query },
-      result: { data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } } }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: {
+          data: { allPeople: { people: [{ name: "Luke Skywalker" }] } },
+        },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let done = false;
-    const Container = graphql(query, {
-      options: { notifyOnNetworkStatusChange: true }
-    })(
-      class extends React.Component<ChildProps> {
-        componentDidUpdate() {
-          const { data } = this.props;
-          expect(data!.networkStatus).toBeTruthy();
-          done = true;
-        }
-        render() {
-          return null;
+      let done = false;
+      const Container = graphql(query, {
+        options: { notifyOnNetworkStatusChange: true },
+      })(
+        class extends React.Component<ChildProps> {
+          componentDidUpdate() {
+            const { data } = this.props;
+            expect(data!.networkStatus).toBeTruthy();
+            done = true;
+          }
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
+    }
+  );
 
-  it('should set the initial networkStatus to 1 (loading)', () => {
+  it("should set the initial networkStatus to 1 (loading)", () => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -68,14 +75,14 @@ describe('[queries] loading', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     @graphql(query, { options: { notifyOnNetworkStatusChange: true } })
@@ -97,250 +104,260 @@ describe('[queries] loading', () => {
       </ApolloProvider>
     );
 
-    unmount()
+    unmount();
   });
 
-  itAsync('should set the networkStatus to 7 (ready) when the query is loaded', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "should set the networkStatus to 7 (ready) when the query is loaded",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const link = mockSingleLink({
-      request: { query },
-      result: { data }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const link = mockSingleLink({
+        request: { query },
+        result: { data },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    const Container = graphql(query, {
-      options: { notifyOnNetworkStatusChange: true }
-    })(
-      class extends React.Component<ChildProps> {
-        componentDidUpdate() {
-          expect(this.props.data!.networkStatus).toBe(7);
-          done = true;
-        }
+      const Container = graphql(query, {
+        options: { notifyOnNetworkStatusChange: true },
+      })(
+        class extends React.Component<ChildProps> {
+          componentDidUpdate() {
+            expect(this.props.data!.networkStatus).toBe(7);
+            done = true;
+          }
 
-        render() {
-          return null;
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true);
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('should set the networkStatus to 2 (setVariables) when the query variables are changed', (resolve, reject) => {
-    let count = 0;
-    const query: DocumentNode = gql`
-      query people($first: Int) {
-        allPeople(first: $first) {
-          people {
-            name
+  itAsync(
+    "should set the networkStatus to 2 (setVariables) when the query variables are changed",
+    (resolve, reject) => {
+      let count = 0;
+      const query: DocumentNode = gql`
+        query people($first: Int) {
+          allPeople(first: $first) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
+      `;
 
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const variables1 = { first: 1 };
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const variables1 = { first: 1 };
 
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
-    const variables2 = { first: 2 };
+      const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
+      const variables2 = { first: 2 };
 
-    type Data = typeof data1;
-    type Vars = typeof variables1;
+      type Data = typeof data1;
+      type Vars = typeof variables1;
 
-    const link = mockSingleLink(
-      { request: { query, variables: variables1 }, result: { data: data1 } },
-      { request: { query, variables: variables2 }, result: { data: data2 } }
-    );
+      const link = mockSingleLink(
+        { request: { query, variables: variables1 }, result: { data: data1 } },
+        { request: { query, variables: variables2 }, result: { data: data2 } }
+      );
 
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let done = false;
-    const Container = graphql<Vars, Data, Vars>(query, {
-      options: props => ({
-        variables: props,
-        notifyOnNetworkStatusChange: true
-      })
-    })(
-      class extends React.Component<ChildProps<Vars, Data, Vars>> {
-        componentDidUpdate(prevProps: ChildProps<Vars, Data, Vars>) {
-          try {
-            // variables changed, new query is loading, but old data is still there
-            switch (++count) {
-              case 1:
-                expect(prevProps.data!.loading).toBe(true);
-                expect(prevProps.data!.variables).toEqual(variables1);
-                expect(prevProps.data!.allPeople).toBe(undefined);
-                expect(prevProps.data!.error).toBe(undefined);
-                expect(prevProps.data!.networkStatus).toBe(NetworkStatus.loading);
-                expect(this.props.data!.loading).toBe(false);
-                expect(this.props.data!.variables).toEqual(variables1);
-                expect(this.props.data!.allPeople).toEqual(data1.allPeople);
-                expect(this.props.data!.error).toBe(undefined);
-                expect(this.props.data!.networkStatus).toBe(NetworkStatus.ready);
-                break;
-              case 2:
-                expect(prevProps.data!.loading).toBe(false);
-                expect(prevProps.data!.variables).toEqual(variables1);
-                expect(prevProps.data!.allPeople).toEqual(data1.allPeople);
-                expect(prevProps.data!.error).toBe(undefined);
-                expect(this.props.data!.loading).toBe(true);
-                expect(this.props.data!.variables).toEqual(variables2);
-                expect(this.props.data!.allPeople).toBe(undefined);
-                expect(this.props.data!.error).toBe(undefined);
-                expect(this.props.data!.networkStatus).toBe(NetworkStatus.setVariables);
-                break;
-              case 3:
-                expect(prevProps.data!.loading).toBe(true);
-                expect(prevProps.data!.variables).toEqual(variables2);
-                expect(prevProps.data!.allPeople).toBe(undefined);
-                expect(prevProps.data!.error).toBe(undefined);
-                expect(prevProps.data!.networkStatus).toBe(NetworkStatus.setVariables);
-                expect(this.props.data!.loading).toBe(false);
-                expect(this.props.data!.variables).toEqual(variables2);
-                expect(this.props.data!.allPeople).toEqual(data2.allPeople);
-                expect(this.props.data!.error).toBe(undefined);
-                expect(this.props.data!.networkStatus).toBe(NetworkStatus.ready);
-                done = true;
-                break;
+      let done = false;
+      const Container = graphql<Vars, Data, Vars>(query, {
+        options: (props) => ({
+          variables: props,
+          notifyOnNetworkStatusChange: true,
+        }),
+      })(
+        class extends React.Component<ChildProps<Vars, Data, Vars>> {
+          componentDidUpdate(prevProps: ChildProps<Vars, Data, Vars>) {
+            try {
+              // variables changed, new query is loading, but old data is still there
+              switch (++count) {
+                case 1:
+                  expect(prevProps.data!.loading).toBe(true);
+                  expect(prevProps.data!.variables).toEqual(variables1);
+                  expect(prevProps.data!.allPeople).toBe(undefined);
+                  expect(prevProps.data!.error).toBe(undefined);
+                  expect(prevProps.data!.networkStatus).toBe(
+                    NetworkStatus.loading
+                  );
+                  expect(this.props.data!.loading).toBe(false);
+                  expect(this.props.data!.variables).toEqual(variables1);
+                  expect(this.props.data!.allPeople).toEqual(data1.allPeople);
+                  expect(this.props.data!.error).toBe(undefined);
+                  expect(this.props.data!.networkStatus).toBe(
+                    NetworkStatus.ready
+                  );
+                  break;
+                case 2:
+                  expect(prevProps.data!.loading).toBe(false);
+                  expect(prevProps.data!.variables).toEqual(variables1);
+                  expect(prevProps.data!.allPeople).toEqual(data1.allPeople);
+                  expect(prevProps.data!.error).toBe(undefined);
+                  expect(this.props.data!.loading).toBe(true);
+                  expect(this.props.data!.variables).toEqual(variables2);
+                  expect(this.props.data!.allPeople).toBe(undefined);
+                  expect(this.props.data!.error).toBe(undefined);
+                  expect(this.props.data!.networkStatus).toBe(
+                    NetworkStatus.setVariables
+                  );
+                  break;
+                case 3:
+                  expect(prevProps.data!.loading).toBe(true);
+                  expect(prevProps.data!.variables).toEqual(variables2);
+                  expect(prevProps.data!.allPeople).toBe(undefined);
+                  expect(prevProps.data!.error).toBe(undefined);
+                  expect(prevProps.data!.networkStatus).toBe(
+                    NetworkStatus.setVariables
+                  );
+                  expect(this.props.data!.loading).toBe(false);
+                  expect(this.props.data!.variables).toEqual(variables2);
+                  expect(this.props.data!.allPeople).toEqual(data2.allPeople);
+                  expect(this.props.data!.error).toBe(undefined);
+                  expect(this.props.data!.networkStatus).toBe(
+                    NetworkStatus.ready
+                  );
+                  done = true;
+                  break;
+              }
+            } catch (err) {
+              reject(err);
             }
-          } catch (err) {
-            reject(err);
+          }
+          render() {
+            return null;
           }
         }
-        render() {
-          return null;
-        }
-      }
-    );
+      );
 
-    class ChangingProps extends React.Component<any, any> {
-      state = { first: 1 };
+      class ChangingProps extends React.Component<any, any> {
+        state = { first: 1 };
 
-      componentDidMount() {
-        setTimeout(() => {
-          this.setState({ first: 2 });
-        }, 50);
-      }
+        componentDidMount() {
+          setTimeout(() => {
+            this.setState({ first: 2 });
+          }, 50);
+        }
 
-      render() {
-        return <Container first={this.state.first} />;
+        render() {
+          return <Container first={this.state.first} />;
+        }
       }
-    }
 
-    render(
-      <ApolloProvider client={client}>
-        <ChangingProps />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <ChangingProps />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(done).toBe(true)).then(resolve, reject);
-  });
+      waitFor(() => expect(done).toBe(true)).then(resolve, reject);
+    }
+  );
 
-  itAsync('resets the loading state after a refetched query', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "resets the loading state after a refetched query",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
 
-    type Data = typeof data;
+      type Data = typeof data;
 
-    const link = mockSingleLink(
-      { request: { query }, result: { data } },
-      { request: { query }, result: { data: data2 } }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      const link = mockSingleLink(
+        { request: { query }, result: { data } },
+        { request: { query }, result: { data: data2 } }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let count = 0;
-    const Container = graphql<{}, Data>(query, {
-      options: { notifyOnNetworkStatusChange: true }
-    })(
-      class extends React.Component<ChildProps<{}, Data>> {
-        componentDidUpdate() {
-          const { data } = this.props;
-          switch (count++) {
-            case 0:
-              expect(data!.networkStatus).toBe(7);
-              // this isn't reloading fully
-              setTimeout(() => {
-                data!.refetch();
-              });
-              break;
-            case 1:
-              if (IS_REACT_18) {
-                expect(data!.loading).toBeFalsy();
-                expect(data!.networkStatus).toBe(NetworkStatus.ready);
-              } else {
+      let count = 0;
+      const Container = graphql<{}, Data>(query, {
+        options: { notifyOnNetworkStatusChange: true },
+      })(
+        class extends React.Component<ChildProps<{}, Data>> {
+          componentDidUpdate() {
+            const { data } = this.props;
+            switch (count++) {
+              case 0:
+                expect(data!.networkStatus).toBe(7);
+                // this isn't reloading fully
+                setTimeout(() => {
+                  data!.refetch();
+                });
+                break;
+              case 1:
                 expect(data!.loading).toBeTruthy();
                 expect(data!.networkStatus).toBe(NetworkStatus.refetch);
-              }
-              expect(data!.allPeople).toEqual(data!.allPeople);
-              break;
-            case 2:
-              expect(data!.loading).toBeFalsy();
-              expect(data!.networkStatus).toBe(7);
-              expect(data!.allPeople).toEqual(data2.allPeople);
-              break;
-            default:
-              reject(new Error('Too many props updates'));
+                expect(data!.allPeople).toEqual(data!.allPeople);
+                break;
+              case 2:
+                expect(data!.loading).toBeFalsy();
+                expect(data!.networkStatus).toBe(7);
+                expect(data!.allPeople).toEqual(data2.allPeople);
+                break;
+              default:
+                reject(new Error("Too many props updates"));
+            }
           }
-        }
 
-        render() {
-          return null;
+          render() {
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toBe(2)
-      } else {
-        expect(count).toBe(3)
-      }
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(count).toBe(3);
+      }).then(resolve, reject);
+    }
+  );
 
-  it('correctly sets loading state on remounted network-only query', async () => {
+  it("correctly sets loading state on remounted network-only query", async () => {
     const query: DocumentNode = gql`
       query pollingPeople {
         allPeople(first: 1) {
@@ -350,7 +367,7 @@ describe('[queries] loading', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Darth Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Darth Skywalker" }] } };
     type Data = typeof data;
 
     const link = mockSingleLink({
@@ -359,16 +376,16 @@ describe('[queries] loading', () => {
       newData: () => ({
         data: {
           allPeople: {
-            people: [{ name: `Darth Skywalker - ${Math.random()}` }]
-          }
-        }
-      })
+            people: [{ name: `Darth Skywalker - ${Math.random()}` }],
+          },
+        },
+      }),
     });
 
     const client = new ApolloClient({
       link,
       cache: new Cache({ addTypename: false }),
-      queryDeduplication: false
+      queryDeduplication: false,
     });
 
     let count = 0;
@@ -376,7 +393,7 @@ describe('[queries] loading', () => {
     const usedFetchPolicies: WatchQueryFetchPolicy[] = [];
     const Container = graphql<{}, Data>(query, {
       options: {
-        fetchPolicy: 'network-only',
+        fetchPolicy: "network-only",
         nextFetchPolicy(currentFetchPolicy, info) {
           if (info.reason === "variables-changed") {
             return info.initialFetchPolicy;
@@ -386,7 +403,7 @@ describe('[queries] loading', () => {
             return "cache-first";
           }
           return currentFetchPolicy;
-        }
+        },
       },
     })(
       class extends React.Component<ChildProps<{}, Data>> {
@@ -426,292 +443,319 @@ describe('[queries] loading', () => {
 
     render(App);
 
-    await waitFor(() => {
-      expect(usedFetchPolicies).toEqual([
-        "network-only",
-        "network-only",
-        "cache-first",
-      ]);
-    }, { interval: 1 });
-    await waitFor(() => {
+    await waitFor(
+      () => {
+        expect(usedFetchPolicies).toEqual([
+          "network-only",
+          "network-only",
+          "cache-first",
+        ]);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
         expect(count).toBe(5);
-    }, { interval: 1 });
+      },
+      { interval: 1 }
+    );
   });
 
-  itAsync('correctly sets loading state on remounted component with changed variables', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query remount($first: Int) {
-        allPeople(first: $first) {
-          people {
-            name
+  itAsync(
+    "correctly sets loading state on remounted component with changed variables",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query remount($first: Int) {
+          allPeople(first: $first) {
+            people {
+              name
+            }
           }
         }
+      `;
+
+      interface Data {
+        allPeople: {
+          people: { name: string }[];
+        };
       }
-    `;
+      const data = { allPeople: null };
+      const variables = { first: 1 };
+      const variables2 = { first: 2 };
 
-    interface Data {
-      allPeople: {
-        people: { name: string }[];
-      };
-    }
-    const data = { allPeople: null };
-    const variables = { first: 1 };
-    const variables2 = { first: 2 };
+      type Vars = typeof variables;
 
-    type Vars = typeof variables;
+      const link = mockSingleLink(
+        { request: { query, variables }, result: { data }, delay: 10 },
+        {
+          request: { query, variables: variables2 },
+          result: { data },
+          delay: 10,
+        }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+      let renderFn: (num: number) => React.ReactElement<any>,
+        count = 0;
+      const testFailures: any[] = [];
 
-    const link = mockSingleLink(
-      { request: { query, variables }, result: { data }, delay: 10 },
-      {
-        request: { query, variables: variables2 },
-        result: { data },
-        delay: 10
+      interface Props {
+        first: number;
       }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-    let renderFn: (num: number) => React.ReactElement<any>,
-      count = 0;
+      const Container = graphql<Props, Data, Vars>(query, {
+        options: ({ first }) => ({ variables: { first } }),
+      })(
+        class extends React.Component<ChildProps<Props, Data, Vars>> {
+          componentDidUpdate() {
+            try {
+              if (count === 0) {
+                // has data
+                unmount();
+                setTimeout(() => {
+                  render(renderFn(2));
+                }, 5);
+              }
 
-    interface Props {
-      first: number;
-    }
-    const Container = graphql<Props, Data, Vars>(query, {
-      options: ({ first }) => ({ variables: { first } })
-    })(
-      class extends React.Component<ChildProps<Props, Data, Vars>> {
-        componentDidUpdate() {
-          if (count === 0) {
-            // has data
-            unmount();
-            setTimeout(() => {
-              render(renderFn(2));
-            }, 5);
+              if (count === 2) {
+                // remounted data after fetch
+                expect(this.props.data!.loading).toBeFalsy();
+              }
+              count++;
+            } catch (e) {
+              testFailures.push(e);
+            }
           }
 
-          if (count === 2) {
-            // remounted data after fetch
-            expect(this.props.data!.loading).toBeFalsy();
-          }
-          count++;
-        }
+          render() {
+            try {
+              if (count === 1) {
+                expect(this.props.data!.loading).toBeTruthy(); // on remount
+                count++;
+              }
+            } catch (e) {
+              testFailures.push(e);
+            }
 
-        render() {
-          if (count === 1) {
-            expect(this.props.data!.loading).toBeTruthy(); // on remount
-            count++;
+            return null;
           }
-
-          return null;
         }
-      }
-    );
+      );
 
-    renderFn = (first: number) => (
-      <ApolloProvider client={client}>
-        <Container first={first} />
-      </ApolloProvider>
-    );
-    const { unmount } = render(renderFn(1));
-    waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toBe(0)
-      } else {
-        expect(count).toBe(3)
-      }
-    }).then(resolve, reject);
-  });
+      renderFn = (first: number) => (
+        <ApolloProvider client={client}>
+          <Container first={first} />
+        </ApolloProvider>
+      );
+      const { unmount } = render(renderFn(1));
+      waitFor(() => {
+        if (testFailures.length > 0) {
+          throw testFailures[0];
+        }
+        expect(count).toBe(3);
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('correctly sets loading state on remounted component with changed variables (alt)', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query remount($name: String) {
-        allPeople(name: $name) {
-          people {
-            name
+  itAsync(
+    "correctly sets loading state on remounted component with changed variables (alt)",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query remount($name: String) {
+          allPeople(name: $name) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
+      `;
 
-    interface Data {
-      allPeople: {
-        people: { name: string }[];
-      };
-    }
-    const data = { allPeople: null };
-    const variables = { name: 'does-not-exist' };
-    const variables2 = { name: 'nothing-either' };
+      interface Data {
+        allPeople: {
+          people: { name: string }[];
+        };
+      }
+      const data = { allPeople: null };
+      const variables = { name: "does-not-exist" };
+      const variables2 = { name: "nothing-either" };
 
-    type Vars = typeof variables;
+      type Vars = typeof variables;
 
-    const link = mockSingleLink(
-      { request: { query, variables }, result: { data } },
-      {
-        request: { query, variables: variables2 },
-        result: { data }
-      }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      const link = mockSingleLink(
+        { request: { query, variables }, result: { data } },
+        {
+          request: { query, variables: variables2 },
+          result: { data },
+        }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let count = 0;
+      let count = 0;
 
-    const Container = graphql<Vars, Data, Vars>(query)(
-      class extends React.Component<ChildProps<Vars, Data, Vars>> {
-        render() {
-          const { loading } = this.props.data!;
-          if (count === 0) expect(loading).toBeTruthy();
-          if (count === 1) {
-            expect(loading).toBeFalsy();
-            setTimeout(() => {
-              unmount();
-              render(
-                <ApolloProvider client={client}>
-                  <Container {...variables2} />
-                </ApolloProvider>
-              );
-            }, 0);
-          }
-          if (count === 2) expect(loading).toBeTruthy();
-          if (count === 3) {
-            expect(loading).toBeFalsy();
-          }
-          count++;
-          return null;
+      const Container = graphql<Vars, Data, Vars>(query)(
+        class extends React.Component<ChildProps<Vars, Data, Vars>> {
+          render() {
+            const { loading } = this.props.data!;
+            if (count === 0) expect(loading).toBeTruthy();
+            if (count === 1) {
+              expect(loading).toBeFalsy();
+              setTimeout(() => {
+                unmount();
+                render(
+                  <ApolloProvider client={client}>
+                    <Container {...variables2} />
+                  </ApolloProvider>
+                );
+              }, 0);
+            }
+            if (count === 2) expect(loading).toBeTruthy();
+            if (count === 3) {
+              expect(loading).toBeFalsy();
+            }
+            count++;
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    const { unmount } = render(
-      <ApolloProvider client={client}>
-        <Container {...variables} />
-      </ApolloProvider>
-    );
+      const { unmount } = render(
+        <ApolloProvider client={client}>
+          <Container {...variables} />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(count).toBe(4)).then(resolve, reject);
-  });
+      waitFor(() => expect(count).toBe(4)).then(resolve, reject);
+    }
+  );
 
-  itAsync('correctly sets loading state on component with changed variables and unchanged result', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query remount($first: Int) {
-        allPeople(first: $first) {
-          people {
-            name
+  itAsync(
+    "correctly sets loading state on component with changed variables and unchanged result",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query remount($first: Int) {
+          allPeople(first: $first) {
+            people {
+              name
+            }
           }
         }
+      `;
+      interface Data {
+        allPeople: {
+          people: { name: string }[];
+        };
       }
-    `;
-    interface Data {
-      allPeople: {
-        people: { name: string }[];
-      };
-    }
 
-    const data = { allPeople: null };
-    const variables = { first: 1 };
-    const variables2 = { first: 2 };
+      const data = { allPeople: null };
+      const variables = { first: 1 };
+      const variables2 = { first: 2 };
 
-    type Vars = typeof variables;
-    const link = mockSingleLink(
-      { request: { query, variables }, result: { data } },
-      {
-        request: { query, variables: variables2 },
-        result: { data }
+      type Vars = typeof variables;
+      const link = mockSingleLink(
+        { request: { query, variables }, result: { data } },
+        {
+          request: { query, variables: variables2 },
+          result: { data },
+        }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+      let count = 0;
+
+      interface Props extends Vars {
+        setFirst: (first: number) => void;
       }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-    let count = 0;
 
-    interface Props extends Vars {
-      setFirst: (first: number) => void;
-    }
+      const connect = (
+        component: React.ComponentType<
+          React.PropsWithChildren<React.PropsWithChildren<Props>>
+        >
+      ): React.ComponentType<
+        React.PropsWithChildren<React.PropsWithChildren<{}>>
+      > => {
+        return class extends React.Component<{}, { first: number }> {
+          constructor(props: {}) {
+            super(props);
 
-    const connect = (
-      component: React.ComponentType<React.PropsWithChildren<React.PropsWithChildren<Props>>>
-    ): React.ComponentType<React.PropsWithChildren<React.PropsWithChildren<{}>>> => {
-      return class extends React.Component<{}, { first: number }> {
-        constructor(props: {}) {
-          super(props);
-
-          this.state = {
-            first: 1
-          };
-          this.setFirst = this.setFirst.bind(this);
-        }
+            this.state = {
+              first: 1,
+            };
+            this.setFirst = this.setFirst.bind(this);
+          }
 
-        setFirst(first: number) {
-          this.setState({ first });
-        }
+          setFirst(first: number) {
+            this.setState({ first });
+          }
 
-        render() {
-          return React.createElement(component, {
-            first: this.state.first,
-            setFirst: this.setFirst
-          });
-        }
+          render() {
+            return React.createElement(component, {
+              first: this.state.first,
+              setFirst: this.setFirst,
+            });
+          }
+        };
       };
-    };
 
-    const Container = connect(
-      graphql<Props, Data, Vars>(query, {
-        options: ({ first }) => ({ variables: { first } })
-      })(
-        class extends React.Component<ChildProps<Props, Data, Vars>> {
-          render() {
-            try {
-              switch (count) {
-                case 0:
-                  expect(this.props.data!.loading).toBe(true);
-                  expect(this.props.data!.allPeople).toBeUndefined();
-                  break;
-                case 1:
-                  expect(this.props.data!.loading).toBe(false);
-                  expect(this.props.data!.allPeople).toBe(data.allPeople);
-                  setTimeout(() => {
-                    this.props.setFirst(2);
-                  });
-                  break;
-                case 2:
-                  expect(this.props.data!.loading).toBe(true); // on variables change
-                  expect(this.props.data!.allPeople).toBeUndefined();
-                  break;
-                case 4:
-                  // new data after fetch
-                  expect(this.props.data!.loading).toBe(false);
-                  expect(this.props.data!.allPeople).toBe(data.allPeople);
-                  break;
+      const Container = connect(
+        graphql<Props, Data, Vars>(query, {
+          options: ({ first }) => ({ variables: { first } }),
+        })(
+          class extends React.Component<ChildProps<Props, Data, Vars>> {
+            render() {
+              try {
+                switch (count) {
+                  case 0:
+                    expect(this.props.data!.loading).toBe(true);
+                    expect(this.props.data!.allPeople).toBeUndefined();
+                    break;
+                  case 1:
+                    expect(this.props.data!.loading).toBe(false);
+                    expect(this.props.data!.allPeople).toBe(data.allPeople);
+                    setTimeout(() => {
+                      this.props.setFirst(2);
+                    });
+                    break;
+                  case 2:
+                    expect(this.props.data!.loading).toBe(true); // on variables change
+                    expect(this.props.data!.allPeople).toBeUndefined();
+                    break;
+                  case 4:
+                    // new data after fetch
+                    expect(this.props.data!.loading).toBe(false);
+                    expect(this.props.data!.allPeople).toBe(data.allPeople);
+                    break;
+                }
+              } catch (err) {
+                reject(err);
               }
-            } catch (err) {
-              reject(err);
-            }
 
-            count++;
+              count++;
 
-            return null;
+              return null;
+            }
           }
-        }
-      )
-    );
+        )
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(count).toBe(4)).then(resolve, reject);
-  });
+      waitFor(() => expect(count).toBe(4)).then(resolve, reject);
+    }
+  );
 
   itAsync(
-    'correctly sets loading state on component with changed variables, ' +
-      'unchanged result, and network-only',
+    "correctly sets loading state on component with changed variables, " +
+      "unchanged result, and network-only",
     (resolve, reject) => {
       const query: DocumentNode = gql`
         query remount($first: Int) {
@@ -728,7 +772,7 @@ describe('[queries] loading', () => {
         };
       }
 
-      const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
       const variables = { first: 1 };
       const variables2 = { first: 2 };
 
@@ -738,12 +782,12 @@ describe('[queries] loading', () => {
         {
           request: { query, variables: variables2 },
           result: { data },
-          delay: 10
+          delay: 10,
         }
       );
       const client = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
       let count = 0;
 
@@ -752,14 +796,18 @@ describe('[queries] loading', () => {
       }
 
       const connect = (
-        component: React.ComponentType<React.PropsWithChildren<React.PropsWithChildren<Props>>>
-      ): React.ComponentType<React.PropsWithChildren<React.PropsWithChildren<{}>>> => {
+        component: React.ComponentType<
+          React.PropsWithChildren<React.PropsWithChildren<Props>>
+        >
+      ): React.ComponentType<
+        React.PropsWithChildren<React.PropsWithChildren<{}>>
+      > => {
         return class extends React.Component<{}, { first: number }> {
           constructor(props: {}) {
             super(props);
 
             this.state = {
-              first: 1
+              first: 1,
             };
             this.setFirst = this.setFirst.bind(this);
           }
@@ -771,7 +819,7 @@ describe('[queries] loading', () => {
           render() {
             return React.createElement(component, {
               first: this.state.first,
-              setFirst: this.setFirst
+              setFirst: this.setFirst,
             });
           }
         };
@@ -781,8 +829,8 @@ describe('[queries] loading', () => {
         graphql<Props, Data, Vars>(query, {
           options: ({ first }) => ({
             variables: { first },
-            fetchPolicy: 'network-only'
-          })
+            fetchPolicy: "network-only",
+          }),
         })(
           class extends React.Component<ChildProps<Props, Data, Vars>> {
             render() {
diff --git a/src/react/hoc/__tests__/queries/observableQuery.test.tsx b/src/react/hoc/__tests__/queries/observableQuery.test.tsx
--- a/src/react/hoc/__tests__/queries/observableQuery.test.tsx
+++ b/src/react/hoc/__tests__/queries/observableQuery.test.tsx
@@ -1,19 +1,19 @@
-import React from 'react';
-import userEvent from '@testing-library/user-event';
-import { render, waitFor, screen } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-describe('[queries] observableQuery', () => {
+import React from "react";
+import userEvent from "@testing-library/user-event";
+import { render, waitFor, screen } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("[queries] observableQuery", () => {
   // observableQuery
-  it('will recycle `ObservableQuery`s when re-rendering the entire tree', async () => {
+  it("will recycle `ObservableQuery`s when re-rendering the entire tree", async () => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -23,7 +23,7 @@ describe('[queries] observableQuery', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const link = mockSingleLink(
@@ -32,7 +32,7 @@ describe('[queries] observableQuery', () => {
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
@@ -41,32 +41,30 @@ describe('[queries] observableQuery', () => {
       const keys = Array.from(
         ((client as any).queryManager as any).queries.keys()
       );
-      await waitFor(() => expect(keys).toEqual(['1']), { interval: 1 });
+      await waitFor(() => expect(keys).toEqual(["1"]), { interval: 1 });
     };
 
     const assert2 = async () => {
       const keys = Array.from(
         ((client as any).queryManager as any).queries.keys()
       );
-      await waitFor(() => expect(keys).toEqual(['1']), { interval: 1 });
+      await waitFor(() => expect(keys).toEqual(["1"]), { interval: 1 });
     };
 
     let done = false;
     const Container = graphql<{}, Data>(query, {
-      options: { fetchPolicy: 'cache-and-network' }
+      options: { fetchPolicy: "cache-and-network" },
     })(
       class extends React.Component<ChildProps<{}, Data>> {
         async componentDidUpdate() {
           if (count === 2) {
             expect(this.props.data!.loading).toBeFalsy();
-            expect(this.props.data!.allPeople).toEqual(
-              data.allPeople
-            );
+            expect(this.props.data!.allPeople).toEqual(data.allPeople);
 
             // ensure first assertion and umount tree
             await assert1();
 
-            userEvent.click(screen.getByText('Break things'));
+            userEvent.click(screen.getByText("Break things"));
 
             // ensure cleanup
             await assert2();
@@ -87,9 +85,7 @@ describe('[queries] observableQuery', () => {
           // be present;
           if (count === 3) {
             expect(this.props.data!.loading).toBeFalsy();
-            expect(this.props.data!.allPeople).toEqual(
-              data.allPeople
-            );
+            expect(this.props.data!.allPeople).toEqual(data.allPeople);
           }
           count++;
           return null;
@@ -109,7 +105,7 @@ describe('[queries] observableQuery', () => {
 
     class AppWrapper extends React.Component<{}, { renderRedirect: boolean }> {
       state = {
-        renderRedirect: false
+        renderRedirect: false,
       };
 
       goToRedirect = () => {
@@ -147,129 +143,131 @@ describe('[queries] observableQuery', () => {
     });
   });
 
-  itAsync("will recycle `ObservableQuery`s when re-rendering a portion of the tree but not return stale data if variables don't match", (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people($first: Int!) {
-        allPeople(first: $first) {
-          people {
-            name
-            friends(id: $first) {
+  itAsync(
+    "will recycle `ObservableQuery`s when re-rendering a portion of the tree but not return stale data if variables don't match",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people($first: Int!) {
+          allPeople(first: $first) {
+            people {
               name
+              friends(id: $first) {
+                name
+              }
             }
           }
         }
-      }
-    `;
-    const variables1 = { first: 1 };
-    const variables2 = { first: 2 };
-    const data = {
-      allPeople: {
-        people: [{ name: 'Luke Skywalker', friends: [{ name: 'r2d2' }] }]
-      }
-    };
-    const data2 = {
-      allPeople: {
-        people: [{ name: 'Leia Skywalker', friends: [{ name: 'luke' }] }]
-      }
-    };
+      `;
+      const variables1 = { first: 1 };
+      const variables2 = { first: 2 };
+      const data = {
+        allPeople: {
+          people: [{ name: "Luke Skywalker", friends: [{ name: "r2d2" }] }],
+        },
+      };
+      const data2 = {
+        allPeople: {
+          people: [{ name: "Leia Skywalker", friends: [{ name: "luke" }] }],
+        },
+      };
 
-    type Data = typeof data;
-    type Vars = typeof variables1;
+      type Data = typeof data;
+      type Vars = typeof variables1;
 
-    const link = mockSingleLink(
-      { request: { query, variables: variables1 }, result: { data } },
-      { request: { query, variables: variables2 }, result: { data: data2 } }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-    let remount: any;
+      const link = mockSingleLink(
+        { request: { query, variables: variables1 }, result: { data } },
+        { request: { query, variables: variables2 }, result: { data: data2 } }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+      let remount: any;
+
+      const Container = graphql<Vars, Data, Vars>(query)(
+        class extends React.Component<ChildProps<Vars, Data, Vars>> {
+          render() {
+            try {
+              const { variables, loading, allPeople } = this.props.data!;
+              // first variable render
+              if (variables.first === 1) {
+                if (loading) expect(allPeople).toBeUndefined();
+                if (!loading) {
+                  expect(allPeople).toEqual(data.allPeople);
+                }
+              }
 
-    const Container = graphql<Vars, Data, Vars>(query)(
-      class extends React.Component<ChildProps<Vars, Data, Vars>> {
-        render() {
-          try {
-            const { variables, loading, allPeople } = this.props.data!;
-            // first variable render
-            if (variables.first === 1) {
-              if (loading) expect(allPeople).toBeUndefined();
-              if (!loading) {
-                expect(allPeople).toEqual(data.allPeople);
+              if (variables.first === 2) {
+                // second variables render
+                if (loading) expect(allPeople).toBeUndefined();
+                if (!loading) expect(allPeople).toEqual(data2.allPeople);
               }
+            } catch (e) {
+              reject(e);
             }
 
-            if (variables.first === 2) {
-              // second variables render
-              if (loading) expect(allPeople).toBeUndefined();
-              if (!loading)
-                expect(allPeople).toEqual(data2.allPeople);
-            }
-          } catch (e) {
-            reject(e);
+            return null;
           }
-
-          return null;
         }
-      }
-    );
-
-    class Remounter extends React.Component<
-      { render: typeof Container },
-      { showChildren: boolean; variables: Vars }
-    > {
-      state = {
-        showChildren: true,
-        variables: variables1
-      };
+      );
 
-      componentDidMount() {
-        remount = () => {
-          this.setState({ showChildren: false }, () => {
-            setTimeout(() => {
-              this.setState({
-                showChildren: true,
-                variables: variables2
-              });
-            }, 10);
-          });
+      class Remounter extends React.Component<
+        { render: typeof Container },
+        { showChildren: boolean; variables: Vars }
+      > {
+        state = {
+          showChildren: true,
+          variables: variables1,
         };
-      }
 
-      render() {
-        if (!this.state.showChildren) return null;
-        const Thing = this.props.render;
-        return <Thing first={this.state.variables.first} />;
+        componentDidMount() {
+          remount = () => {
+            this.setState({ showChildren: false }, () => {
+              setTimeout(() => {
+                this.setState({
+                  showChildren: true,
+                  variables: variables2,
+                });
+              }, 10);
+            });
+          };
+        }
+
+        render() {
+          if (!this.state.showChildren) return null;
+          const Thing = this.props.render;
+          return <Thing first={this.state.variables.first} />;
+        }
       }
-    }
 
-    // the initial mount fires off the query
-    // the same as episode id = 1
-    render(
-      <ApolloProvider client={client}>
-        <Remounter render={Container} />
-      </ApolloProvider>
-    );
+      // the initial mount fires off the query
+      // the same as episode id = 1
+      render(
+        <ApolloProvider client={client}>
+          <Remounter render={Container} />
+        </ApolloProvider>
+      );
 
-    // after the initial data has been returned
-    // the user navigates to a different page
-    // but the query is recycled
-    let done = false;
-    setTimeout(() => {
-      // move to the "home" page from the "episode" page
-      remount();
+      // after the initial data has been returned
+      // the user navigates to a different page
+      // but the query is recycled
+      let done = false;
       setTimeout(() => {
-        // move to a new "epsiode" page
-        // epsiode id = 2
-        // wait to verify the data isn't stale then end
-        done = true;
-      }, 20);
-    }, 5);
-
-    return waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
+        // move to the "home" page from the "episode" page
+        remount();
+        setTimeout(() => {
+          // move to a new "epsiode" page
+          // epsiode id = 2
+          // wait to verify the data isn't stale then end
+          done = true;
+        }, 20);
+      }, 5);
+
+      return waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
+    }
+  );
 
-  it('not overly rerender', async () => {
+  it("not overly rerender", async () => {
     const query: DocumentNode = gql`
       query people($first: Int!) {
         allPeople(first: $first) {
@@ -286,20 +284,20 @@ describe('[queries] observableQuery', () => {
     const variables = { first: 1 };
     const data = {
       allPeople: {
-        people: [{ name: 'Luke Skywalker', friends: [{ name: 'r2d2' }] }]
-      }
+        people: [{ name: "Luke Skywalker", friends: [{ name: "r2d2" }] }],
+      },
     };
     type Data = typeof data;
     type Vars = typeof variables;
 
     const link = mockSingleLink({
       request: { query, variables },
-      result: { data }
+      result: { data },
     });
 
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     let remount: any;
 
@@ -335,7 +333,7 @@ describe('[queries] observableQuery', () => {
     > {
       state = {
         showChildren: true,
-        variables
+        variables,
       };
 
       componentDidMount() {
@@ -382,93 +380,96 @@ describe('[queries] observableQuery', () => {
     });
   });
 
-  itAsync('does rerender if query returns differnt result', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people($first: Int!) {
-        allPeople(first: $first) {
-          people {
-            name
-            friends(id: $first) {
+  itAsync(
+    "does rerender if query returns differnt result",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people($first: Int!) {
+          allPeople(first: $first) {
+            people {
               name
+              friends(id: $first) {
+                name
+              }
             }
           }
         }
-      }
-    `;
-
-    const variables = { first: 1 };
-    const dataOne = {
-      allPeople: {
-        people: [{ name: 'Luke Skywalker', friends: [{ name: 'r2d2' }] }]
-      }
-    };
-    const dataTwo = {
-      allPeople: {
-        people: [
-          { name: 'Luke Skywalker', friends: [{ name: 'Leia Skywalker' }] }
-        ]
-      }
-    };
-
-    type Data = typeof dataOne;
-    type Vars = typeof variables;
+      `;
 
-    const link = mockSingleLink(
-      {
-        request: { query, variables },
-        result: { data: dataOne }
-      },
-      {
-        request: { query, variables },
-        result: { data: dataTwo }
-      }
-    );
+      const variables = { first: 1 };
+      const dataOne = {
+        allPeople: {
+          people: [{ name: "Luke Skywalker", friends: [{ name: "r2d2" }] }],
+        },
+      };
+      const dataTwo = {
+        allPeople: {
+          people: [
+            { name: "Luke Skywalker", friends: [{ name: "Leia Skywalker" }] },
+          ],
+        },
+      };
 
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      type Data = typeof dataOne;
+      type Vars = typeof variables;
+
+      const link = mockSingleLink(
+        {
+          request: { query, variables },
+          result: { data: dataOne },
+        },
+        {
+          request: { query, variables },
+          result: { data: dataTwo },
+        }
+      );
 
-    let count = 0;
-    const Container = graphql<Vars, Data, Vars>(query)(
-      class extends React.Component<ChildProps<Vars, Data, Vars>> {
-        render() {
-          count++;
-          try {
-            const { loading, allPeople, refetch } = this.props.data!;
-            // first variable render
-            if (count === 1) {
-              expect(loading).toBe(true);
-            }
-            if (count === 2) {
-              expect(loading).toBe(false);
-              expect(allPeople).toEqual(dataOne.allPeople);
-              refetch();
-            }
-            if (count === 3) {
-              expect(loading).toBe(false);
-              expect(allPeople).toEqual(dataTwo.allPeople);
-            }
-            if (count > 3) {
-              throw new Error('too many renders');
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      let count = 0;
+      const Container = graphql<Vars, Data, Vars>(query)(
+        class extends React.Component<ChildProps<Vars, Data, Vars>> {
+          render() {
+            count++;
+            try {
+              const { loading, allPeople, refetch } = this.props.data!;
+              // first variable render
+              if (count === 1) {
+                expect(loading).toBe(true);
+              }
+              if (count === 2) {
+                expect(loading).toBe(false);
+                expect(allPeople).toEqual(dataOne.allPeople);
+                refetch();
+              }
+              if (count === 3) {
+                expect(loading).toBe(false);
+                expect(allPeople).toEqual(dataTwo.allPeople);
+              }
+              if (count > 3) {
+                throw new Error("too many renders");
+              }
+            } catch (e) {
+              reject(e);
             }
-          } catch (e) {
-            reject(e);
-          }
 
-          return null;
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    // the initial mount fires off the query
-    // the same as episode id = 1
-    render(
-      <ApolloProvider client={client}>
-        <Container first={1} />
-      </ApolloProvider>
-    );
+      // the initial mount fires off the query
+      // the same as episode id = 1
+      render(
+        <ApolloProvider client={client}>
+          <Container first={1} />
+        </ApolloProvider>
+      );
 
-    return waitFor(() => expect(count).toBe(3)).then(resolve, reject);
-  });
+      return waitFor(() => expect(count).toBe(3)).then(resolve, reject);
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/queries/polling.test.tsx b/src/react/hoc/__tests__/queries/polling.test.tsx
--- a/src/react/hoc/__tests__/queries/polling.test.tsx
+++ b/src/react/hoc/__tests__/queries/polling.test.tsx
@@ -1,17 +1,17 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient, ApolloLink } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { Observable } from '../../../../utilities';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-describe('[queries] polling', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient, ApolloLink } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { Observable } from "../../../../utilities";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("[queries] polling", () => {
   let error: typeof console.error;
 
   beforeEach(() => {
@@ -25,7 +25,7 @@ describe('[queries] polling', () => {
   });
 
   // polling
-  itAsync('allows a polling query to be created', (resolve, reject) => {
+  itAsync("allows a polling query to be created", (resolve, reject) => {
     const POLL_INTERVAL = 5;
     const query: DocumentNode = gql`
       query people {
@@ -36,14 +36,14 @@ describe('[queries] polling', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+    const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
     const link = mockSingleLink(
       { request: { query }, result: { data } },
       { request: { query }, result: { data: data2 } },
       { request: { query }, result: { data } }
     );
-    const cache = new Cache({ addTypename: false })
+    const cache = new Cache({ addTypename: false });
     const client = new ApolloClient({
       link,
       cache,
@@ -54,12 +54,12 @@ describe('[queries] polling', () => {
       options: () => ({
         pollInterval: POLL_INTERVAL,
         notifyOnNetworkStatusChange: false,
-      })
+      }),
     })(({ data }) => {
       count++;
       if (count === 4) {
         data!.stopPolling();
-        expect(cache.readQuery({query})).toBeTruthy();
+        expect(cache.readQuery({ query })).toBeTruthy();
         resolve();
       }
       return null;
@@ -74,55 +74,58 @@ describe('[queries] polling', () => {
     waitFor(() => expect(count).toBe(4)).then(resolve, reject);
   });
 
-  itAsync('ensures polling respects no-cache fetchPolicy', (resolve, reject) => {
-    const POLL_INTERVAL = 5;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "ensures polling respects no-cache fetchPolicy",
+    (resolve, reject) => {
+      const POLL_INTERVAL = 5;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
-    const link = mockSingleLink(
-      { request: { query }, result: { data } },
-      { request: { query }, result: { data: data2 } },
-      { request: { query }, result: { data } }
-    );
-    const cache = new Cache({ addTypename: false })
-    const client = new ApolloClient({
-      link,
-      cache,
-    });
-
-    let count = 0;
-    const Container = graphql(query, {
-      options: () => ({
-        pollInterval: POLL_INTERVAL,
-        notifyOnNetworkStatusChange: false,
-        fetchPolicy: 'no-cache'
-      })
-    })(({ data }) => {
-      count++;
-      if (count === 4) {
-        data!.stopPolling();
-        expect(cache.readQuery({query})).toBeNull();
-        resolve();
-      }
-      return null;
-    });
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
+      const link = mockSingleLink(
+        { request: { query }, result: { data } },
+        { request: { query }, result: { data: data2 } },
+        { request: { query }, result: { data } }
+      );
+      const cache = new Cache({ addTypename: false });
+      const client = new ApolloClient({
+        link,
+        cache,
+      });
+
+      let count = 0;
+      const Container = graphql(query, {
+        options: () => ({
+          pollInterval: POLL_INTERVAL,
+          notifyOnNetworkStatusChange: false,
+          fetchPolicy: "no-cache",
+        }),
+      })(({ data }) => {
+        count++;
+        if (count === 4) {
+          data!.stopPolling();
+          expect(cache.readQuery({ query })).toBeNull();
+          resolve();
+        }
+        return null;
+      });
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(count).toBe(4)).then(resolve, reject);
-  });
+      waitFor(() => expect(count).toBe(4)).then(resolve, reject);
+    }
+  );
 
   const allPeopleQuery: DocumentNode = gql`
     query people {
@@ -134,25 +137,26 @@ describe('[queries] polling', () => {
     }
   `;
 
-  const lukeLink = new ApolloLink(operation => new Observable(observer => {
-    expect(operation.query).toBe(allPeopleQuery);
-    observer.next({
-      data: {
-        allPeople: {
-          people: [
-            { name: "Luke Skywalker" },
-          ],
-        },
-      },
-    });
-    observer.complete();
-  }));
+  const lukeLink = new ApolloLink(
+    (operation) =>
+      new Observable((observer) => {
+        expect(operation.query).toBe(allPeopleQuery);
+        observer.next({
+          data: {
+            allPeople: {
+              people: [{ name: "Luke Skywalker" }],
+            },
+          },
+        });
+        observer.complete();
+      })
+  );
 
-  itAsync('exposes stopPolling as part of the props api', (resolve, reject) => {
+  itAsync("exposes stopPolling as part of the props api", (resolve, reject) => {
     let done = false;
     const client = new ApolloClient({
       link: lukeLink,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql(allPeopleQuery)(
@@ -184,39 +188,44 @@ describe('[queries] polling', () => {
     }).then(resolve, reject);
   });
 
-  itAsync('exposes startPolling as part of the props api', (resolve, reject) => {
-    let done = false;
-    const client = new ApolloClient({
-      link: lukeLink,
-      cache: new Cache({ addTypename: false })
-    });
-
-    const Container = graphql(allPeopleQuery, { options: { pollInterval: 10 } })(
-      class extends React.Component<ChildProps> {
-        componentDidUpdate() {
-          try {
-            const { data } = this.props;
-            expect(data!.startPolling).toBeTruthy();
-            expect(data!.startPolling instanceof Function).toBeTruthy();
-            done = true;
-          } catch (e) {
-            reject(e);
+  itAsync(
+    "exposes startPolling as part of the props api",
+    (resolve, reject) => {
+      let done = false;
+      const client = new ApolloClient({
+        link: lukeLink,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      const Container = graphql(allPeopleQuery, {
+        options: { pollInterval: 10 },
+      })(
+        class extends React.Component<ChildProps> {
+          componentDidUpdate() {
+            try {
+              const { data } = this.props;
+              expect(data!.startPolling).toBeTruthy();
+              expect(data!.startPolling instanceof Function).toBeTruthy();
+              done = true;
+            } catch (e) {
+              reject(e);
+            }
+          }
+          render() {
+            return null;
           }
         }
-        render() {
-          return null;
-        }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true);
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/queries/recomposeWithState.ts b/src/react/hoc/__tests__/queries/recomposeWithState.ts
--- a/src/react/hoc/__tests__/queries/recomposeWithState.ts
+++ b/src/react/hoc/__tests__/queries/recomposeWithState.ts
@@ -2,7 +2,6 @@
 // to avoid incurring an indirect dependency on ua-parser-js via fbjs.
 
 import React, { createFactory, Component } from "react";
-import '../../../../utilities/globals'; // For __DEV__
 
 const setStatic =
   (key: string, value: string) => (BaseComponent: React.ComponentClass) => {
@@ -35,7 +34,10 @@ export const withState =
   (stateName: string, stateUpdaterName: string, initialState: unknown) =>
   (BaseComponent: React.ComponentClass) => {
     const factory = createFactory(BaseComponent);
-    class WithState extends Component<Record<string, unknown>, { stateValue: unknown }> {
+    class WithState extends Component<
+      Record<string, unknown>,
+      { stateValue: unknown }
+    > {
       state = {
         stateValue:
           typeof initialState === "function"
diff --git a/src/react/hoc/__tests__/queries/reducer.test.tsx b/src/react/hoc/__tests__/queries/reducer.test.tsx
--- a/src/react/hoc/__tests__/queries/reducer.test.tsx
+++ b/src/react/hoc/__tests__/queries/reducer.test.tsx
@@ -1,18 +1,18 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { DataValue } from '../../types';
-
-describe('[queries] reducer', () => {
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { DataValue } from "../../types";
+
+describe("[queries] reducer", () => {
   // props reducer
-  itAsync('allows custom mapping of a result to props', (resolve, reject) => {
+  itAsync("allows custom mapping of a result to props", (resolve, reject) => {
     const query: DocumentNode = gql`
       query thing {
         getThing {
@@ -23,11 +23,11 @@ describe('[queries] reducer', () => {
     const result = { getThing: { thing: true } };
     const link = mockSingleLink({
       request: { query },
-      result: { data: result }
+      result: { data: result },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     type Data = typeof result;
@@ -36,7 +36,7 @@ describe('[queries] reducer', () => {
 
     let count = 0;
     const ContainerWithData = graphql<{}, Data, {}, ChildProps>(query, {
-      props: ({ data }) => ({ ...data! })
+      props: ({ data }) => ({ ...data! }),
     })(({ getThing, loading }) => {
       count++;
       if (count === 1) expect(loading).toBe(true);
@@ -55,60 +55,63 @@ describe('[queries] reducer', () => {
     waitFor(() => expect(count).toBe(2)).then(resolve, reject);
   });
 
-  itAsync('allows custom mapping of a result to props that includes the passed props', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query thing {
-        getThing {
-          thing
+  itAsync(
+    "allows custom mapping of a result to props that includes the passed props",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query thing {
+          getThing {
+            thing
+          }
         }
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: { data: { getThing: { thing: true } } },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      interface Data {
+        getThing: { thing: boolean };
       }
-    `;
-    const link = mockSingleLink({
-      request: { query },
-      result: { data: { getThing: { thing: true } } }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    interface Data {
-      getThing: { thing: boolean };
-    }
-    interface Props {
-      sample: number;
-    }
-
-    type FinalProps = {
-      showSpinner: boolean;
-    };
-
-    let count = 0;
-    const ContainerWithData = graphql<Props, Data, {}, FinalProps>(query, {
-      props: ({ data, ownProps }) => {
-        expect(ownProps.sample).toBe(1);
-        return { showSpinner: data!.loading };
-      }
-    })(({ showSpinner }: FinalProps) => {
-      if (count === 0) {
-        expect(showSpinner).toBeTruthy();
+      interface Props {
+        sample: number;
       }
-      count += 1;
-      return null;
-    });
 
-    render(
-      <ApolloProvider client={client}>
-        <ContainerWithData sample={1} />
-      </ApolloProvider>
-    );
+      type FinalProps = {
+        showSpinner: boolean;
+      };
+
+      let count = 0;
+      const ContainerWithData = graphql<Props, Data, {}, FinalProps>(query, {
+        props: ({ data, ownProps }) => {
+          expect(ownProps.sample).toBe(1);
+          return { showSpinner: data!.loading };
+        },
+      })(({ showSpinner }: FinalProps) => {
+        if (count === 0) {
+          expect(showSpinner).toBeTruthy();
+        }
+        count += 1;
+        return null;
+      });
 
-    waitFor(() => {
-      expect(count).toBe(2);
-    }).then(resolve, reject);
-  });
+      render(
+        <ApolloProvider client={client}>
+          <ContainerWithData sample={1} />
+        </ApolloProvider>
+      );
+
+      waitFor(() => {
+        expect(count).toBe(2);
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('allows custom mapping of a result to props 2', (resolve, reject) => {
+  itAsync("allows custom mapping of a result to props 2", (resolve, reject) => {
     let done = false;
     const query: DocumentNode = gql`
       query thing {
@@ -120,11 +123,11 @@ describe('[queries] reducer', () => {
     const expectedData = { getThing: { thing: true } };
     const link = mockSingleLink({
       request: { query },
-      result: { data: expectedData }
+      result: { data: expectedData },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Data {
@@ -136,7 +139,7 @@ describe('[queries] reducer', () => {
     }
 
     const withData = graphql<{}, Data, {}, FinalProps>(query, {
-      props: ({ data }) => ({ thingy: data!.getThing! })
+      props: ({ data }) => ({ thingy: data!.getThing! }),
     });
 
     class Container extends React.Component<FinalProps> {
@@ -158,85 +161,89 @@ describe('[queries] reducer', () => {
     );
 
     waitFor(() => {
-      expect(done).toBe(true)
+      expect(done).toBe(true);
     }).then(resolve, reject);
   });
 
-  itAsync('passes the prior props to the result-props mapper', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query thing {
-        getThing {
-          thing
+  itAsync(
+    "passes the prior props to the result-props mapper",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query thing {
+          getThing {
+            thing
+          }
+          other
         }
-        other
-      }
-    `;
-    const expectedData = { getThing: { thing: true }, other: false };
-    const expectedDataAfterRefetch = { getThing: { thing: true }, other: true };
-    const link = mockSingleLink(
-      {
-        request: { query },
-        result: { data: expectedData }
-      },
-      {
-        request: { query },
-        result: { data: expectedDataAfterRefetch }
-      }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    type Data = typeof expectedData;
-    interface FinalProps {
-      wrapper: { thingy: { thing: boolean } };
-      refetch: () => any;
-    }
-
-    const withData = graphql<{}, Data, {}, FinalProps>(query, {
-      props: ({ data }, lastProps) => {
-        const refetch = data!.refetch!;
-        let wrapper = { thingy: data!.getThing! };
-
-        // If the current thingy is equal to the last thingy,
-        // reuse the wrapper (to preserve referential equality).
-        if (lastProps && lastProps.wrapper.thingy === wrapper.thingy) {
-          wrapper = lastProps!.wrapper!;
+      `;
+      const expectedData = { getThing: { thing: true }, other: false };
+      const expectedDataAfterRefetch = {
+        getThing: { thing: true },
+        other: true,
+      };
+      const link = mockSingleLink(
+        {
+          request: { query },
+          result: { data: expectedData },
+        },
+        {
+          request: { query },
+          result: { data: expectedDataAfterRefetch },
         }
-
-        return { wrapper, refetch };
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      type Data = typeof expectedData;
+      interface FinalProps {
+        wrapper: { thingy: { thing: boolean } };
+        refetch: () => any;
       }
-    });
 
-    let counter = 0;
-    let done = false;
-    class Container extends React.Component<FinalProps> {
-      componentDidUpdate(nextProps: FinalProps) {
-        expect(this.props.wrapper.thingy).toEqual(
-          expectedData.getThing
-        );
-        if (counter === 1) {
-          expect(this.props.wrapper).toEqual(nextProps.wrapper);
-          done = true;
-        } else {
-          counter++;
-          this.props.refetch();
+      const withData = graphql<{}, Data, {}, FinalProps>(query, {
+        props: ({ data }, lastProps) => {
+          const refetch = data!.refetch!;
+          let wrapper = { thingy: data!.getThing! };
+
+          // If the current thingy is equal to the last thingy,
+          // reuse the wrapper (to preserve referential equality).
+          if (lastProps && lastProps.wrapper.thingy === wrapper.thingy) {
+            wrapper = lastProps!.wrapper!;
+          }
+
+          return { wrapper, refetch };
+        },
+      });
+
+      let counter = 0;
+      let done = false;
+      class Container extends React.Component<FinalProps> {
+        componentDidUpdate(nextProps: FinalProps) {
+          expect(this.props.wrapper.thingy).toEqual(expectedData.getThing);
+          if (counter === 1) {
+            expect(this.props.wrapper).toEqual(nextProps.wrapper);
+            done = true;
+          } else {
+            counter++;
+            this.props.refetch();
+          }
+        }
+        render() {
+          return null;
         }
       }
-      render() {
-        return null;
-      }
-    }
 
-    const ContainerWithData = withData(Container);
+      const ContainerWithData = withData(Container);
 
-    render(
-      <ApolloProvider client={client}>
-        <ContainerWithData />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <ContainerWithData />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/queries/skip.test.tsx b/src/react/hoc/__tests__/queries/skip.test.tsx
--- a/src/react/hoc/__tests__/queries/skip.test.tsx
+++ b/src/react/hoc/__tests__/queries/skip.test.tsx
@@ -1,755 +1,772 @@
-import React from 'react';
-import { render, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { ApolloLink } from '../../../../link/core';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
-
-const IS_REACT_18 = React.version.startsWith('18');
-
-describe('[queries] skip', () => {
-  itAsync('allows you to skip a query without running it', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { ApolloLink } from "../../../../link/core";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
+
+describe("[queries] skip", () => {
+  itAsync(
+    "allows you to skip a query without running it",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const link = mockSingleLink({
-      request: { query },
-      result: { data }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-    interface Props {
-      skip: boolean;
-    }
-
-    let queryExecuted = false;
-    const Container = graphql<Props>(query, {
-      skip: ({ skip }) => skip
-    })(
-      class extends React.Component<ChildProps<Props>> {
-        componentDidUpdate() {
-          queryExecuted = true;
-        }
-        render() {
-          expect(this.props.data).toBeUndefined();
-          return null;
-        }
-      }
-    );
-
-    render(
-      <ApolloProvider client={client}>
-        <Container skip={true} />
-      </ApolloProvider>
-    );
-
-    let done = false;
-    setTimeout(() => {
-      if (!queryExecuted) {
-        done = true;
-        return;
-      }
-      reject(new Error('query ran even though skip present'));
-    }, 25);
-
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
-
-  itAsync('continues to not subscribe to a skipped query when props change', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const link = mockSingleLink({
+        request: { query },
+        result: { data },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+      interface Props {
+        skip: boolean;
+      }
+
+      let queryExecuted = false;
+      const Container = graphql<Props>(query, {
+        skip: ({ skip }) => skip,
+      })(
+        class extends React.Component<ChildProps<Props>> {
+          componentDidUpdate() {
+            queryExecuted = true;
+          }
+          render() {
+            expect(this.props.data).toBeUndefined();
+            return null;
           }
         }
-      }
-    `;
+      );
 
-    const link = new ApolloLink((o, f) => {
-      reject(new Error('query ran even though skip present'));
-      return f ? f(o) : null;
-    }).concat(mockSingleLink());
-    // const oldQuery = link.query;
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    interface Props {
-      foo: number;
-    }
+      render(
+        <ApolloProvider client={client}>
+          <Container skip={true} />
+        </ApolloProvider>
+      );
 
-    let done = false;
-    const Container = graphql<Props>(query, { skip: true })(
-      class extends React.Component<ChildProps<Props>> {
-        componentDidUpdate() {
+      let done = false;
+      setTimeout(() => {
+        if (!queryExecuted) {
           done = true;
+          return;
         }
-        render() {
-          return null;
-        }
-      }
-    );
-
-    class Parent extends React.Component<{}, { foo: number }> {
-      state = { foo: 42 };
+        reject(new Error("query ran even though skip present"));
+      }, 25);
 
-      componentDidMount() {
-        this.setState({ foo: 43 });
-      }
-      render() {
-        return <Container foo={this.state.foo} />;
-      }
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
     }
-
-    render(
-      <ApolloProvider client={client}>
-        <Parent />
-      </ApolloProvider>
-    );
-
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
-
-  itAsync('supports using props for skipping which are used in options', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people($id: ID!) {
-        allPeople(first: $id) {
-          people {
-            id
+  );
+
+  itAsync(
+    "continues to not subscribe to a skipped query when props change",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-
-    const data = {
-      allPeople: { people: { id: 1 } }
-    };
-
-    type Data = typeof data;
+      `;
 
-    const variables = { id: 1 };
-    type Vars = typeof variables;
+      const link = new ApolloLink((o, f) => {
+        reject(new Error("query ran even though skip present"));
+        return f ? f(o) : null;
+      }).concat(mockSingleLink());
+      // const oldQuery = link.query;
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    const link = mockSingleLink({
-      request: { query, variables },
-      result: { data }
-    });
+      interface Props {
+        foo: number;
+      }
 
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      let done = false;
+      const Container = graphql<Props>(query, { skip: true })(
+        class extends React.Component<ChildProps<Props>> {
+          componentDidUpdate() {
+            done = true;
+          }
+          render() {
+            return null;
+          }
+        }
+      );
 
-    let count = 0;
-    let renderCount = 0;
+      class Parent extends React.Component<{}, { foo: number }> {
+        state = { foo: 42 };
 
-    interface Props {
-      person: { id: number } | null;
-    }
-    const Container = graphql<Props, Data, Vars>(query, {
-      skip: ({ person }) => !person,
-      options: ({ person }) => ({
-        variables: {
-          id: person!.id
-        }
-      })
-    })(
-      class extends React.Component<ChildProps<Props, Data, Vars>> {
-        componentDidUpdate() {
-          try {
-            const { props } = this;
-            switch (++count) {
-              case 1:
-                expect(props.data!.loading).toBe(true);
-                break;
-              case 2:
-                expect(props.data!.loading).toBe(false);
-                expect(props.data!.allPeople).toEqual(data.allPeople);
-                expect(renderCount).toBe(3);
-                break;
-              default:
-                reject(`Too many renders (${count})`);
-            }
-          } catch (err) {
-            reject(err);
-          }
+        componentDidMount() {
+          this.setState({ foo: 43 });
         }
         render() {
-          renderCount++;
-          return null;
+          return <Container foo={this.state.foo} />;
         }
       }
-    );
 
-    class Parent extends React.Component<
-      {},
-      { person: { id: number } | null }
-    > {
-      state = { person: null };
+      render(
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
 
-      componentDidMount() {
-        this.setState({ person: { id: 1 } });
-      }
-      render() {
-        return <Container person={this.state.person} />;
-      }
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
     }
-
-    render(
-      <ApolloProvider client={client}>
-        <Parent />
-      </ApolloProvider>
-    );
-
-    waitFor(() => expect(count).toBe(2)).then(resolve, reject);
-  });
-
-  itAsync("doesn't run options or props when skipped, including option.client", (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  );
+
+  itAsync(
+    "supports using props for skipping which are used in options",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people($id: ID!) {
+          allPeople(first: $id) {
+            people {
+              id
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const link = mockSingleLink({
-      request: { query },
-      result: { data }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    let queryExecuted = false;
-    let optionsCalled = false;
-
-    interface Props {
-      skip: boolean;
-      pollInterval?: number;
-    }
+      `;
+
+      const data = {
+        allPeople: { people: { id: 1 } },
+      };
+
+      type Data = typeof data;
+
+      const variables = { id: 1 };
+      type Vars = typeof variables;
+
+      const link = mockSingleLink({
+        request: { query, variables },
+        result: { data },
+      });
+
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      let count = 0;
+      let renderCount = 0;
+
+      interface Props {
+        person: { id: number } | null;
+      }
+      const Container = graphql<Props, Data, Vars>(query, {
+        skip: ({ person }) => !person,
+        options: ({ person }) => ({
+          variables: {
+            id: person!.id,
+          },
+        }),
+      })(
+        class extends React.Component<ChildProps<Props, Data, Vars>> {
+          componentDidUpdate() {
+            try {
+              const { props } = this;
+              switch (++count) {
+                case 1:
+                  expect(props.data!.loading).toBe(true);
+                  break;
+                case 2:
+                  expect(props.data!.loading).toBe(false);
+                  expect(props.data!.allPeople).toEqual(data.allPeople);
+                  expect(renderCount).toBe(3);
+                  break;
+                default:
+                  reject(`Too many renders (${count})`);
+              }
+            } catch (err) {
+              reject(err);
+            }
+          }
+          render() {
+            renderCount++;
+            return null;
+          }
+        }
+      );
 
-    interface FinalProps {
-      pollInterval: number;
-      data?: {};
-    }
+      class Parent extends React.Component<
+        {},
+        { person: { id: number } | null }
+      > {
+        state = { person: null };
 
-    const Container = graphql<Props, {}, {}, FinalProps>(query, {
-      skip: ({ skip }) => skip,
-      options: props => {
-        optionsCalled = true;
-        return {
-          pollInterval: props.pollInterval
-        };
-      },
-      props: props => ({
-        // intentionally incorrect
-        pollInterval: (props as any).willThrowIfAccesed.pollInterval
-      })
-    })(
-      class extends React.Component<FinalProps & Props> {
-        componentDidUpdate() {
-          queryExecuted = true;
+        componentDidMount() {
+          this.setState({ person: { id: 1 } });
         }
         render() {
-          expect(this.props.data).toBeFalsy();
-          return null;
+          return <Container person={this.state.person} />;
         }
       }
-    );
-
-    render(
-      <ApolloProvider client={client}>
-        <Container skip={true} />
-      </ApolloProvider>
-    );
 
-    let done = false;
-    setTimeout(() => {
-      if (!queryExecuted) {
-        done = true;
-        return;
-      }
-      if (optionsCalled) {
-        reject(new Error('options ran even though skip present'));
-        return;
-      }
-      reject(new Error('query ran even though skip present'));
-    }, 25);
-
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
+      render(
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
 
-  itAsync("doesn't run options or props when skipped even if the component updates", (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+      waitFor(() => expect(count).toBe(2)).then(resolve, reject);
+    }
+  );
+
+  itAsync(
+    "doesn't run options or props when skipped, including option.client",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-
-    const link = mockSingleLink({
-      request: { query },
-      result: {}
-    });
-
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    let queryWasSkipped = true;
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const link = mockSingleLink({
+        request: { query },
+        result: { data },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      let queryExecuted = false;
+      let optionsCalled = false;
+
+      interface Props {
+        skip: boolean;
+        pollInterval?: number;
+      }
+
+      interface FinalProps {
+        pollInterval: number;
+        data?: {};
+      }
+
+      const Container = graphql<Props, {}, {}, FinalProps>(query, {
+        skip: ({ skip }) => skip,
+        options: (props) => {
+          optionsCalled = true;
+          return {
+            pollInterval: props.pollInterval,
+          };
+        },
+        props: (props) => ({
+          // intentionally incorrect
+          pollInterval: (props as any).willThrowIfAccesed.pollInterval,
+        }),
+      })(
+        class extends React.Component<FinalProps & Props> {
+          componentDidUpdate() {
+            queryExecuted = true;
+          }
+          render() {
+            expect(this.props.data).toBeFalsy();
+            return null;
+          }
+        }
+      );
 
-    interface Props {
-      foo: string;
-    }
+      render(
+        <ApolloProvider client={client}>
+          <Container skip={true} />
+        </ApolloProvider>
+      );
 
-    let done = false;
-    const Container = graphql<Props>(query, {
-      skip: true,
-      options: () => {
-        queryWasSkipped = false;
-        return {};
-      },
-      props: () => {
-        queryWasSkipped = false;
-        return {};
-      }
-    })(
-      class extends React.Component<ChildProps<Props>> {
-        componentDidUpdate() {
-          expect(queryWasSkipped).toBeTruthy();
+      let done = false;
+      setTimeout(() => {
+        if (!queryExecuted) {
           done = true;
+          return;
         }
-        render() {
-          return null;
+        if (optionsCalled) {
+          reject(new Error("options ran even though skip present"));
+          return;
         }
-      }
-    );
+        reject(new Error("query ran even though skip present"));
+      }, 25);
 
-    class Parent extends React.Component<{}, { foo: string }> {
-      state = { foo: 'bar' };
-      componentDidMount() {
-        this.setState({ foo: 'baz' });
-      }
-      render() {
-        return <Container foo={this.state.foo} />;
-      }
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
     }
-
-    render(
-      <ApolloProvider client={client}>
-        <Parent />
-      </ApolloProvider>
-    );
-
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
-
-  itAsync('allows you to skip a query without running it (alternate syntax)', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  );
+
+  itAsync(
+    "doesn't run options or props when skipped even if the component updates",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const link = mockSingleLink({
-      request: { query },
-      result: { data }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      `;
 
-    let queryExecuted = false;
-    const Container = graphql(query, { skip: true })(
-      class extends React.Component<ChildProps> {
-        componentDidUpdate() {
-          queryExecuted = true;
-        }
-        render() {
-          expect(this.props.data).toBeFalsy();
-          return null;
-        }
-      }
-    );
+      const link = mockSingleLink({
+        request: { query },
+        result: {},
+      });
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let done = false;
-    setTimeout(() => {
-      if (!queryExecuted) {
-        done = true;
-        return;
-      }
-      reject(new Error('query ran even though skip present'));
-    }, 25);
+      let queryWasSkipped = true;
 
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
+      interface Props {
+        foo: string;
+      }
 
-  // test the case of skip:false -> skip:true -> skip:false to make sure things
-  // are cleaned up properly
-  itAsync('allows you to skip then unskip a query with top-level syntax', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+      let done = false;
+      const Container = graphql<Props>(query, {
+        skip: true,
+        options: () => {
+          queryWasSkipped = false;
+          return {};
+        },
+        props: () => {
+          queryWasSkipped = false;
+          return {};
+        },
+      })(
+        class extends React.Component<ChildProps<Props>> {
+          componentDidUpdate() {
+            expect(queryWasSkipped).toBeTruthy();
+            done = true;
+          }
+          render() {
+            return null;
           }
         }
-      }
-    `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const link = mockSingleLink({
-      request: { query },
-      result: { data }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    let hasSkipped = false;
+      );
 
-    interface Props {
-      skip: boolean;
-      setSkip: (skip: boolean) => void;
-    }
-
-    const Container = graphql<Props>(query, { skip: ({ skip }) => skip })(
-      class extends React.Component<ChildProps<Props>> {
-        componentDidUpdate(prevProps: ChildProps<Props>) {
-          if (this.props.skip) {
-            hasSkipped = true;
-            prevProps.setSkip(false);
-          } else {
-            if (!hasSkipped) {
-              prevProps.setSkip(true);
-            }
-          }
+      class Parent extends React.Component<{}, { foo: string }> {
+        state = { foo: "bar" };
+        componentDidMount() {
+          this.setState({ foo: "baz" });
         }
         render() {
-          return null;
+          return <Container foo={this.state.foo} />;
         }
       }
-    );
 
-    class Parent extends React.Component<any, any> {
-      state = { skip: false };
-      render() {
-        return (
-          <Container
-            skip={this.state.skip}
-            setSkip={skip => this.setState({ skip })}
-          />
-        );
-      }
-    }
-
-    render(
-      <ApolloProvider client={client}>
-        <Parent />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(hasSkipped).toBeTruthy()).then(resolve, reject);
-  });
-
-  itAsync('allows you to skip then unskip a query with new options (top-level syntax)', (resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people($first: Int) {
-        allPeople(first: $first) {
-          people {
-            name
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
+    }
+  );
+
+  itAsync(
+    "allows you to skip a query without running it (alternate syntax)",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const dataOne = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const dataTwo = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const link = mockSingleLink({
+        request: { query },
+        result: { data },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      let queryExecuted = false;
+      const Container = graphql(query, { skip: true })(
+        class extends React.Component<ChildProps> {
+          componentDidUpdate() {
+            queryExecuted = true;
+          }
+          render() {
+            expect(this.props.data).toBeFalsy();
+            return null;
+          }
+        }
+      );
 
-    type Data = typeof dataOne;
-    type Vars = { first: number };
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    const link = mockSingleLink(
-      {
-        request: { query, variables: { first: 1 } },
-        result: { data: dataOne }
-      },
-      {
-        request: { query, variables: { first: 2 } },
-        result: { data: dataTwo }
-      },
-      {
-        request: { query, variables: { first: 2 } },
-        result: { data: dataTwo }
-      }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
-
-    let hasSkipped = false;
+      let done = false;
+      setTimeout(() => {
+        if (!queryExecuted) {
+          done = true;
+          return;
+        }
+        reject(new Error("query ran even though skip present"));
+      }, 25);
 
-    interface Props {
-      skip: boolean;
-      first: number;
-      setState: <K extends 'skip' | 'first'>(
-        state: Pick<{ skip: boolean; first: number }, K>
-      ) => void;
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
     }
+  );
 
-    let done = false;
-    const Container = graphql<Props, Data, Vars>(query, {
-      skip: ({ skip }) => skip
-    })(
-      class extends React.Component<ChildProps<Props, Data, Vars>> {
-        componentDidUpdate(prevProps: ChildProps<Props, Data, Vars>) {
-          if (this.props.skip) {
-            hasSkipped = true;
-            // change back to skip: false, with a different variable
-            prevProps.setState({ skip: false, first: 2 });
-          } else {
-            if (hasSkipped) {
-              if (!this.props.data!.loading) {
-                expect(this.props.data!.allPeople).toEqual(
-                  dataTwo.allPeople
-                );
-                done = true;
-              }
+  // test the case of skip:false -> skip:true -> skip:false to make sure things
+  // are cleaned up properly
+  itAsync(
+    "allows you to skip then unskip a query with top-level syntax",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
+          }
+        }
+      `;
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const link = mockSingleLink({
+        request: { query },
+        result: { data },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      let hasSkipped = false;
+
+      interface Props {
+        skip: boolean;
+        setSkip: (skip: boolean) => void;
+      }
+
+      const Container = graphql<Props>(query, { skip: ({ skip }) => skip })(
+        class extends React.Component<ChildProps<Props>> {
+          componentDidUpdate(prevProps: ChildProps<Props>) {
+            if (this.props.skip) {
+              hasSkipped = true;
+              prevProps.setSkip(false);
             } else {
-              expect(this.props.data!.allPeople).toEqual(
-                dataOne.allPeople
-              );
-              prevProps.setState({ skip: true });
+              if (!hasSkipped) {
+                prevProps.setSkip(true);
+              }
             }
           }
+          render() {
+            return null;
+          }
         }
+      );
+
+      class Parent extends React.Component<any, any> {
+        state = { skip: false };
         render() {
-          return null;
+          return (
+            <Container
+              skip={this.state.skip}
+              setSkip={(skip) => this.setState({ skip })}
+            />
+          );
         }
       }
-    );
 
-    class Parent extends React.Component<{}, { skip: boolean; first: number }> {
-      state = { skip: false, first: 1 };
-      render() {
-        return (
-          <Container
-            skip={this.state.skip}
-            first={this.state.first}
-            setState={state => this.setState(state)}
-          />
-        );
-      }
-    }
-
-    render(
-      <ApolloProvider client={client}>
-        <Parent />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
 
-    waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
-  });
-
-  it('allows you to skip then unskip a query with opts syntax', () => new Promise((resolve, reject) => {
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+      waitFor(() => expect(hasSkipped).toBeTruthy()).then(resolve, reject);
+    }
+  );
+
+  itAsync(
+    "allows you to skip then unskip a query with new options (top-level syntax)",
+    (resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people($first: Int) {
+          allPeople(first: $first) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    const nextData = { allPeople: { people: [{ name: 'Anakin Skywalker' }] } };
-    const finalData = { allPeople: { people: [{ name: 'Darth Vader' }] } };
+      `;
+      const dataOne = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const dataTwo = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
 
-    let ranQuery = 0;
+      type Data = typeof dataOne;
+      type Vars = { first: number };
 
-    const link = new ApolloLink((o, f) => {
-      ranQuery++;
-      return f ? f(o) : null;
-    }).concat(
-      mockSingleLink(
+      const link = mockSingleLink(
         {
-          request: { query },
-          result: { data },
+          request: { query, variables: { first: 1 } },
+          result: { data: dataOne },
         },
         {
-          request: { query },
-          result: { data: nextData },
+          request: { query, variables: { first: 2 } },
+          result: { data: dataTwo },
         },
         {
-          request: { query },
-          result: { data: finalData },
-        },
-      )
-    );
-
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false }),
-      queryDeduplication: false
-    });
+          request: { query, variables: { first: 2 } },
+          result: { data: dataTwo },
+        }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
+
+      let hasSkipped = false;
+
+      interface Props {
+        skip: boolean;
+        first: number;
+        setState: <K extends "skip" | "first">(
+          state: Pick<{ skip: boolean; first: number }, K>
+        ) => void;
+      }
+
+      let done = false;
+      const Container = graphql<Props, Data, Vars>(query, {
+        skip: ({ skip }) => skip,
+      })(
+        class extends React.Component<ChildProps<Props, Data, Vars>> {
+          componentDidUpdate(prevProps: ChildProps<Props, Data, Vars>) {
+            if (this.props.skip) {
+              hasSkipped = true;
+              // change back to skip: false, with a different variable
+              prevProps.setState({ skip: false, first: 2 });
+            } else {
+              if (hasSkipped) {
+                if (!this.props.data!.loading) {
+                  expect(this.props.data!.allPeople).toEqual(dataTwo.allPeople);
+                  done = true;
+                }
+              } else {
+                expect(this.props.data!.allPeople).toEqual(dataOne.allPeople);
+                prevProps.setState({ skip: true });
+              }
+            }
+          }
+          render() {
+            return null;
+          }
+        }
+      );
 
-    let count = 0;
-    const Container = graphql<any>(query, {
-      options: {
-        fetchPolicy: 'network-only',
-        nextFetchPolicy: 'cache-first',
-        notifyOnNetworkStatusChange: true
-      },
-      skip: ({ skip }) => skip
-    })(
-      class extends React.Component<any> {
+      class Parent extends React.Component<
+        {},
+        { skip: boolean; first: number }
+      > {
+        state = { skip: false, first: 1 };
         render() {
-          expect(this.props.data?.error).toBeUndefined();
-
-          try {
-            switch (++count) {
-              case 1:
-                expect(this.props.data.loading).toBe(true);
-                expect(ranQuery).toBe(0);
-                break;
-              case 2:
-                // The first batch of data is fetched over the network, and
-                // verified here, followed by telling the component we want to
-                // skip running subsequent queries.
-                expect(this.props.data.loading).toBe(false);
-                expect(this.props.data.allPeople).toEqual(data.allPeople);
-                expect(ranQuery).toBe(1);
-                setTimeout(() => {
-                  this.props.setSkip(true);
-                }, 10);
-                break;
-              case 3:
-                // This render is triggered after setting skip to true. Now
-                // let's set skip to false to re-trigger the query.
-                setTimeout(() => {
-                  this.props.setSkip(false);
-                }, 10);
-                expect(this.props.skip).toBe(true);
-                expect(this.props.data).toBeUndefined();
-                expect(ranQuery).toBe(1);
-                break;
-              case 4:
-                expect(this.props.skip).toBe(false);
-                expect(this.props.data!.loading).toBe(false);
-                expect(this.props.data.allPeople).toEqual(data.allPeople);
-                expect(ranQuery).toBe(2);
-                break;
-              case 5:
-                expect(this.props.skip).toBe(false);
-                expect(this.props.data!.loading).toBe(false);
-                expect(this.props.data.allPeople).toEqual(nextData.allPeople);
-                expect(ranQuery).toBe(2);
-                // Since the `nextFetchPolicy` was set to `cache-first`, our
-                // query isn't loading as it's able to find the result of the
-                // query directly from the cache. Let's trigger a refetch
-                // to manually load the next batch of data.
-                setTimeout(() => {
-                  this.props.data.refetch();
-                }, 10);
-                break;
-              case 6:
-                expect(this.props.skip).toBe(false);
-                if (IS_REACT_18) {
+          return (
+            <Container
+              skip={this.state.skip}
+              first={this.state.first}
+              setState={(state) => this.setState(state)}
+            />
+          );
+        }
+      }
+
+      render(
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+
+      waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
+    }
+  );
+
+  it("allows you to skip then unskip a query with opts syntax", () =>
+    new Promise((resolve, reject) => {
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
+          }
+        }
+      `;
+
+      const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const nextData = {
+        allPeople: { people: [{ name: "Anakin Skywalker" }] },
+      };
+      const finalData = { allPeople: { people: [{ name: "Darth Vader" }] } };
+
+      let ranQuery = 0;
+
+      const link = new ApolloLink((o, f) => {
+        ranQuery++;
+        return f ? f(o) : null;
+      }).concat(
+        mockSingleLink(
+          {
+            request: { query },
+            result: { data },
+          },
+          {
+            request: { query },
+            result: { data: nextData },
+          },
+          {
+            request: { query },
+            result: { data: finalData },
+          }
+        )
+      );
+
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+        queryDeduplication: false,
+      });
+
+      let count = 0;
+      const Container = graphql<any>(query, {
+        options: {
+          fetchPolicy: "network-only",
+          nextFetchPolicy: "cache-first",
+          notifyOnNetworkStatusChange: true,
+        },
+        skip: ({ skip }) => skip,
+      })(
+        class extends React.Component<any> {
+          render() {
+            expect(this.props.data?.error).toBeUndefined();
+
+            try {
+              switch (++count) {
+                case 1:
+                  expect(this.props.data.loading).toBe(true);
+                  expect(ranQuery).toBe(0);
+                  break;
+                case 2:
+                  // The first batch of data is fetched over the network, and
+                  // verified here, followed by telling the component we want to
+                  // skip running subsequent queries.
+                  expect(this.props.data.loading).toBe(false);
+                  expect(this.props.data.allPeople).toEqual(data.allPeople);
+                  expect(ranQuery).toBe(1);
+                  setTimeout(() => {
+                    this.props.setSkip(true);
+                  }, 10);
+                  break;
+                case 3:
+                  // This render is triggered after setting skip to true. Now
+                  // let's set skip to false to re-trigger the query.
+                  setTimeout(() => {
+                    this.props.setSkip(false);
+                  }, 10);
+                  expect(this.props.skip).toBe(true);
+                  expect(this.props.data).toBeUndefined();
+                  expect(ranQuery).toBe(1);
+                  break;
+                case 4:
+                  expect(this.props.skip).toBe(false);
                   expect(this.props.data!.loading).toBe(false);
-                  expect(this.props.data.allPeople).toEqual(finalData.allPeople);
-                } else {
+                  expect(this.props.data.allPeople).toEqual(data.allPeople);
+                  expect(ranQuery).toBe(2);
+                  break;
+                case 5:
+                  expect(this.props.skip).toBe(false);
+                  expect(this.props.data!.loading).toBe(false);
+                  expect(this.props.data.allPeople).toEqual(nextData.allPeople);
+                  expect(ranQuery).toBe(2);
+                  // Since the `nextFetchPolicy` was set to `cache-first`, our
+                  // query isn't loading as it's able to find the result of the
+                  // query directly from the cache. Let's trigger a refetch
+                  // to manually load the next batch of data.
+                  setTimeout(() => {
+                    this.props.data.refetch();
+                  }, 10);
+                  break;
+                case 6:
+                  expect(this.props.skip).toBe(false);
                   expect(ranQuery).toBe(3);
                   expect(this.props.data.allPeople).toEqual(nextData.allPeople);
                   expect(this.props.data!.loading).toBe(true);
-                }
-                break;
-              case 7:
-                // The next batch of data has loaded.
-                expect(this.props.skip).toBe(false);
-                expect(this.props.data!.loading).toBe(false);
-                expect(this.props.data.allPeople).toEqual(finalData.allPeople);
-                expect(ranQuery).toBe(3);
-                break;
-              default:
-                throw new Error(`too many renders (${count})`);
+                  break;
+                case 7:
+                  // The next batch of data has loaded.
+                  expect(this.props.skip).toBe(false);
+                  expect(this.props.data!.loading).toBe(false);
+                  expect(this.props.data.allPeople).toEqual(
+                    finalData.allPeople
+                  );
+                  expect(ranQuery).toBe(3);
+                  break;
+                default:
+                  throw new Error(`too many renders (${count})`);
+              }
+            } catch (err) {
+              reject(err);
             }
-          } catch (err) {
-            reject(err);
-          }
 
-          return null;
+            return null;
+          }
         }
-      },
-    );
+      );
 
-    class Parent extends React.Component<{}, { skip: boolean }> {
-      state = { skip: false };
-      render() {
-        return (
-          <Container
-            skip={this.state.skip}
-            setSkip={(skip: boolean) => this.setState({ skip })}
-          />
-        );
+      class Parent extends React.Component<{}, { skip: boolean }> {
+        state = { skip: false };
+        render() {
+          return (
+            <Container
+              skip={this.state.skip}
+              setSkip={(skip: boolean) => this.setState({ skip })}
+            />
+          );
+        }
       }
-    }
 
-    render(
-      <ApolloProvider client={client}>
-        <Parent />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      if (IS_REACT_18) {
-        expect(count).toEqual(6)
-      } else {
-        expect(count).toEqual(7)
-      }
-    }).then(resolve, reject);
-  }));
+      waitFor(() => {
+        expect(count).toEqual(7);
+      }).then(resolve, reject);
+    }));
 
   // This test might have value, but is currently broken (the count === 0 test
   // is never hit, for example, because count++ happens the first time before
   // componentDidUpdate is called), so we are skipping it for now.
-  it.skip('removes the injected props if skip becomes true', async () => {
+  it.skip("removes the injected props if skip becomes true", async () => {
     let count = 0;
     const query: DocumentNode = gql`
       query people($first: Int) {
@@ -761,13 +778,13 @@ describe('[queries] skip', () => {
       }
     `;
 
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     const variables1 = { first: 1 };
 
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
+    const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
     const variables2 = { first: 2 };
 
-    const data3 = { allPeople: { people: [{ name: 'Anakin Skywalker' }] } };
+    const data3 = { allPeople: { people: [{ name: "Anakin Skywalker" }] } };
     const variables3 = { first: 3 };
 
     type Data = typeof data1;
@@ -781,18 +798,17 @@ describe('[queries] skip', () => {
 
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const Container = graphql<Vars, Data>(query, {
-      skip: () => count === 1
+      skip: () => count === 1,
     })(
       class extends React.Component<ChildProps<Vars, Data>> {
         componentDidUpdate() {
           const { data } = this.props;
           // loading is true, but data still there
-          if (count === 0)
-            expect(data!.allPeople).toEqual(data1.allPeople);
+          if (count === 0) expect(data!.allPeople).toEqual(data1.allPeople);
           if (count === 1) expect(data).toBeUndefined();
           if (count === 2 && !data!.loading) {
             expect(data!.allPeople).toEqual(data3.allPeople);
@@ -834,7 +850,7 @@ describe('[queries] skip', () => {
     });
   });
 
-  itAsync('allows you to unmount a skipped query', (resolve, reject) => {
+  itAsync("allows you to unmount a skipped query", (resolve, reject) => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -847,7 +863,7 @@ describe('[queries] skip', () => {
     const link = mockSingleLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Props {
@@ -856,7 +872,7 @@ describe('[queries] skip', () => {
 
     let done = false;
     const Container = graphql<Props>(query, {
-      skip: true
+      skip: true,
     })(
       class extends React.Component<ChildProps<Props>> {
         componentDidMount() {
diff --git a/src/react/hoc/__tests__/queries/updateQuery.test.tsx b/src/react/hoc/__tests__/queries/updateQuery.test.tsx
--- a/src/react/hoc/__tests__/queries/updateQuery.test.tsx
+++ b/src/react/hoc/__tests__/queries/updateQuery.test.tsx
@@ -1,18 +1,18 @@
-import React from 'react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-import { render, waitFor } from '@testing-library/react';
+import React from "react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+import { render, waitFor } from "@testing-library/react";
 
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
 
-describe('[queries] updateQuery', () => {
+describe("[queries] updateQuery", () => {
   // updateQuery
-  itAsync('exposes updateQuery as part of the props api', (resolve, reject) => {
+  itAsync("exposes updateQuery as part of the props api", (resolve, reject) => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -24,11 +24,11 @@ describe('[queries] updateQuery', () => {
     `;
     const link = mockSingleLink({
       request: { query },
-      result: { data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } } }
+      result: { data: { allPeople: { people: [{ name: "Luke Skywalker" }] } } },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let done = false;
@@ -39,7 +39,9 @@ describe('[queries] updateQuery', () => {
           expect(data!.updateQuery).toBeTruthy();
           expect(data!.updateQuery instanceof Function).toBeTruthy();
           try {
-            data!.updateQuery(() => { done = true; });
+            data!.updateQuery(() => {
+              done = true;
+            });
           } catch (error) {
             reject(error);
           }
@@ -59,213 +61,229 @@ describe('[queries] updateQuery', () => {
     waitFor(() => expect(done).toBeTruthy()).then(resolve, reject);
   });
 
-  itAsync('exposes updateQuery as part of the props api during componentWillMount', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "exposes updateQuery as part of the props api during componentWillMount",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const link = mockSingleLink({
-      request: { query },
-      result: { data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } } }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: {
+          data: { allPeople: { people: [{ name: "Luke Skywalker" }] } },
+        },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    const Container = graphql(query)(
-      class extends React.Component<ChildProps> {
-        render() {
-          expect(this.props.data!.updateQuery).toBeTruthy();
-          expect(this.props.data!.updateQuery instanceof Function).toBeTruthy();
-          done = true;
-          return null;
+      const Container = graphql(query)(
+        class extends React.Component<ChildProps> {
+          render() {
+            expect(this.props.data!.updateQuery).toBeTruthy();
+            expect(
+              this.props.data!.updateQuery instanceof Function
+            ).toBeTruthy();
+            done = true;
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true)
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('updateQuery throws if called before data has returned', (resolve, reject) => {
-    let renderCount = 0;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "updateQuery throws if called before data has returned",
+    (resolve, reject) => {
+      let renderCount = 0;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const link = mockSingleLink({
-      request: { query },
-      result: { data: { allPeople: { people: [{ name: 'Luke Skywalker' }] } } }
-    });
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: {
+          data: { allPeople: { people: [{ name: "Luke Skywalker" }] } },
+        },
+      });
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    const Container = graphql(query)(
-      class extends React.Component<ChildProps> {
-        render() {
-          expect(this.props.data!.updateQuery).toBeTruthy();
-          expect(this.props.data!.updateQuery instanceof Function).toBeTruthy();
-          try {
-            this.props.data!.updateQuery(p => p);
-          } catch (e) {
-            // TODO: branch never hit in test
-            expect(e.toString()).toMatch(
-              /ObservableQuery with this id doesn't exist:/
-            );
-          }
-          renderCount += 1;
+      const Container = graphql(query)(
+        class extends React.Component<ChildProps> {
+          render() {
+            expect(this.props.data!.updateQuery).toBeTruthy();
+            expect(
+              this.props.data!.updateQuery instanceof Function
+            ).toBeTruthy();
+            try {
+              this.props.data!.updateQuery((p) => p);
+            } catch (e) {
+              // TODO: branch never hit in test
+              expect(e.toString()).toMatch(
+                /ObservableQuery with this id doesn't exist:/
+              );
+            }
+            renderCount += 1;
 
-          return null;
+            return null;
+          }
         }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(renderCount).toBe(2)
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(renderCount).toBe(2);
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('allows updating query results after query has finished (early binding)', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "allows updating query results after query has finished (early binding)",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    type Data = typeof data1;
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
-    const link = mockSingleLink(
-      { request: { query }, result: { data: data1 } },
-      { request: { query }, result: { data: data2 } }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      `;
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      type Data = typeof data1;
+      const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
+      const link = mockSingleLink(
+        { request: { query }, result: { data: data1 } },
+        { request: { query }, result: { data: data2 } }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let isUpdated = false;
-    const Container = graphql<{}, Data>(query)(
-      class extends React.Component<ChildProps<{}, Data>> {
-        public updateQuery: any;
-        componentDidUpdate() {
-          if (isUpdated) {
-            expect(this.props.data!.allPeople).toEqual(
-              data2.allPeople
-            );
-            done = true;
-            return;
-          } else {
-            isUpdated = true;
-            this.updateQuery(() => {
-              return data2;
-            });
+      let isUpdated = false;
+      const Container = graphql<{}, Data>(query)(
+        class extends React.Component<ChildProps<{}, Data>> {
+          public updateQuery: any;
+          componentDidUpdate() {
+            if (isUpdated) {
+              expect(this.props.data!.allPeople).toEqual(data2.allPeople);
+              done = true;
+              return;
+            } else {
+              isUpdated = true;
+              this.updateQuery(() => {
+                return data2;
+              });
+            }
+          }
+          render() {
+            this.updateQuery = this.props.data!.updateQuery;
+            return null;
           }
         }
-        render() {
-          this.updateQuery = this.props.data!.updateQuery;
-          return null;
-        }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true);
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
 
-  itAsync('allows updating query results after query has finished', (resolve, reject) => {
-    let done = false;
-    const query: DocumentNode = gql`
-      query people {
-        allPeople(first: 1) {
-          people {
-            name
+  itAsync(
+    "allows updating query results after query has finished",
+    (resolve, reject) => {
+      let done = false;
+      const query: DocumentNode = gql`
+        query people {
+          allPeople(first: 1) {
+            people {
+              name
+            }
           }
         }
-      }
-    `;
-    const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-    type Data = typeof data1;
+      `;
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      type Data = typeof data1;
 
-    const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
-    const link = mockSingleLink(
-      { request: { query }, result: { data: data1 } },
-      { request: { query }, result: { data: data2 } }
-    );
-    const client = new ApolloClient({
-      link,
-      cache: new Cache({ addTypename: false })
-    });
+      const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } };
+      const link = mockSingleLink(
+        { request: { query }, result: { data: data1 } },
+        { request: { query }, result: { data: data2 } }
+      );
+      const client = new ApolloClient({
+        link,
+        cache: new Cache({ addTypename: false }),
+      });
 
-    let isUpdated = false;
-    const Container = graphql<{}, Data>(query)(
-      class extends React.Component<ChildProps<{}, Data>> {
-        componentDidUpdate() {
-          if (isUpdated) {
-            expect(this.props.data!.allPeople).toEqual(
-              data2.allPeople
-            );
-            done = true
-            return;
-          } else {
-            isUpdated = true;
-            this.props.data!.updateQuery(() => {
-              return data2;
-            });
+      let isUpdated = false;
+      const Container = graphql<{}, Data>(query)(
+        class extends React.Component<ChildProps<{}, Data>> {
+          componentDidUpdate() {
+            if (isUpdated) {
+              expect(this.props.data!.allPeople).toEqual(data2.allPeople);
+              done = true;
+              return;
+            } else {
+              isUpdated = true;
+              this.props.data!.updateQuery(() => {
+                return data2;
+              });
+            }
+          }
+          render() {
+            return null;
           }
         }
-        render() {
-          return null;
-        }
-      }
-    );
+      );
 
-    render(
-      <ApolloProvider client={client}>
-        <Container />
-      </ApolloProvider>
-    );
+      render(
+        <ApolloProvider client={client}>
+          <Container />
+        </ApolloProvider>
+      );
 
-    waitFor(() => {
-      expect(done).toBe(true)
-    }).then(resolve, reject);
-  });
+      waitFor(() => {
+        expect(done).toBe(true);
+      }).then(resolve, reject);
+    }
+  );
 });
diff --git a/src/react/hoc/__tests__/shared-operations.test.tsx b/src/react/hoc/__tests__/shared-operations.test.tsx
--- a/src/react/hoc/__tests__/shared-operations.test.tsx
+++ b/src/react/hoc/__tests__/shared-operations.test.tsx
@@ -1,41 +1,41 @@
-import React from 'react';
-import { render, cleanup } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../core';
-import { ApolloProvider } from '../../context';
-import { InMemoryCache as Cache } from '../../../cache';
-import { ApolloLink } from '../../../link/core';
-import { itAsync, mockSingleLink } from '../../../testing';
-import { graphql } from '../graphql';
-import { ChildProps, DataValue } from '../types';
-import { withApollo } from '../withApollo';
+import React from "react";
+import { render, cleanup } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../core";
+import { ApolloProvider } from "../../context";
+import { InMemoryCache as Cache } from "../../../cache";
+import { ApolloLink } from "../../../link/core";
+import { itAsync, mockSingleLink } from "../../../testing";
+import { graphql } from "../graphql";
+import { ChildProps, DataValue } from "../types";
+import { withApollo } from "../withApollo";
 
 function compose(...funcs: Function[]) {
   const functions = funcs.reverse();
-  return function(...args: any[]) {
+  return function (...args: any[]) {
     const [firstFunction, ...restFunctions] = functions;
     let result = firstFunction.apply(null, args);
-    restFunctions.forEach(fnc => {
+    restFunctions.forEach((fnc) => {
       result = fnc.call(null, result);
     });
     return result;
   };
 }
 
-describe('shared operations', () => {
+describe("shared operations", () => {
   afterEach(cleanup);
 
-  describe('withApollo', () => {
-    it('passes apollo-client to props', () => {
+  describe("withApollo", () => {
+    it("passes apollo-client to props", () => {
       const client = new ApolloClient({
         link: new ApolloLink((o, f) => (f ? f(o) : null)),
-        cache: new Cache()
+        cache: new Cache(),
       });
 
       @withApollo
-      class ContainerWithData extends React.Component<any > {
+      class ContainerWithData extends React.Component<any> {
         render(): React.ReactNode {
           expect(this.props.client).toEqual(client);
           return null;
@@ -50,7 +50,7 @@ describe('shared operations', () => {
     });
   });
 
-  it('binds two queries to props', () => {
+  it("binds two queries to props", () => {
     const peopleQuery: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -60,7 +60,7 @@ describe('shared operations', () => {
         }
       }
     `;
-    const peopleData = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const peopleData = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     interface PeopleData {
       allPeople: { people: [{ name: string }] };
     }
@@ -74,7 +74,7 @@ describe('shared operations', () => {
         }
       }
     `;
-    const shipsData = { allships: { ships: [{ name: 'Tie Fighter' }] } };
+    const shipsData = { allships: { ships: [{ name: "Tie Fighter" }] } };
     interface ShipsData {
       allShips: { ships: [{ name: string }] };
     }
@@ -85,7 +85,7 @@ describe('shared operations', () => {
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface PeopleChildProps {
@@ -97,7 +97,7 @@ describe('shared operations', () => {
     const withPeople: any = graphql<{}, PeopleData, {}, PeopleChildProps>(
       peopleQuery,
       {
-        name: 'people'
+        name: "people",
       }
     );
 
@@ -107,7 +107,7 @@ describe('shared operations', () => {
     const withShips: any = graphql<{}, ShipsData, {}, ShipsChildProps>(
       shipsQuery,
       {
-        name: 'ships'
+        name: "ships",
       }
     );
 
@@ -134,7 +134,7 @@ describe('shared operations', () => {
     unmount();
   });
 
-  it('binds two queries to props with different syntax', () => {
+  it("binds two queries to props with different syntax", () => {
     const peopleQuery: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -144,7 +144,7 @@ describe('shared operations', () => {
         }
       }
     `;
-    const peopleData = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const peopleData = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     interface PeopleData {
       allPeople: { people: [{ name: string }] };
     }
@@ -157,7 +157,7 @@ describe('shared operations', () => {
         }
       }
     `;
-    const shipsData = { allships: { ships: [{ name: 'Tie Fighter' }] } };
+    const shipsData = { allships: { ships: [{ name: "Tie Fighter" }] } };
     interface ShipsData {
       allShips: { ships: [{ name: string }] };
     }
@@ -168,7 +168,7 @@ describe('shared operations', () => {
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface PeopleChildProps {
@@ -178,7 +178,7 @@ describe('shared operations', () => {
     const withPeople = graphql<{}, PeopleData, {}, PeopleChildProps>(
       peopleQuery,
       {
-        name: 'people'
+        name: "people",
       }
     );
 
@@ -191,7 +191,7 @@ describe('shared operations', () => {
       {},
       ShipsAndPeopleChildProps
     >(shipsQuery, {
-      name: 'ships'
+      name: "ships",
     });
 
     const ContainerWithData = withPeople(
@@ -216,7 +216,7 @@ describe('shared operations', () => {
     unmount();
   });
 
-  it('binds two operations to props', () => {
+  it("binds two operations to props", () => {
     const peopleQuery: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -226,7 +226,7 @@ describe('shared operations', () => {
         }
       }
     `;
-    const peopleData = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const peopleData = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
 
     const peopleMutation: DocumentNode = gql`
       mutation addPerson {
@@ -238,23 +238,23 @@ describe('shared operations', () => {
       }
     `;
     const peopleMutationData = {
-      allPeople: { people: [{ name: 'Leia Skywalker' }] }
+      allPeople: { people: [{ name: "Leia Skywalker" }] },
     };
 
     const link = mockSingleLink(
       { request: { query: peopleQuery }, result: { data: peopleData } },
       {
         request: { query: peopleMutation },
-        result: { data: peopleMutationData }
+        result: { data: peopleMutationData },
       }
     );
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
-    const withPeople = graphql(peopleQuery, { name: 'people' });
-    const withPeopleMutation = graphql(peopleMutation, { name: 'addPerson' });
+    const withPeople = graphql(peopleQuery, { name: "people" });
+    const withPeopleMutation = graphql(peopleMutation, { name: "addPerson" });
 
     const ContainerWithData = withPeople(
       withPeopleMutation(
@@ -281,7 +281,7 @@ describe('shared operations', () => {
     unmount();
   });
 
-  itAsync('allows options to take an object', (resolve, reject) => {
+  itAsync("allows options to take an object", (resolve, reject) => {
     const query: DocumentNode = gql`
       query people {
         allPeople(first: 1) {
@@ -291,16 +291,16 @@ describe('shared operations', () => {
         }
       }
     `;
-    const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
+    const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
     type Data = typeof data;
 
     const link = mockSingleLink({
       request: { query },
-      result: { data }
+      result: { data },
     });
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let queryExecuted = false;
@@ -327,12 +327,12 @@ describe('shared operations', () => {
         resolve();
         return;
       }
-      reject(new Error('query ran even though skip present'));
+      reject(new Error("query ran even though skip present"));
     }, 25);
   });
 
-  describe('compose', () => {
-    it('binds two queries to props with different syntax', () => {
+  describe("compose", () => {
+    it("binds two queries to props with different syntax", () => {
       const peopleQuery: DocumentNode = gql`
         query people {
           allPeople(first: 1) {
@@ -343,7 +343,7 @@ describe('shared operations', () => {
         }
       `;
       const peopleData = {
-        allPeople: { people: [{ name: 'Luke Skywalker' }] }
+        allPeople: { people: [{ name: "Luke Skywalker" }] },
       };
 
       type PeopleData = typeof peopleData;
@@ -357,7 +357,7 @@ describe('shared operations', () => {
           }
         }
       `;
-      const shipsData = { allships: { ships: [{ name: 'Tie Fighter' }] } };
+      const shipsData = { allships: { ships: [{ name: "Tie Fighter" }] } };
 
       type ShipsData = typeof shipsData;
 
@@ -367,7 +367,7 @@ describe('shared operations', () => {
       );
       const client = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface PeopleChildProps {
@@ -381,12 +381,12 @@ describe('shared operations', () => {
 
       const enhanced = compose(
         graphql<{}, PeopleData, {}, PeopleChildProps>(peopleQuery, {
-          name: 'people'
+          name: "people",
         }),
         graphql<PeopleChildProps, ShipsData, {}, ShipsAndPeopleChildProps>(
           shipsQuery,
           {
-            name: 'ships'
+            name: "ships",
           }
         )
       );
diff --git a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx
--- a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx
+++ b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx
@@ -1,21 +1,21 @@
 /** @jest-environment node */
-import React from 'react';
-import PropTypes from 'prop-types';
-import ReactDOM from 'react-dom/server';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { itAsync, mockSingleLink } from '../../../../testing';
-import { Query } from '../../../components';
-import { getDataFromTree, getMarkupFromTree } from '../../../ssr';
-import { graphql } from '../../graphql';
-import { ChildProps, DataValue } from '../../types';
-
-describe('SSR', () => {
-  describe('`getDataFromTree`', () => {
+import React from "react";
+import PropTypes from "prop-types";
+import ReactDOM from "react-dom/server";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { itAsync, mockSingleLink } from "../../../../testing";
+import { Query } from "../../../components";
+import { getDataFromTree, getMarkupFromTree } from "../../../ssr";
+import { graphql } from "../../graphql";
+import { ChildProps, DataValue } from "../../types";
+
+describe("SSR", () => {
+  describe("`getDataFromTree`", () => {
     const consoleWarn = console.warn;
     beforeAll(() => {
       console.warn = () => null;
@@ -25,7 +25,7 @@ describe('SSR', () => {
       console.warn = consoleWarn;
     });
 
-    it('should run through all of the queries that want SSR', async () => {
+    it("should run through all of the queries that want SSR", async () => {
       const query = gql`
         {
           currentUser {
@@ -33,14 +33,14 @@ describe('SSR', () => {
           }
         }
       `;
-      const data1 = { currentUser: { firstName: 'James' } };
+      const data1 = { currentUser: { firstName: "James" } };
       const link = mockSingleLink({
         request: { query },
         result: { data: data1 },
       });
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Props {}
@@ -49,15 +49,15 @@ describe('SSR', () => {
           firstName: string;
         };
       }
-      const WrappedElement = graphql<Props, Data>(
-        query
-      )(({ data }: ChildProps<Props, Data>) => (
-        <div>
-          {!data || data.loading || !data.currentUser
-            ? 'loading'
-            : data.currentUser.firstName}
-        </div>
-      ));
+      const WrappedElement = graphql<Props, Data>(query)(
+        ({ data }: ChildProps<Props, Data>) => (
+          <div>
+            {!data || data.loading || !data.currentUser
+              ? "loading"
+              : data.currentUser.firstName}
+          </div>
+        )
+      );
 
       const app = (
         <ApolloProvider client={apolloClient}>
@@ -65,19 +65,19 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      await getDataFromTree(app).then(markup => {
+      await getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
 
       await getMarkupFromTree({
         tree: app,
-        renderFunction: ReactDOM.renderToString
-      }).then(markup => {
+        renderFunction: ReactDOM.renderToString,
+      }).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    it('should allow network-only fetchPolicy as an option and still render prefetched data', () => {
+    it("should allow network-only fetchPolicy as an option and still render prefetched data", () => {
       const query = gql`
         {
           currentUser {
@@ -87,12 +87,12 @@ describe('SSR', () => {
       `;
       const link = mockSingleLink({
         request: { query },
-        result: { data: { currentUser: { firstName: 'James' } } },
+        result: { data: { currentUser: { firstName: "James" } } },
       });
       const apolloClient = new ApolloClient({
         link,
         cache: new Cache({ addTypename: false }),
-        ssrMode: true
+        ssrMode: true,
       });
 
       interface Props {}
@@ -102,11 +102,11 @@ describe('SSR', () => {
         };
       }
       const WrappedElement = graphql<Props, Data>(query, {
-        options: { fetchPolicy: 'network-only' }
+        options: { fetchPolicy: "network-only" },
       })(({ data }: ChildProps<Props, Data>) => (
         <div>
           {!data || data.loading || !data.currentUser
-            ? 'loading'
+            ? "loading"
             : data.currentUser.firstName}
         </div>
       ));
@@ -117,12 +117,12 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    it('should allow cache-and-network fetchPolicy as an option and still render prefetched data', () => {
+    it("should allow cache-and-network fetchPolicy as an option and still render prefetched data", () => {
       const query = gql`
         {
           currentUser {
@@ -132,11 +132,11 @@ describe('SSR', () => {
       `;
       const link = mockSingleLink({
         request: { query },
-        result: { data: { currentUser: { firstName: 'James' } } }
+        result: { data: { currentUser: { firstName: "James" } } },
       });
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Props {}
@@ -146,10 +146,10 @@ describe('SSR', () => {
         };
       }
       const WrappedElement = graphql<Props, Data>(query, {
-        options: { fetchPolicy: 'cache-and-network' }
+        options: { fetchPolicy: "cache-and-network" },
       })(({ data }: ChildProps<Props, Data>) => (
         <div>
-          {data && data.currentUser ? data.currentUser.firstName : 'loading'}
+          {data && data.currentUser ? data.currentUser.firstName : "loading"}
         </div>
       ));
 
@@ -159,12 +159,12 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    it('should pick up queries deep in the render tree', () => {
+    it("should pick up queries deep in the render tree", () => {
       const query = gql`
         {
           currentUser {
@@ -174,11 +174,11 @@ describe('SSR', () => {
       `;
       const link = mockSingleLink({
         request: { query },
-        result: { data: { currentUser: { firstName: 'James' } } },
+        result: { data: { currentUser: { firstName: "James" } } },
       });
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Props {}
@@ -188,15 +188,15 @@ describe('SSR', () => {
         };
       }
 
-      const WrappedElement = graphql<Props, Data>(
-        query
-      )(({ data }: ChildProps<Props, Data>) => (
-        <div>
-          {!data || data.loading || !data.currentUser
-            ? 'loading'
-            : data.currentUser.firstName}
-        </div>
-      ));
+      const WrappedElement = graphql<Props, Data>(query)(
+        ({ data }: ChildProps<Props, Data>) => (
+          <div>
+            {!data || data.loading || !data.currentUser
+              ? "loading"
+              : data.currentUser.firstName}
+          </div>
+        )
+      );
 
       const Page = () => (
         <div>
@@ -213,12 +213,12 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    it('should handle nested queries that depend on each other', () => {
+    it("should handle nested queries that depend on each other", () => {
       const idQuery: DocumentNode = gql`
         {
           currentUser {
@@ -226,7 +226,7 @@ describe('SSR', () => {
           }
         }
       `;
-      const idData = { currentUser: { id: '1234' } };
+      const idData = { currentUser: { id: "1234" } };
       const userQuery: DocumentNode = gql`
         query getUser($id: String) {
           user(id: $id) {
@@ -234,8 +234,8 @@ describe('SSR', () => {
           }
         }
       `;
-      const variables = { id: '1234' };
-      const userData = { user: { firstName: 'James' } };
+      const variables = { id: "1234" };
+      const userData = { user: { firstName: "James" } };
       const link = mockSingleLink(
         { request: { query: idQuery }, result: { data: idData } },
         {
@@ -245,7 +245,7 @@ describe('SSR', () => {
       );
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Props {}
@@ -275,15 +275,15 @@ describe('SSR', () => {
       >(userQuery, {
         skip: ({ data }) => data!.loading,
         options: ({ data }) => ({
-          variables: { id: data!.currentUser!.id }
-        })
+          variables: { id: data!.currentUser!.id },
+        }),
       });
-      const Component: React.FunctionComponent<React.PropsWithChildren<React.PropsWithChildren<any>>> = ({
-        data
-      }) => (
+      const Component: React.FunctionComponent<
+        React.PropsWithChildren<React.PropsWithChildren<any>>
+      > = ({ data }) => (
         <div>
           {!data || data.loading || !data.user
-            ? 'loading'
+            ? "loading"
             : data.user.firstName}
         </div>
       );
@@ -296,12 +296,12 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    it.skip('should return the first of multiple errors thrown by nested wrapped components', () => {
+    it.skip("should return the first of multiple errors thrown by nested wrapped components", () => {
       const lastNameQuery = gql`
         {
           currentUser {
@@ -328,9 +328,9 @@ describe('SSR', () => {
           result: {
             data: {
               currentUser: {
-                lastName: 'Tester'
+                lastName: "Tester",
               },
-            }
+            },
           },
         },
         {
@@ -338,15 +338,15 @@ describe('SSR', () => {
           result: {
             data: {
               currentUser: {
-                firstName: 'James'
+                firstName: "James",
               },
-            }
+            },
           },
         }
       );
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Props {}
@@ -354,19 +354,19 @@ describe('SSR', () => {
       type WithLastNameProps = ChildProps<Props, LastNameData>;
       const withLastName = graphql<Props, LastNameData>(lastNameQuery);
 
-      const fooError = new Error('foo');
+      const fooError = new Error("foo");
       const BorkedComponent = () => {
         throw fooError;
       };
 
       const WrappedBorkedComponent = withLastName(BorkedComponent);
 
-      const ContainerComponent: React.FunctionComponent<React.PropsWithChildren<React.PropsWithChildren<WithLastNameProps>>> = ({
-        data
-      }) => (
+      const ContainerComponent: React.FunctionComponent<
+        React.PropsWithChildren<React.PropsWithChildren<WithLastNameProps>>
+      > = ({ data }) => (
         <div>
           {!data || data.loading || !data.currentUser
-            ? 'loading'
+            ? "loading"
             : data.currentUser.lastName}
           <WrappedBorkedComponent />
           <WrappedBorkedComponent />
@@ -385,16 +385,16 @@ describe('SSR', () => {
 
       return getDataFromTree(app).then(
         () => {
-          throw new Error('Should have thrown an error');
+          throw new Error("Should have thrown an error");
         },
-        e => {
-          expect(e.toString()).toEqual('Error: foo');
+        (e) => {
+          expect(e.toString()).toEqual("Error: foo");
           expect(e).toBe(fooError);
         }
       );
     });
 
-    it('should handle errors thrown by queries', () => {
+    it("should handle errors thrown by queries", () => {
       const query = gql`
         {
           currentUser {
@@ -404,11 +404,11 @@ describe('SSR', () => {
       `;
       const link = mockSingleLink({
         request: { query },
-        error: new Error('Failed to fetch'),
+        error: new Error("Failed to fetch"),
       });
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Props {}
@@ -417,11 +417,13 @@ describe('SSR', () => {
           firstName: string;
         };
       }
-      const WrappedElement = graphql<Props, Data>(
-        query
-      )(({ data }: ChildProps<Props, Data>) => (
-        <div>{!data || data.loading ? 'loading' : data.error?.toString()}</div>
-      ));
+      const WrappedElement = graphql<Props, Data>(query)(
+        ({ data }: ChildProps<Props, Data>) => (
+          <div>
+            {!data || data.loading ? "loading" : data.error?.toString()}
+          </div>
+        )
+      );
 
       const Page = () => (
         <div>
@@ -438,13 +440,13 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).catch(e => {
+      return getDataFromTree(app).catch((e) => {
         expect(e).toBeTruthy();
         expect(e.toString()).toMatch(/Failed to fetch/);
       });
     });
 
-    it('should correctly skip queries (deprecated)', () => {
+    it("should correctly skip queries (deprecated)", () => {
       const query = gql`
         {
           currentUser {
@@ -454,11 +456,11 @@ describe('SSR', () => {
       `;
       const link = mockSingleLink({
         request: { query },
-        result: { data: { currentUser: { firstName: 'James' } } },
+        result: { data: { currentUser: { firstName: "James" } } },
       });
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Props {}
@@ -468,9 +470,9 @@ describe('SSR', () => {
         };
       }
       const WrappedElement = graphql<Props, Data>(query, {
-        skip: true
+        skip: true,
       })(({ data }: ChildProps<Props, Data>) => (
-        <div>{!data ? 'skipped' : 'dang'}</div>
+        <div>{!data ? "skipped" : "dang"}</div>
       ));
 
       const app = (
@@ -479,12 +481,12 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/skipped/);
       });
     });
 
-    it('should use the correct default props for a query', () => {
+    it("should use the correct default props for a query", () => {
       const query = gql`
         query user($id: ID) {
           currentUser(id: $id) {
@@ -492,8 +494,8 @@ describe('SSR', () => {
           }
         }
       `;
-      const resultData = { currentUser: { firstName: 'James' } };
-      const variables = { id: '1' };
+      const resultData = { currentUser: { firstName: "James" } };
+      const variables = { id: "1" };
       const link = mockSingleLink({
         request: { query, variables },
         result: { data: resultData },
@@ -502,7 +504,7 @@ describe('SSR', () => {
       const cache = new Cache({ addTypename: false });
       const apolloClient = new ApolloClient({
         link,
-        cache
+        cache,
       });
 
       interface Props {
@@ -516,19 +518,19 @@ describe('SSR', () => {
       interface Variables {
         id: string;
       }
-      const Element = graphql<Props, Data, Variables>(
-        query,
-      )(({ data }: ChildProps<Props, Data, Variables>) => (
-        <div>
-          {!data || data.loading || !data.currentUser
-            ? 'loading'
-            : data.currentUser.firstName}
-        </div>
-      ));
+      const Element = graphql<Props, Data, Variables>(query)(
+        ({ data }: ChildProps<Props, Data, Variables>) => (
+          <div>
+            {!data || data.loading || !data.currentUser
+              ? "loading"
+              : data.currentUser.firstName}
+          </div>
+        )
+      );
 
       const app = (
         <ApolloProvider client={apolloClient}>
-          <Element id={'1'} />
+          <Element id={"1"} />
         </ApolloProvider>
       );
 
@@ -541,85 +543,88 @@ describe('SSR', () => {
       });
     });
 
-    itAsync('should allow for setting state in a component', (resolve, reject) => {
-      const query = gql`
-        query user($id: ID) {
-          currentUser(id: $id) {
-            firstName
+    itAsync(
+      "should allow for setting state in a component",
+      (resolve, reject) => {
+        const query = gql`
+          query user($id: ID) {
+            currentUser(id: $id) {
+              firstName
+            }
           }
+        `;
+        const resultData = { currentUser: { firstName: "James" } };
+        const variables = { id: "1" };
+        const link = mockSingleLink({
+          request: { query, variables },
+          result: { data: resultData },
+        });
+
+        const cache = new Cache({ addTypename: false });
+        const apolloClient = new ApolloClient({
+          link,
+          cache,
+        });
+
+        interface Props {
+          id: string;
         }
-      `;
-      const resultData = { currentUser: { firstName: 'James' } };
-      const variables = { id: '1' };
-      const link = mockSingleLink({
-        request: { query, variables },
-        result: { data: resultData },
-      });
-
-      const cache = new Cache({ addTypename: false });
-      const apolloClient = new ApolloClient({
-        link,
-        cache
-      });
-
-      interface Props {
-        id: string;
-      }
-      interface Data {
-        currentUser: {
-          firstName: string;
-        };
-      }
-      interface Variables {
-        id: string;
-      }
-
-      class Element extends React.Component<
-        ChildProps<Props, Data, Variables>,
-        { thing: number }
-      > {
-        state = { thing: 1 };
-
-        static getDerivedStateFromProps() {
-          return {
-            thing: 2
+        interface Data {
+          currentUser: {
+            firstName: string;
           };
         }
-
-        render() {
-          const { data } = this.props;
-          expect(this.state.thing).toBe(2);
-          return (
-            <div>
-              {!data || data.loading || !data.currentUser
-                ? 'loading'
-                : data.currentUser.firstName}
-            </div>
-          );
+        interface Variables {
+          id: string;
         }
-      }
 
-      const ElementWithData = graphql<Props, Data, Variables>(query)(Element);
+        class Element extends React.Component<
+          ChildProps<Props, Data, Variables>,
+          { thing: number }
+        > {
+          state = { thing: 1 };
 
-      const app = (
-        <ApolloProvider client={apolloClient}>
-          <ElementWithData id={'1'} />
-        </ApolloProvider>
-      );
+          static getDerivedStateFromProps() {
+            return {
+              thing: 2,
+            };
+          }
 
-      getDataFromTree(app)
-        .then(() => {
-          const initialState = cache.extract();
-          expect(initialState).toBeTruthy();
-          expect(
-            initialState.ROOT_QUERY!['currentUser({"id":"1"})']
-          ).toBeTruthy();
-          resolve();
-        })
-        .catch(console.error);
-    });
+          render() {
+            const { data } = this.props;
+            expect(this.state.thing).toBe(2);
+            return (
+              <div>
+                {!data || data.loading || !data.currentUser
+                  ? "loading"
+                  : data.currentUser.firstName}
+              </div>
+            );
+          }
+        }
+
+        const ElementWithData = graphql<Props, Data, Variables>(query)(Element);
+
+        const app = (
+          <ApolloProvider client={apolloClient}>
+            <ElementWithData id={"1"} />
+          </ApolloProvider>
+        );
+
+        getDataFromTree(app)
+          .then(() => {
+            const initialState = cache.extract();
+            expect(initialState).toBeTruthy();
+            expect(
+              initialState.ROOT_QUERY!['currentUser({"id":"1"})']
+            ).toBeTruthy();
+            resolve();
+          })
+          .catch(console.error);
+      }
+    );
 
-    it('should correctly initialize an empty state to null', () => {
+    it("should correctly initialize an empty state to null", () => {
       class Element extends React.Component<any, any> {
         render() {
           expect(this.state).toBeNull();
@@ -630,15 +635,15 @@ describe('SSR', () => {
       return getDataFromTree(<Element />);
     });
 
-    it('should maintain any state set in the element constructor', () => {
+    it("should maintain any state set in the element constructor", () => {
       class Element extends React.Component<{}, { foo: string }> {
         constructor(props: {}) {
           super(props);
-          this.state = { foo: 'bar' };
+          this.state = { foo: "bar" };
         }
 
         render() {
-          expect(this.state).toEqual({ foo: 'bar' });
+          expect(this.state).toEqual({ foo: "bar" });
           return null;
         }
       }
@@ -646,7 +651,7 @@ describe('SSR', () => {
       return getDataFromTree(<Element />);
     });
 
-    itAsync('should allow prepping state from props', (resolve, reject) => {
+    itAsync("should allow prepping state from props", (resolve, reject) => {
       const query = gql`
         query user($id: ID) {
           currentUser(id: $id) {
@@ -654,8 +659,8 @@ describe('SSR', () => {
           }
         }
       `;
-      const resultData = { currentUser: { firstName: 'James' } };
-      const variables = { id: '1' };
+      const resultData = { currentUser: { firstName: "James" } };
+      const variables = { id: "1" };
       const link = mockSingleLink({
         request: { query, variables },
         result: { data: resultData },
@@ -663,8 +668,8 @@ describe('SSR', () => {
       const apolloClient = new ApolloClient({
         link,
         cache: new Cache({
-          addTypename: false
-        })
+          addTypename: false,
+        }),
       });
       interface Props {
         id: string;
@@ -691,14 +696,14 @@ describe('SSR', () => {
         state: State = {
           thing: 1,
           userId: null,
-          client: null
+          client: null,
         };
 
         static getDerivedStateFromProps(props: Props, state: State) {
           return {
             thing: state.thing + 1,
             userId: props.id,
-            client: apolloClient
+            client: apolloClient,
           };
         }
 
@@ -710,7 +715,7 @@ describe('SSR', () => {
           return (
             <div>
               {!data || data.loading || !data.currentUser
-                ? 'loading'
+                ? "loading"
                 : data.currentUser.firstName}
             </div>
           );
@@ -721,7 +726,7 @@ describe('SSR', () => {
 
       const app = (
         <ApolloProvider client={apolloClient}>
-          <ElementWithData id={'1'} />
+          <ElementWithData id={"1"} />
         </ApolloProvider>
       );
 
@@ -745,8 +750,8 @@ describe('SSR', () => {
           }
         }
       `;
-      const resultData = { currentUser: { firstName: 'James' } };
-      const variables = { id: '1' };
+      const resultData = { currentUser: { firstName: "James" } };
+      const variables = { id: "1" };
       const link = mockSingleLink({
         request: { query, variables },
         result: { data: resultData },
@@ -755,7 +760,7 @@ describe('SSR', () => {
       const cache = new Cache({ addTypename: false });
       const apolloClient = new ApolloClient({
         link,
-        cache
+        cache,
       });
 
       interface Data {
@@ -777,18 +782,18 @@ describe('SSR', () => {
       }
 
       const Element = graphql<Props, Data, Variables>(query, {
-        options: props => ({ variables: props, ssr: false })
+        options: (props) => ({ variables: props, ssr: false }),
       })(({ data }) => (
         <div>
           {!data || data.loading || !data.currentUser
-            ? 'loading'
+            ? "loading"
             : data.currentUser.firstName}
         </div>
       ));
 
       const app = (
         <ApolloProvider client={apolloClient}>
-          <Element id={'1'} />
+          <Element id={"1"} />
         </ApolloProvider>
       );
 
@@ -807,8 +812,8 @@ describe('SSR', () => {
           }
         }
       `;
-      const resultData = { currentUser: { firstName: 'James' } };
-      const variables = { id: '1' };
+      const resultData = { currentUser: { firstName: "James" } };
+      const variables = { id: "1" };
       const link = mockSingleLink({
         request: { query, variables },
         result: { data: resultData },
@@ -817,7 +822,7 @@ describe('SSR', () => {
       const cache = new Cache({ addTypename: false });
       const apolloClient = new ApolloClient({
         link,
-        cache
+        cache,
       });
 
       interface Data {
@@ -830,7 +835,7 @@ describe('SSR', () => {
         <Query query={query} ssr={false} variables={props}>
           {({ data, loading }: { data: Data; loading: boolean }) => (
             <div>
-              {loading || !data ? 'loading' : data.currentUser!.firstName}
+              {loading || !data ? "loading" : data.currentUser!.firstName}
             </div>
           )}
         </Query>
@@ -838,7 +843,7 @@ describe('SSR', () => {
 
       const app = (
         <ApolloProvider client={apolloClient}>
-          <Element id={'1'} />
+          <Element id={"1"} />
         </ApolloProvider>
       );
 
@@ -849,7 +854,7 @@ describe('SSR', () => {
       });
     });
 
-    it('should correctly handle SSR mutations', () => {
+    it("should correctly handle SSR mutations", () => {
       const query = gql`
         {
           currentUser {
@@ -857,7 +862,7 @@ describe('SSR', () => {
           }
         }
       `;
-      const data1 = { currentUser: { firstName: 'James' } };
+      const data1 = { currentUser: { firstName: "James" } };
 
       const mutation = gql`
         mutation {
@@ -866,7 +871,7 @@ describe('SSR', () => {
           }
         }
       `;
-      const mutationData = { logRoutes: { id: 'foo' } };
+      const mutationData = { logRoutes: { id: "foo" } };
 
       const link = mockSingleLink(
         { request: { query }, result: { data: data1 } },
@@ -877,7 +882,7 @@ describe('SSR', () => {
       );
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       interface Data {
@@ -900,9 +905,9 @@ describe('SSR', () => {
             expect(data!.refetch).toBeTruthy();
             return {
               refetchQuery: data!.refetch,
-              data: data!
+              data: data!,
             };
-          }
+          },
         }
       );
 
@@ -913,23 +918,29 @@ describe('SSR', () => {
         { action: (variables: {}) => Promise<any> }
       >(mutation, {
         props: ({ ownProps, mutate }: any) => {
-          if (ownProps.loading || typeof ownProps.loading === 'undefined')
+          if (ownProps.loading || typeof ownProps.loading === "undefined")
             return { action: () => Promise.resolve() };
           expect(ownProps.refetchQuery).toBeTruthy();
           return {
             action(variables: {}) {
               return mutate!({ variables }).then(() => ownProps.refetchQuery());
-            }
+            },
           };
-        }
+        },
       });
 
-      const Element: React.FunctionComponent<React.PropsWithChildren<React.PropsWithChildren<QueryChildProps & {
-        action: (variables: {}) => Promise<any>;
-      }>>> = ({ data }) => (
+      const Element: React.FunctionComponent<
+        React.PropsWithChildren<
+          React.PropsWithChildren<
+            QueryChildProps & {
+              action: (variables: {}) => Promise<any>;
+            }
+          >
+        >
+      > = ({ data }) => (
         <div>
           {!data || data.loading || !data.currentUser
-            ? 'loading'
+            ? "loading"
             : data.currentUser.firstName}
         </div>
       );
@@ -942,12 +953,12 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    it('should correctly handle SSR mutations, reverse order', () => {
+    it("should correctly handle SSR mutations, reverse order", () => {
       const query = gql`
         {
           currentUser {
@@ -979,16 +990,16 @@ describe('SSR', () => {
       const link = mockSingleLink(
         {
           request: { query },
-          result: { data: { currentUser: { firstName: 'James' } } },
+          result: { data: { currentUser: { firstName: "James" } } },
         },
         {
           request: { query: mutation },
-          result: { data: { logRoutes: { id: 'foo' } } },
+          result: { data: { logRoutes: { id: "foo" } } },
         }
       );
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
       const withMutation = graphql<Props, MutationData>(mutation);
@@ -999,19 +1010,21 @@ describe('SSR', () => {
         props: ({ ownProps, data }) => {
           expect(ownProps.mutate).toBeTruthy();
           return {
-            data
+            data,
           };
-        }
+        },
       });
 
-      const Element: React.FunctionComponent<React.PropsWithChildren<React.PropsWithChildren<ChildProps<
-        ChildProps<Props, MutationData>,
-        QueryData,
-        {}
-      >>>> = ({ data }) => (
+      const Element: React.FunctionComponent<
+        React.PropsWithChildren<
+          React.PropsWithChildren<
+            ChildProps<ChildProps<Props, MutationData>, QueryData, {}>
+          >
+        >
+      > = ({ data }) => (
         <div>
           {!data || data.loading || !data.currentUser
-            ? 'loading'
+            ? "loading"
             : data.currentUser.firstName}
         </div>
       );
@@ -1024,12 +1037,12 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
 
-    it('should not require `ApolloProvider` to be the root component', () => {
+    it("should not require `ApolloProvider` to be the root component", () => {
       const query = gql`
         {
           currentUser {
@@ -1045,27 +1058,30 @@ describe('SSR', () => {
 
       const link = mockSingleLink({
         request: { query },
-        result: { data: { currentUser: { firstName: 'James' } } },
+        result: { data: { currentUser: { firstName: "James" } } },
       });
       const apolloClient = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
 
-      const WrappedElement = graphql<{}, Data>(
-        query
-      )(({ data }: ChildProps<{}, Data>) => (
-        <div>
-          {!data || data.loading || !data.currentUser
-            ? 'loading'
-            : data.currentUser.firstName}
-        </div>
-      ));
+      const WrappedElement = graphql<{}, Data>(query)(
+        ({ data }: ChildProps<{}, Data>) => (
+          <div>
+            {!data || data.loading || !data.currentUser
+              ? "loading"
+              : data.currentUser.firstName}
+          </div>
+        )
+      );
 
-      class MyRootContainer extends React.Component<React.PropsWithChildren, { color: string }> {
+      class MyRootContainer extends React.Component<
+        React.PropsWithChildren,
+        { color: string }
+      > {
         constructor(props: {}) {
           super(props);
-          this.state = { color: 'purple' };
+          this.state = { color: "purple" };
         }
 
         getChildContext() {
@@ -1078,7 +1094,7 @@ describe('SSR', () => {
       }
 
       (MyRootContainer as any).childContextTypes = {
-        color: PropTypes.string
+        color: PropTypes.string,
       };
 
       const app = (
@@ -1089,7 +1105,7 @@ describe('SSR', () => {
         </MyRootContainer>
       );
 
-      return getDataFromTree(app).then(markup => {
+      return getDataFromTree(app).then((markup) => {
         expect(markup).toMatch(/James/);
       });
     });
diff --git a/src/react/hoc/__tests__/ssr/server.test.tsx b/src/react/hoc/__tests__/ssr/server.test.tsx
--- a/src/react/hoc/__tests__/ssr/server.test.tsx
+++ b/src/react/hoc/__tests__/ssr/server.test.tsx
@@ -1,5 +1,5 @@
 /** @jest-environment node */
-import React from 'react';
+import React from "react";
 import {
   print,
   graphql as execute,
@@ -8,134 +8,136 @@ import {
   GraphQLList,
   GraphQLString,
   GraphQLID,
-  DocumentNode
-} from 'graphql';
-import gql from 'graphql-tag';
+  DocumentNode,
+} from "graphql";
+import gql from "graphql-tag";
 
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { ApolloLink } from '../../../../link/core';
-import { Observable } from '../../../../utilities';
-import { renderToStringWithData } from '../../../ssr';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { ApolloLink } from "../../../../link/core";
+import { Observable } from "../../../../utilities";
+import { renderToStringWithData } from "../../../ssr";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
 
-const planetMap = new Map([['Planet:1', { id: 'Planet:1', name: 'Tatooine' }]]);
+const planetMap = new Map([["Planet:1", { id: "Planet:1", name: "Tatooine" }]]);
 
 const shipMap = new Map([
   [
-    'Ship:2',
+    "Ship:2",
     {
-      id: 'Ship:2',
-      name: 'CR90 corvette',
-      films: ['Film:4', 'Film:6', 'Film:3']
-    }
+      id: "Ship:2",
+      name: "CR90 corvette",
+      films: ["Film:4", "Film:6", "Film:3"],
+    },
   ],
   [
-    'Ship:3',
+    "Ship:3",
     {
-      id: 'Ship:3',
-      name: 'Star Destroyer',
-      films: ['Film:4', 'Film:5', 'Film:6']
-    }
-  ]
+      id: "Ship:3",
+      name: "Star Destroyer",
+      films: ["Film:4", "Film:5", "Film:6"],
+    },
+  ],
 ]);
 
 const filmMap = new Map([
-  ['Film:3', { id: 'Film:3', title: 'Revenge of the Sith' }],
-  ['Film:4', { id: 'Film:4', title: 'A New Hope' }],
-  ['Film:5', { id: 'Film:5', title: 'the Empire Strikes Back' }],
-  ['Film:6', { id: 'Film:6', title: 'Return of the Jedi' }]
+  ["Film:3", { id: "Film:3", title: "Revenge of the Sith" }],
+  ["Film:4", { id: "Film:4", title: "A New Hope" }],
+  ["Film:5", { id: "Film:5", title: "the Empire Strikes Back" }],
+  ["Film:6", { id: "Film:6", title: "Return of the Jedi" }],
 ]);
 
 const PlanetType = new GraphQLObjectType({
-  name: 'Planet',
+  name: "Planet",
   fields: {
     id: { type: GraphQLID },
-    name: { type: GraphQLString }
-  }
+    name: { type: GraphQLString },
+  },
 });
 
 const FilmType = new GraphQLObjectType({
-  name: 'Film',
+  name: "Film",
   fields: {
     id: { type: GraphQLID },
-    title: { type: GraphQLString }
-  }
+    title: { type: GraphQLString },
+  },
 });
 
 const ShipType = new GraphQLObjectType({
-  name: 'Ship',
+  name: "Ship",
   fields: {
     id: { type: GraphQLID },
     name: { type: GraphQLString },
     films: {
       type: new GraphQLList(FilmType),
-      resolve: ({ films }) => films.map((id: string) => filmMap.get(id))
-    }
-  }
+      resolve: ({ films }) => films.map((id: string) => filmMap.get(id)),
+    },
+  },
 });
 
 const QueryType = new GraphQLObjectType({
-  name: 'Query',
+  name: "Query",
   fields: {
     allPlanets: {
       type: new GraphQLList(PlanetType),
-      resolve: () => Array.from(planetMap.values())
+      resolve: () => Array.from(planetMap.values()),
     },
     allShips: {
       type: new GraphQLList(ShipType),
-      resolve: () => Array.from(shipMap.values())
+      resolve: () => Array.from(shipMap.values()),
     },
     ship: {
       type: ShipType,
       args: { id: { type: GraphQLID } },
-      resolve: (_, { id }) => shipMap.get(id)
+      resolve: (_, { id }) => shipMap.get(id),
     },
     film: {
       type: FilmType,
       args: { id: { type: GraphQLID } },
-      resolve: (_, { id }) => filmMap.get(id)
-    }
-  }
+      resolve: (_, { id }) => filmMap.get(id),
+    },
+  },
 });
 
 const Schema = new GraphQLSchema({ query: QueryType });
 
-describe('SSR', () => {
-  describe('`renderToStringWithData`', () => {
+describe("SSR", () => {
+  describe("`renderToStringWithData`", () => {
     // XXX break into smaller tests
     // XXX mock all queries
-    it('should work on a non trivial example', function() {
+    it("should work on a non trivial example", function () {
       const apolloClient = new ApolloClient({
-        link: new ApolloLink(config => {
-          return new Observable(observer => {
+        link: new ApolloLink((config) => {
+          return new Observable((observer) => {
             execute({
               schema: Schema,
               source: print(config.query),
               variableValues: config.variables,
-              operationName: config.operationName
+              operationName: config.operationName,
             })
-              .then(result => {
+              .then((result) => {
                 observer.next(result);
                 observer.complete();
               })
-              .catch(e => {
+              .catch((e) => {
                 observer.error(e);
               });
           });
         }),
-        cache: new Cache()
+        cache: new Cache(),
       });
 
-      @graphql(gql`
-        query data($id: ID!) {
-          film(id: $id) {
-            title
+      @graphql(
+        gql`
+          query data($id: ID!) {
+            film(id: $id) {
+              title
+            }
           }
-        }
-      ` as DocumentNode)
+        ` as DocumentNode
+      )
       class Film extends React.Component<any, any> {
         render(): React.ReactNode {
           const { data } = this.props;
@@ -156,16 +158,18 @@ describe('SSR', () => {
         id: string;
       }
 
-      @graphql<ShipVariables, ShipData, ShipVariables>(gql`
-        query data($id: ID!) {
-          ship(id: $id) {
-            name
-            films {
-              id
+      @graphql<ShipVariables, ShipData, ShipVariables>(
+        gql`
+          query data($id: ID!) {
+            ship(id: $id) {
+              name
+              films {
+                id
+              }
             }
           }
-        }
-      ` as DocumentNode)
+        ` as DocumentNode
+      )
       class Starship extends React.Component<
         ChildProps<ShipVariables, ShipData, ShipVariables>
       > {
@@ -193,13 +197,15 @@ describe('SSR', () => {
         allShips: { id: string }[];
       }
 
-      @graphql<{}, AllShipsData>(gql`
-        query data {
-          allShips {
-            id
+      @graphql<{}, AllShipsData>(
+        gql`
+          query data {
+            allShips {
+              id
+            }
           }
-        }
-      ` as DocumentNode)
+        ` as DocumentNode
+      )
       class AllShips extends React.Component<ChildProps<{}, AllShipsData>> {
         render(): React.ReactNode {
           const { data } = this.props;
@@ -222,13 +228,15 @@ describe('SSR', () => {
         allPlanets: { name: string }[];
       }
 
-      @graphql<{}, AllPlanetsData>(gql`
-        query data {
-          allPlanets {
-            name
+      @graphql<{}, AllPlanetsData>(
+        gql`
+          query data {
+            allPlanets {
+              name
+            }
           }
-        }
-      ` as DocumentNode)
+        ` as DocumentNode
+      )
       class AllPlanets extends React.Component<ChildProps<{}, AllPlanetsData>> {
         render(): React.ReactNode {
           const { data } = this.props;
@@ -267,7 +275,7 @@ describe('SSR', () => {
         </ApolloProvider>
       );
 
-      return renderToStringWithData(app).then(markup => {
+      return renderToStringWithData(app).then((markup) => {
         expect(markup).toMatch(/CR90 corvette/);
         expect(markup).toMatch(/Return of the Jedi/);
         expect(markup).toMatch(/A New Hope/);
diff --git a/src/react/hoc/__tests__/statics.test.tsx b/src/react/hoc/__tests__/statics.test.tsx
--- a/src/react/hoc/__tests__/statics.test.tsx
+++ b/src/react/hoc/__tests__/statics.test.tsx
@@ -1,7 +1,7 @@
-import React from 'react';
-import gql from 'graphql-tag';
+import React from "react";
+import gql from "graphql-tag";
 
-import { graphql } from '../graphql';
+import { graphql } from "../graphql";
 
 let sampleOperation = gql`
   {
@@ -11,33 +11,33 @@ let sampleOperation = gql`
   }
 `;
 
-describe('statics', () => {
-  it('should be preserved', () => {
+describe("statics", () => {
+  it("should be preserved", () => {
     const ApolloContainer = graphql(sampleOperation)(
       class extends React.Component<any, any> {
-        static veryStatic = 'such global';
+        static veryStatic = "such global";
       }
     );
 
-    expect((ApolloContainer as any).veryStatic).toBe('such global');
+    expect((ApolloContainer as any).veryStatic).toBe("such global");
   });
 
-  it('exposes a debuggable displayName', () => {
+  it("exposes a debuggable displayName", () => {
     @graphql(sampleOperation)
     class ApolloContainer extends React.Component<any, any> {}
 
     expect((ApolloContainer as any).displayName).toBe(
-      'Apollo(ApolloContainer)'
+      "Apollo(ApolloContainer)"
     );
   });
 
-  it('honors custom display names', () => {
+  it("honors custom display names", () => {
     const ApolloContainer = graphql(sampleOperation)(
       class extends React.Component<any, any> {
-        static displayName = 'Foo';
+        static displayName = "Foo";
       }
     );
 
-    expect((ApolloContainer as any).displayName).toBe('Apollo(Foo)');
+    expect((ApolloContainer as any).displayName).toBe("Apollo(Foo)");
   });
 });
diff --git a/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx b/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx
--- a/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx
+++ b/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx
@@ -1,19 +1,19 @@
-import React from 'react';
-import { act, render } from '@testing-library/react';
-import gql from 'graphql-tag';
-import { DocumentNode } from 'graphql';
-
-import { ApolloClient } from '../../../../core';
-import { ApolloProvider } from '../../../context';
-import { InMemoryCache as Cache } from '../../../../cache';
-import { ApolloLink } from '../../../../link/core';
-import { itAsync, MockSubscriptionLink } from '../../../../testing';
-import { graphql } from '../../graphql';
-import { ChildProps } from '../../types';
+import React from "react";
+import { act, render } from "@testing-library/react";
+import gql from "graphql-tag";
+import { DocumentNode } from "graphql";
+
+import { ApolloClient } from "../../../../core";
+import { ApolloProvider } from "../../../context";
+import { InMemoryCache as Cache } from "../../../../cache";
+import { ApolloLink } from "../../../../link/core";
+import { itAsync, MockSubscriptionLink } from "../../../../testing";
+import { graphql } from "../../graphql";
+import { ChildProps } from "../../types";
 
 const IS_REACT_18 = React.version.startsWith("18");
 
-describe('subscriptions', () => {
+describe("subscriptions", () => {
   let error: typeof console.error;
 
   beforeEach(() => {
@@ -27,16 +27,16 @@ describe('subscriptions', () => {
   });
 
   const results = [
-    'James Baxley',
-    'John Pinkerton',
-    'Sam Claridge',
-    'Ben Coleman'
-  ].map(name => ({
+    "James Baxley",
+    "John Pinkerton",
+    "Sam Claridge",
+    "Ben Coleman",
+  ].map((name) => ({
     result: { data: { user: { name } } },
-    delay: 10
+    delay: 10,
   }));
 
-  it('binds a subscription to props', () => {
+  it("binds a subscription to props", () => {
     const query: DocumentNode = gql`
       subscription UserInfo {
         user {
@@ -47,7 +47,7 @@ describe('subscriptions', () => {
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Props {}
@@ -55,14 +55,14 @@ describe('subscriptions', () => {
       user: { name: string };
     }
 
-    const ContainerWithData = graphql<Props, Data>(query)(
-      ({ data }: ChildProps<Props, Data>) => {
-        expect(data).toBeTruthy();
-        expect(data!.user).toBeFalsy();
-        expect(data!.loading).toBeTruthy();
-        return null;
-      }
-    );
+    const ContainerWithData = graphql<Props, Data>(query)(({
+      data,
+    }: ChildProps<Props, Data>) => {
+      expect(data).toBeTruthy();
+      expect(data!.user).toBeFalsy();
+      expect(data!.loading).toBeTruthy();
+      return null;
+    });
 
     render(
       <ApolloProvider client={client}>
@@ -71,7 +71,7 @@ describe('subscriptions', () => {
     );
   });
 
-  it('includes the variables in the props', () => {
+  it("includes the variables in the props", () => {
     const query: DocumentNode = gql`
       subscription UserInfo($name: String) {
         user(name: $name) {
@@ -79,11 +79,11 @@ describe('subscriptions', () => {
         }
       }
     `;
-    const variables = { name: 'James Baxley' };
+    const variables = { name: "James Baxley" };
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     interface Variables {
@@ -94,22 +94,22 @@ describe('subscriptions', () => {
       user: { name: string };
     }
 
-    const ContainerWithData = graphql<Variables, Data>(query)(
-      ({ data }: ChildProps<Variables, Data>) => {
-        expect(data).toBeTruthy();
-        expect(data!.variables).toEqual(variables);
-        return null;
-      }
-    );
+    const ContainerWithData = graphql<Variables, Data>(query)(({
+      data,
+    }: ChildProps<Variables, Data>) => {
+      expect(data).toBeTruthy();
+      expect(data!.variables).toEqual(variables);
+      return null;
+    });
 
     render(
       <ApolloProvider client={client}>
-        <ContainerWithData name={'James Baxley'} />
+        <ContainerWithData name={"James Baxley"} />
       </ApolloProvider>
     );
   });
 
-  itAsync('does not swallow children errors', (resolve, reject) => {
+  itAsync("does not swallow children errors", (resolve, reject) => {
     const query: DocumentNode = gql`
       subscription UserInfo {
         user {
@@ -120,7 +120,7 @@ describe('subscriptions', () => {
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let bar: any;
@@ -151,7 +151,7 @@ describe('subscriptions', () => {
     );
   });
 
-  itAsync('executes a subscription', (resolve, reject) => {
+  itAsync("executes a subscription", (resolve, reject) => {
     jest.useFakeTimers();
 
     const query: DocumentNode = gql`
@@ -168,7 +168,7 @@ describe('subscriptions', () => {
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
@@ -218,34 +218,34 @@ describe('subscriptions', () => {
 
     act(() => {
       jest.advanceTimersByTime(230);
-    })
+    });
   });
 
-  itAsync('resubscribes to a subscription', (resolve, reject) => {
+  itAsync("resubscribes to a subscription", (resolve, reject) => {
     //we make an extra Hoc which will trigger the inner HoC to resubscribe
     //these are the results for the outer subscription
     const triggerResults = [
-      '0',
-      'trigger resubscribe',
-      '3',
-      '4',
-      '5',
-      '6',
-      '7'
-    ].map(trigger => ({
+      "0",
+      "trigger resubscribe",
+      "3",
+      "4",
+      "5",
+      "6",
+      "7",
+    ].map((trigger) => ({
       result: { data: { trigger } },
-      delay: 10
+      delay: 10,
     }));
 
     //These are the results from the resubscription
     const results3 = [
-      'NewUser: 1',
-      'NewUser: 2',
-      'NewUser: 3',
-      'NewUser: 4'
-    ].map(name => ({
+      "NewUser: 1",
+      "NewUser: 2",
+      "NewUser: 3",
+      "NewUser: 4",
+    ].map((name) => ({
       result: { data: { user: { name } } },
-      delay: 10
+      delay: 10,
     }));
 
     const query: DocumentNode = gql`
@@ -271,14 +271,14 @@ describe('subscriptions', () => {
     const userLink = new MockSubscriptionLink();
     const triggerLink = new MockSubscriptionLink();
     const link = new ApolloLink((o, f) => (f ? f(o) : null)).split(
-      ({ operationName }) => operationName === 'UserInfo',
+      ({ operationName }) => operationName === "UserInfo",
       userLink,
       triggerLink
     );
 
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     let count = 0;
@@ -288,9 +288,9 @@ describe('subscriptions', () => {
 
     const Container = graphql<{}, TriggerData>(triggerQuery)(
       graphql<TriggerQueryChildProps, QueryData>(query, {
-        shouldResubscribe: nextProps => {
-          return nextProps.data!.trigger === 'trigger resubscribe';
-        }
+        shouldResubscribe: (nextProps) => {
+          return nextProps.data!.trigger === "trigger resubscribe";
+        },
       })(
         class Query extends React.Component<ComposedProps> {
           componentDidUpdate() {
diff --git a/src/react/hooks/__tests__/useApolloClient.test.tsx b/src/react/hooks/__tests__/useApolloClient.test.tsx
--- a/src/react/hooks/__tests__/useApolloClient.test.tsx
+++ b/src/react/hooks/__tests__/useApolloClient.test.tsx
@@ -1,22 +1,18 @@
-import React from 'react';
-import { render } from '@testing-library/react';
-import { InvariantError } from 'ts-invariant';
+import React from "react";
+import { render } from "@testing-library/react";
+import { InvariantError } from "ts-invariant";
 
-import { ApolloClient } from '../../../core';
-import { ApolloLink } from '../../../link/core';
-import { ApolloProvider, resetApolloContext } from '../../context';
-import { InMemoryCache } from '../../../cache';
-import { useApolloClient } from '../useApolloClient';
+import { ApolloClient } from "../../../core";
+import { ApolloLink } from "../../../link/core";
+import { ApolloProvider } from "../../context";
+import { InMemoryCache } from "../../../cache";
+import { useApolloClient } from "../useApolloClient";
 
-describe('useApolloClient Hook', () => {
-  afterEach(() => {
-    resetApolloContext();
-  });
-
-  it('should return a client instance from the context if available', () => {
+describe("useApolloClient Hook", () => {
+  it("should return a client instance from the context if available", () => {
     const client = new ApolloClient({
       cache: new InMemoryCache(),
-      link: ApolloLink.empty()
+      link: ApolloLink.empty(),
     });
 
     function App() {
diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx
@@ -0,0 +1,5436 @@
+import React, { Fragment, StrictMode, Suspense } from "react";
+import {
+  act,
+  render,
+  screen,
+  renderHook,
+  RenderHookOptions,
+  waitFor,
+} from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary";
+import { expectTypeOf } from "expect-type";
+import { GraphQLError } from "graphql";
+import {
+  gql,
+  ApolloError,
+  DocumentNode,
+  ApolloClient,
+  ErrorPolicy,
+  NormalizedCacheObject,
+  NetworkStatus,
+  ApolloCache,
+  TypedDocumentNode,
+  ApolloLink,
+  Observable,
+  FetchMoreQueryOptions,
+  OperationVariables,
+  ApolloQueryResult,
+} from "../../../core";
+import {
+  MockedResponse,
+  MockedProvider,
+  MockLink,
+  MockSubscriptionLink,
+  mockSingleLink,
+} from "../../../testing";
+import {
+  concatPagination,
+  offsetLimitPagination,
+  DeepPartial,
+} from "../../../utilities";
+import { useBackgroundQuery } from "../useBackgroundQuery";
+import { useReadQuery } from "../useReadQuery";
+import { ApolloProvider } from "../../context";
+import { unwrapQueryRef, QueryReference } from "../../cache/QueryReference";
+import { InMemoryCache } from "../../../cache";
+import {
+  SuspenseQueryHookFetchPolicy,
+  SuspenseQueryHookOptions,
+} from "../../types/types";
+import equal from "@wry/equality";
+import { RefetchWritePolicy } from "../../../core/watchQueryOptions";
+import { skipToken } from "../constants";
+
+function renderIntegrationTest({
+  client,
+}: {
+  client?: ApolloClient<NormalizedCacheObject>;
+} = {}) {
+  const query: TypedDocumentNode<QueryData> = gql`
+    query SimpleQuery {
+      foo {
+        bar
+      }
+    }
+  `;
+
+  const mocks = [
+    {
+      request: { query },
+      result: { data: { foo: { bar: "hello" } } },
+    },
+  ];
+  const _client =
+    client ||
+    new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+  interface Renders {
+    errors: Error[];
+    errorCount: number;
+    suspenseCount: number;
+    count: number;
+  }
+  const renders: Renders = {
+    errors: [],
+    errorCount: 0,
+    suspenseCount: 0,
+    count: 0,
+  };
+  const errorBoundaryProps: ErrorBoundaryProps = {
+    fallback: <div>Error</div>,
+    onError: (error) => {
+      renders.errorCount++;
+      renders.errors.push(error);
+    },
+  };
+
+  interface QueryData {
+    foo: { bar: string };
+  }
+
+  function SuspenseFallback() {
+    renders.suspenseCount++;
+    return <div>loading</div>;
+  }
+
+  function Child({ queryRef }: { queryRef: QueryReference<QueryData> }) {
+    const { data } = useReadQuery(queryRef);
+    // count renders in the child component
+    renders.count++;
+    return <div>{data.foo.bar}</div>;
+  }
+
+  function Parent() {
+    const [queryRef] = useBackgroundQuery(query);
+    return <Child queryRef={queryRef} />;
+  }
+
+  function ParentWithVariables() {
+    const [queryRef] = useBackgroundQuery(query);
+    return <Child queryRef={queryRef} />;
+  }
+
+  function App({ variables }: { variables?: Record<string, unknown> }) {
+    return (
+      <ApolloProvider client={_client}>
+        <ErrorBoundary {...errorBoundaryProps}>
+          <Suspense fallback={<SuspenseFallback />}>
+            {variables ? <ParentWithVariables /> : <Parent />}
+          </Suspense>
+        </ErrorBoundary>
+      </ApolloProvider>
+    );
+  }
+
+  const { ...rest } = render(<App />);
+  return { ...rest, query, client: _client, renders };
+}
+
+interface VariablesCaseData {
+  character: {
+    id: string;
+    name: string;
+  };
+}
+
+interface VariablesCaseVariables {
+  id: string;
+}
+
+function useVariablesIntegrationTestCase() {
+  const query: TypedDocumentNode<
+    VariablesCaseData,
+    VariablesCaseVariables
+  > = gql`
+    query CharacterQuery($id: ID!) {
+      character(id: $id) {
+        id
+        name
+      }
+    }
+  `;
+  const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"];
+  let mocks = [...CHARACTERS].map((name, index) => ({
+    request: { query, variables: { id: String(index + 1) } },
+    result: { data: { character: { id: String(index + 1), name } } },
+  }));
+  return { mocks, query };
+}
+
+function renderVariablesIntegrationTest({
+  variables,
+  mocks,
+  errorPolicy,
+  options,
+  cache,
+}: {
+  mocks?: {
+    request: { query: DocumentNode; variables: { id: string } };
+    result: {
+      data?: {
+        character: {
+          id: string;
+          name: string | null;
+        };
+      };
+    };
+  }[];
+  variables: { id: string };
+  options?: SuspenseQueryHookOptions;
+  cache?: InMemoryCache;
+  errorPolicy?: ErrorPolicy;
+}) {
+  let { mocks: _mocks, query } = useVariablesIntegrationTestCase();
+
+  // duplicate mocks with (updated) in the name for refetches
+  _mocks = [..._mocks, ..._mocks, ..._mocks].map(
+    ({ request, result }, index) => {
+      return {
+        request: request,
+        result: {
+          data: {
+            character: {
+              ...result.data.character,
+              name:
+                index > 3
+                  ? index > 7
+                    ? `${result.data.character.name} (updated again)`
+                    : `${result.data.character.name} (updated)`
+                  : result.data.character.name,
+            },
+          },
+        },
+      };
+    }
+  );
+  const client = new ApolloClient({
+    cache: cache || new InMemoryCache(),
+    link: new MockLink(mocks || _mocks),
+  });
+  interface Renders {
+    errors: Error[];
+    errorCount: number;
+    suspenseCount: number;
+    count: number;
+    frames: {
+      data: VariablesCaseData;
+      networkStatus: NetworkStatus;
+      error: ApolloError | undefined;
+    }[];
+  }
+  const renders: Renders = {
+    errors: [],
+    errorCount: 0,
+    suspenseCount: 0,
+    count: 0,
+    frames: [],
+  };
+
+  const errorBoundaryProps: ErrorBoundaryProps = {
+    fallback: <div>Error</div>,
+    onError: (error) => {
+      renders.errorCount++;
+      renders.errors.push(error);
+    },
+  };
+
+  function SuspenseFallback() {
+    renders.suspenseCount++;
+    return <div>loading</div>;
+  }
+
+  function Child({
+    refetch,
+    variables: _variables,
+    queryRef,
+  }: {
+    variables: VariablesCaseVariables;
+    refetch: (
+      variables?: Partial<OperationVariables> | undefined
+    ) => Promise<ApolloQueryResult<VariablesCaseData>>;
+    queryRef: QueryReference<VariablesCaseData>;
+  }) {
+    const { data, error, networkStatus } = useReadQuery(queryRef);
+    const [variables, setVariables] = React.useState(_variables);
+    // count renders in the child component
+    renders.count++;
+    renders.frames.push({ data, networkStatus, error });
+
+    return (
+      <div>
+        {error ? <div>{error.message}</div> : null}
+        <button
+          onClick={() => {
+            refetch(variables);
+          }}
+        >
+          Refetch
+        </button>
+        <button
+          onClick={() => {
+            setVariables({ id: "2" });
+          }}
+        >
+          Set variables to id: 2
+        </button>
+        {data?.character.id} - {data?.character.name}
+      </div>
+    );
+  }
+
+  function ParentWithVariables({
+    variables,
+    errorPolicy = "none",
+  }: {
+    variables: VariablesCaseVariables;
+    errorPolicy?: ErrorPolicy;
+  }) {
+    const [queryRef, { refetch }] = useBackgroundQuery(query, {
+      ...options,
+      variables,
+      errorPolicy,
+    });
+    return (
+      <Child refetch={refetch} variables={variables} queryRef={queryRef} />
+    );
+  }
+
+  function App({
+    variables,
+    errorPolicy,
+  }: {
+    variables: VariablesCaseVariables;
+    errorPolicy?: ErrorPolicy;
+  }) {
+    return (
+      <ApolloProvider client={client}>
+        <ErrorBoundary {...errorBoundaryProps}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <ParentWithVariables
+              variables={variables}
+              errorPolicy={errorPolicy}
+            />
+          </Suspense>
+        </ErrorBoundary>
+      </ApolloProvider>
+    );
+  }
+
+  const { ...rest } = render(
+    <App errorPolicy={errorPolicy} variables={variables} />
+  );
+  const rerender = ({ variables }: { variables: VariablesCaseVariables }) => {
+    return rest.rerender(<App variables={variables} />);
+  };
+  return { ...rest, query, rerender, client, renders, mocks: mocks || _mocks };
+}
+
+function renderPaginatedIntegrationTest({
+  updateQuery,
+  fieldPolicies,
+}: {
+  fieldPolicies?: boolean;
+  updateQuery?: boolean;
+  mocks?: {
+    request: {
+      query: DocumentNode;
+      variables: { offset: number; limit: number };
+    };
+    result: {
+      data: {
+        letters: {
+          letter: string;
+          position: number;
+        }[];
+      };
+    };
+  }[];
+} = {}) {
+  interface QueryData {
+    letters: {
+      letter: string;
+      position: number;
+    }[];
+  }
+
+  interface Variables {
+    limit?: number;
+    offset?: number;
+  }
+
+  const query: TypedDocumentNode<QueryData, Variables> = gql`
+    query letters($limit: Int, $offset: Int) {
+      letters(limit: $limit) {
+        letter
+        position
+      }
+    }
+  `;
+
+  const data = "ABCDEFG"
+    .split("")
+    .map((letter, index) => ({ letter, position: index + 1 }));
+
+  const link = new ApolloLink((operation) => {
+    const { offset = 0, limit = 2 } = operation.variables;
+    const letters = data.slice(offset, offset + limit);
+
+    return new Observable((observer) => {
+      setTimeout(() => {
+        observer.next({ data: { letters } });
+        observer.complete();
+      }, 10);
+    });
+  });
+
+  const cacheWithTypePolicies = new InMemoryCache({
+    typePolicies: {
+      Query: {
+        fields: {
+          letters: concatPagination(),
+        },
+      },
+    },
+  });
+  const client = new ApolloClient({
+    cache: fieldPolicies ? cacheWithTypePolicies : new InMemoryCache(),
+    link,
+  });
+  interface Renders {
+    errors: Error[];
+    errorCount: number;
+    suspenseCount: number;
+    count: number;
+  }
+  const renders: Renders = {
+    errors: [],
+    errorCount: 0,
+    suspenseCount: 0,
+    count: 0,
+  };
+
+  const errorBoundaryProps: ErrorBoundaryProps = {
+    fallback: <div>Error</div>,
+    onError: (error) => {
+      renders.errorCount++;
+      renders.errors.push(error);
+    },
+  };
+
+  function SuspenseFallback() {
+    renders.suspenseCount++;
+    return <div>loading</div>;
+  }
+
+  function Child({
+    queryRef,
+    onFetchMore,
+  }: {
+    onFetchMore: (options: FetchMoreQueryOptions<Variables, QueryData>) => void;
+    queryRef: QueryReference<QueryData>;
+  }) {
+    const { data, error } = useReadQuery(queryRef);
+    // count renders in the child component
+    renders.count++;
+    return (
+      <div>
+        {error ? <div>{error.message}</div> : null}
+        <button
+          onClick={() => {
+            const fetchMoreOpts: FetchMoreQueryOptions<Variables, QueryData> & {
+              updateQuery?: (
+                previousQueryResult: QueryData,
+                options: {
+                  fetchMoreResult: QueryData;
+                  variables: Variables;
+                }
+              ) => QueryData;
+            } = { variables: { offset: 2, limit: 2 } };
+
+            if (updateQuery) {
+              fetchMoreOpts.updateQuery = (prev, { fetchMoreResult }) => ({
+                letters: prev.letters.concat(fetchMoreResult.letters),
+              });
+            }
+
+            onFetchMore(fetchMoreOpts);
+          }}
+        >
+          Fetch more
+        </button>
+        <ul>
+          {data.letters.map(({ letter, position }) => (
+            <li data-testid="letter" key={position}>
+              {letter}
+            </li>
+          ))}
+        </ul>
+      </div>
+    );
+  }
+
+  function ParentWithVariables() {
+    const [queryRef, { fetchMore }] = useBackgroundQuery(query, {
+      variables: { limit: 2, offset: 0 },
+    });
+    return <Child onFetchMore={fetchMore} queryRef={queryRef} />;
+  }
+
+  function App() {
+    return (
+      <ApolloProvider client={client}>
+        <ErrorBoundary {...errorBoundaryProps}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <ParentWithVariables />
+          </Suspense>
+        </ErrorBoundary>
+      </ApolloProvider>
+    );
+  }
+
+  const { ...rest } = render(<App />);
+  return { ...rest, data, query, client, renders };
+}
+
+type RenderSuspenseHookOptions<Props, TSerializedCache = {}> = Omit<
+  RenderHookOptions<Props>,
+  "wrapper"
+> & {
+  client?: ApolloClient<TSerializedCache>;
+  link?: ApolloLink;
+  cache?: ApolloCache<TSerializedCache>;
+  mocks?: MockedResponse[];
+  strictMode?: boolean;
+};
+
+interface Renders<Result> {
+  errors: Error[];
+  errorCount: number;
+  suspenseCount: number;
+  count: number;
+  frames: Result[];
+}
+
+interface SimpleQueryData {
+  greeting: string;
+}
+
+function renderSuspenseHook<Result, Props>(
+  render: (initialProps: Props) => Result,
+  options: RenderSuspenseHookOptions<Props> = Object.create(null)
+) {
+  function SuspenseFallback() {
+    renders.suspenseCount++;
+
+    return <div>loading</div>;
+  }
+
+  const renders: Renders<Result> = {
+    errors: [],
+    errorCount: 0,
+    suspenseCount: 0,
+    count: 0,
+    frames: [],
+  };
+
+  const { mocks = [], strictMode, ...renderHookOptions } = options;
+
+  const client =
+    options.client ||
+    new ApolloClient({
+      cache: options.cache || new InMemoryCache(),
+      link: options.link || new MockLink(mocks),
+    });
+
+  const view = renderHook(
+    (props) => {
+      renders.count++;
+
+      const view = render(props);
+
+      renders.frames.push(view);
+
+      return view;
+    },
+    {
+      ...renderHookOptions,
+      wrapper: ({ children }) => {
+        const Wrapper = strictMode ? StrictMode : Fragment;
+
+        return (
+          <Wrapper>
+            <Suspense fallback={<SuspenseFallback />}>
+              <ErrorBoundary
+                fallback={<div>Error</div>}
+                onError={(error) => {
+                  renders.errorCount++;
+                  renders.errors.push(error);
+                }}
+              >
+                <ApolloProvider client={client}>{children}</ApolloProvider>
+              </ErrorBoundary>
+            </Suspense>
+          </Wrapper>
+        );
+      },
+    }
+  );
+
+  return { ...view, renders };
+}
+
+describe("useBackgroundQuery", () => {
+  it("fetches a simple query with minimal config", async () => {
+    const query = gql`
+      query {
+        hello
+      }
+    `;
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { hello: "world 1" } },
+      },
+    ];
+    const { result } = renderHook(() => useBackgroundQuery(query), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    expect(_result).toEqual({
+      data: { hello: "world 1" },
+      loading: false,
+      networkStatus: 7,
+    });
+  });
+
+  it("allows the client to be overridden", async () => {
+    const query: TypedDocumentNode<SimpleQueryData> = gql`
+      query UserQuery {
+        greeting
+      }
+    `;
+
+    const globalClient = new ApolloClient({
+      link: new ApolloLink(() =>
+        Observable.of({ data: { greeting: "global hello" } })
+      ),
+      cache: new InMemoryCache(),
+    });
+
+    const localClient = new ApolloClient({
+      link: new ApolloLink(() =>
+        Observable.of({ data: { greeting: "local hello" } })
+      ),
+      cache: new InMemoryCache(),
+    });
+
+    const { result } = renderSuspenseHook(
+      () => useBackgroundQuery(query, { client: localClient }),
+      { client: globalClient }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    await waitFor(() => {
+      expect(_result).toEqual({
+        data: { greeting: "local hello" },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+      });
+    });
+  });
+
+  it("passes context to the link", async () => {
+    const query = gql`
+      query ContextQuery {
+        context
+      }
+    `;
+
+    const link = new ApolloLink((operation) => {
+      return new Observable((observer) => {
+        const { valueA, valueB } = operation.getContext();
+
+        observer.next({ data: { context: { valueA, valueB } } });
+        observer.complete();
+      });
+    });
+
+    const { result } = renderHook(
+      () =>
+        useBackgroundQuery(query, {
+          context: { valueA: "A", valueB: "B" },
+        }),
+      {
+        wrapper: ({ children }) => (
+          <MockedProvider link={link}>{children}</MockedProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    await waitFor(() => {
+      expect(_result).toMatchObject({
+        data: { context: { valueA: "A", valueB: "B" } },
+        networkStatus: NetworkStatus.ready,
+      });
+    });
+  });
+
+  it('enables canonical results when canonizeResults is "true"', async () => {
+    interface Result {
+      __typename: string;
+      value: number;
+    }
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Result: {
+          keyFields: false,
+        },
+      },
+    });
+
+    const query: TypedDocumentNode<{ results: Result[] }> = gql`
+      query {
+        results {
+          value
+        }
+      }
+    `;
+
+    const results: Result[] = [
+      { __typename: "Result", value: 0 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 2 },
+      { __typename: "Result", value: 3 },
+      { __typename: "Result", value: 5 },
+    ];
+
+    cache.writeQuery({
+      query,
+      data: { results },
+    });
+
+    const { result } = renderHook(
+      () =>
+        useBackgroundQuery(query, {
+          canonizeResults: true,
+        }),
+      {
+        wrapper: ({ children }) => (
+          <MockedProvider cache={cache}>{children}</MockedProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+    const resultSet = new Set(_result.data.results);
+    const values = Array.from(resultSet).map((item) => item.value);
+
+    expect(_result.data).toEqual({ results });
+    expect(_result.data.results.length).toBe(6);
+    expect(resultSet.size).toBe(5);
+    expect(values).toEqual([0, 1, 2, 3, 5]);
+  });
+
+  it("can disable canonical results when the cache's canonizeResults setting is true", async () => {
+    interface Result {
+      __typename: string;
+      value: number;
+    }
+
+    const cache = new InMemoryCache({
+      canonizeResults: true,
+      typePolicies: {
+        Result: {
+          keyFields: false,
+        },
+      },
+    });
+
+    const query: TypedDocumentNode<{ results: Result[] }> = gql`
+      query {
+        results {
+          value
+        }
+      }
+    `;
+
+    const results: Result[] = [
+      { __typename: "Result", value: 0 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 2 },
+      { __typename: "Result", value: 3 },
+      { __typename: "Result", value: 5 },
+    ];
+
+    cache.writeQuery({
+      query,
+      data: { results },
+    });
+
+    const { result } = renderHook(
+      () =>
+        useBackgroundQuery(query, {
+          canonizeResults: false,
+        }),
+      {
+        wrapper: ({ children }) => (
+          <MockedProvider cache={cache}>{children}</MockedProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+    const resultSet = new Set(_result.data.results);
+    const values = Array.from(resultSet).map((item) => item.value);
+
+    expect(_result.data).toEqual({ results });
+    expect(_result.data.results.length).toBe(6);
+    expect(resultSet.size).toBe(6);
+    expect(values).toEqual([0, 1, 1, 2, 3, 5]);
+  });
+
+  // TODO(FIXME): test fails, should return cache data first if it exists
+  it.skip("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => {
+    const query = gql`
+      {
+        hello
+      }
+    `;
+    const cache = new InMemoryCache();
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: { hello: "from link" } },
+      delay: 20,
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache,
+    });
+
+    cache.writeQuery({ query, data: { hello: "from cache" } });
+
+    const { result } = renderHook(
+      () => useBackgroundQuery(query, { fetchPolicy: "cache-and-network" }),
+      {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    expect(_result).toEqual({
+      data: { hello: "from link" },
+      loading: false,
+      networkStatus: 7,
+    });
+  });
+
+  it("all data is present in the cache, no network request is made", async () => {
+    const query = gql`
+      {
+        hello
+      }
+    `;
+    const cache = new InMemoryCache();
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: { hello: "from link" } },
+      delay: 20,
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache,
+    });
+
+    cache.writeQuery({ query, data: { hello: "from cache" } });
+
+    const { result } = renderHook(
+      () => useBackgroundQuery(query, { fetchPolicy: "cache-first" }),
+      {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    expect(_result).toEqual({
+      data: { hello: "from cache" },
+      loading: false,
+      networkStatus: 7,
+    });
+  });
+  it("partial data is present in the cache so it is ignored and network request is made", async () => {
+    const query = gql`
+      {
+        hello
+        foo
+      }
+    `;
+    const cache = new InMemoryCache();
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: { hello: "from link", foo: "bar" } },
+      delay: 20,
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache,
+    });
+
+    // we expect a "Missing field 'foo' while writing result..." error
+    // when writing hello to the cache, so we'll silence the console.error
+    const originalConsoleError = console.error;
+    console.error = () => {
+      /* noop */
+    };
+    cache.writeQuery({ query, data: { hello: "from cache" } });
+    console.error = originalConsoleError;
+
+    const { result } = renderHook(
+      () => useBackgroundQuery(query, { fetchPolicy: "cache-first" }),
+      {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    expect(_result).toEqual({
+      data: { foo: "bar", hello: "from link" },
+      loading: false,
+      networkStatus: 7,
+    });
+  });
+
+  it("existing data in the cache is ignored", async () => {
+    const query = gql`
+      {
+        hello
+      }
+    `;
+    const cache = new InMemoryCache();
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: { hello: "from link" } },
+      delay: 20,
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache,
+    });
+
+    cache.writeQuery({ query, data: { hello: "from cache" } });
+
+    const { result } = renderHook(
+      () => useBackgroundQuery(query, { fetchPolicy: "network-only" }),
+      {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    expect(_result).toEqual({
+      data: { hello: "from link" },
+      loading: false,
+      networkStatus: 7,
+    });
+    expect(client.cache.extract()).toEqual({
+      ROOT_QUERY: { __typename: "Query", hello: "from link" },
+    });
+  });
+
+  it("fetches data from the network but does not update the cache", async () => {
+    const query = gql`
+      {
+        hello
+      }
+    `;
+    const cache = new InMemoryCache();
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: { hello: "from link" } },
+      delay: 20,
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache,
+    });
+
+    cache.writeQuery({ query, data: { hello: "from cache" } });
+
+    const { result } = renderHook(
+      () => useBackgroundQuery(query, { fetchPolicy: "no-cache" }),
+      {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      }
+    );
+
+    const [queryRef] = result.current;
+
+    const _result = await unwrapQueryRef(queryRef).promise;
+
+    expect(_result).toEqual({
+      data: { hello: "from link" },
+      loading: false,
+      networkStatus: 7,
+    });
+    // ...but not updated in the cache
+    expect(client.cache.extract()).toEqual({
+      ROOT_QUERY: { __typename: "Query", hello: "from cache" },
+    });
+  });
+
+  describe("integration tests with useReadQuery", () => {
+    it("suspends and renders hello", async () => {
+      const { renders } = renderIntegrationTest();
+      // ensure the hook suspends immediately
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      // the parent component re-renders when promise fulfilled
+      expect(await screen.findByText("hello")).toBeInTheDocument();
+      expect(renders.count).toBe(1);
+    });
+
+    it("works with startTransition to change variables", async () => {
+      type Variables = {
+        id: string;
+      };
+
+      interface Data {
+        todo: {
+          id: string;
+          name: string;
+          completed: boolean;
+        };
+      }
+      const user = userEvent.setup();
+
+      const query: TypedDocumentNode<Data, Variables> = gql`
+        query TodoItemQuery($id: ID!) {
+          todo(id: $id) {
+            id
+            name
+            completed
+          }
+        }
+      `;
+
+      const mocks: MockedResponse<Data, Variables>[] = [
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { todo: { id: "1", name: "Clean room", completed: false } },
+          },
+          delay: 10,
+        },
+        {
+          request: { query, variables: { id: "2" } },
+          result: {
+            data: {
+              todo: { id: "2", name: "Take out trash", completed: true },
+            },
+          },
+          delay: 10,
+        },
+      ];
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache: new InMemoryCache(),
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [id, setId] = React.useState("1");
+        const [queryRef] = useBackgroundQuery(query, {
+          variables: { id },
+        });
+        return <Todo queryRef={queryRef} onChange={setId} />;
+      }
+
+      function Todo({
+        queryRef,
+        onChange,
+      }: {
+        queryRef: QueryReference<Data>;
+        onChange: (id: string) => void;
+      }) {
+        const { data } = useReadQuery(queryRef);
+        const [isPending, startTransition] = React.useTransition();
+        const { todo } = data;
+
+        return (
+          <>
+            <button
+              onClick={() => {
+                startTransition(() => {
+                  onChange("2");
+                });
+              }}
+            >
+              Refresh
+            </button>
+            <div data-testid="todo" aria-busy={isPending}>
+              {todo.name}
+              {todo.completed && " (completed)"}
+            </div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(screen.getByText("Loading")).toBeInTheDocument();
+
+      expect(await screen.findByTestId("todo")).toBeInTheDocument();
+
+      const todo = screen.getByTestId("todo");
+      const button = screen.getByText("Refresh");
+
+      expect(todo).toHaveTextContent("Clean room");
+
+      await act(() => user.click(button));
+
+      // startTransition will avoid rendering the suspense fallback for already
+      // revealed content if the state update inside the transition causes the
+      // component to suspend.
+      //
+      // Here we should not see the suspense fallback while the component suspends
+      // until the todo is finished loading. Seeing the suspense fallback is an
+      // indication that we are suspending the component too late in the process.
+      expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+
+      // We can ensure this works with isPending from useTransition in the process
+      expect(todo).toHaveAttribute("aria-busy", "true");
+
+      // Ensure we are showing the stale UI until the new todo has loaded
+      expect(todo).toHaveTextContent("Clean room");
+
+      // Eventually we should see the updated todo content once its done
+      // suspending.
+      await waitFor(() => {
+        expect(todo).toHaveTextContent("Take out trash (completed)");
+      });
+    });
+
+    it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => {
+      interface Data {
+        greeting: {
+          __typename: string;
+          message: string;
+          recipient: { name: string; __typename: string };
+        };
+      }
+
+      const query: TypedDocumentNode<Data> = gql`
+        query {
+          greeting {
+            message
+            ... on Greeting @defer {
+              recipient {
+                name
+              }
+            }
+          }
+        }
+      `;
+
+      const link = new MockSubscriptionLink();
+      const cache = new InMemoryCache();
+      cache.writeQuery({
+        query,
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello cached",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+      });
+      const client = new ApolloClient({ cache, link });
+      let renders = 0;
+      let suspenseCount = 0;
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        suspenseCount++;
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [queryRef] = useBackgroundQuery(query, {
+          fetchPolicy: "cache-and-network",
+        });
+        return <Todo queryRef={queryRef} />;
+      }
+
+      function Todo({ queryRef }: { queryRef: QueryReference<Data> }) {
+        const { data, networkStatus, error } = useReadQuery(queryRef);
+        const { greeting } = data;
+        renders++;
+
+        return (
+          <>
+            <div>Message: {greeting.message}</div>
+            <div>Recipient: {greeting.recipient.name}</div>
+            <div>Network status: {networkStatus}</div>
+            <div>Error: {error ? error.message : "none"}</div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(screen.getByText(/Message/i)).toHaveTextContent(
+        "Message: Hello cached"
+      );
+      expect(screen.getByText(/Recipient/i)).toHaveTextContent(
+        "Recipient: Cached Alice"
+      );
+      expect(screen.getByText(/Network status/i)).toHaveTextContent(
+        "Network status: 1" // loading
+      );
+      expect(screen.getByText(/Error/i)).toHaveTextContent("none");
+
+      link.simulateResult({
+        result: {
+          data: {
+            greeting: { __typename: "Greeting", message: "Hello world" },
+          },
+          hasNext: true,
+        },
+      });
+
+      await waitFor(() => {
+        expect(screen.getByText(/Message/i)).toHaveTextContent(
+          "Message: Hello world"
+        );
+      });
+      expect(screen.getByText(/Recipient/i)).toHaveTextContent(
+        "Recipient: Cached Alice"
+      );
+      expect(screen.getByText(/Network status/i)).toHaveTextContent(
+        "Network status: 7" // ready
+      );
+      expect(screen.getByText(/Error/i)).toHaveTextContent("none");
+
+      link.simulateResult({
+        result: {
+          incremental: [
+            {
+              data: {
+                recipient: { name: "Alice", __typename: "Person" },
+                __typename: "Greeting",
+              },
+              path: ["greeting"],
+            },
+          ],
+          hasNext: false,
+        },
+      });
+
+      await waitFor(() => {
+        expect(screen.getByText(/Recipient/i)).toHaveTextContent(
+          "Recipient: Alice"
+        );
+      });
+      expect(screen.getByText(/Message/i)).toHaveTextContent(
+        "Message: Hello world"
+      );
+      expect(screen.getByText(/Network status/i)).toHaveTextContent(
+        "Network status: 7" // ready
+      );
+      expect(screen.getByText(/Error/i)).toHaveTextContent("none");
+
+      expect(renders).toBe(3);
+      expect(suspenseCount).toBe(0);
+    });
+  });
+
+  it("reacts to cache updates", async () => {
+    const { renders, client, query } = renderIntegrationTest();
+
+    expect(renders.suspenseCount).toBe(1);
+    expect(screen.getByText("loading")).toBeInTheDocument();
+
+    // the parent component re-renders when promise fulfilled
+    expect(await screen.findByText("hello")).toBeInTheDocument();
+    expect(renders.count).toBe(1);
+
+    client.writeQuery({
+      query,
+      data: { foo: { bar: "baz" } },
+    });
+
+    // the parent component re-renders when promise fulfilled
+    expect(await screen.findByText("baz")).toBeInTheDocument();
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+
+    client.writeQuery({
+      query,
+      data: { foo: { bar: "bat" } },
+    });
+
+    expect(await screen.findByText("bat")).toBeInTheDocument();
+
+    expect(renders.suspenseCount).toBe(1);
+  });
+
+  it("reacts to variables updates", async () => {
+    const { renders, rerender } = renderVariablesIntegrationTest({
+      variables: { id: "1" },
+    });
+
+    expect(renders.suspenseCount).toBe(1);
+    expect(screen.getByText("loading")).toBeInTheDocument();
+
+    expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument();
+
+    rerender({ variables: { id: "2" } });
+
+    expect(renders.suspenseCount).toBe(2);
+    expect(screen.getByText("loading")).toBeInTheDocument();
+
+    expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument();
+  });
+
+  it("does not suspend when `skip` is true", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [queryRef] = useBackgroundQuery(query, { skip: true });
+
+      return (
+        <Suspense fallback={<SuspenseFallback />}>
+          {queryRef && <Greeting queryRef={queryRef} />}
+        </Suspense>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
+    expect(screen.queryByTestId("greeting")).not.toBeInTheDocument();
+  });
+
+  it("does not suspend when using `skipToken` in options", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [queryRef] = useBackgroundQuery(query, skipToken);
+
+      return (
+        <Suspense fallback={<SuspenseFallback />}>
+          {queryRef && <Greeting queryRef={queryRef} />}
+        </Suspense>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
+    expect(screen.queryByTestId("greeting")).not.toBeInTheDocument();
+  });
+
+  it("suspends when `skip` becomes `false` after it was `true`", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [queryRef] = useBackgroundQuery(query, { skip });
+
+      return (
+        <>
+          <button onClick={() => setSkip(false)}>Run query</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
+    expect(screen.queryByTestId("greeting")).not.toBeInTheDocument();
+
+    await act(() => user.click(screen.getByText("Run query")));
+
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+  });
+
+  it("suspends when switching away from `skipToken` in options", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [queryRef] = useBackgroundQuery(
+        query,
+        skip ? skipToken : undefined
+      );
+
+      return (
+        <>
+          <button onClick={() => setSkip(false)}>Run query</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
+    expect(screen.queryByTestId("greeting")).not.toBeInTheDocument();
+
+    await act(() => user.click(screen.getByText("Run query")));
+
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+  });
+
+  it("renders skip result, does not suspend, and maintains `data` when `skip` becomes `true` after it was `false`", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(false);
+      const [queryRef] = useBackgroundQuery(query, { skip });
+
+      return (
+        <>
+          <button onClick={() => setSkip((skip) => !skip)}>Toggle skip</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+  });
+
+  it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(false);
+      const [queryRef] = useBackgroundQuery(
+        query,
+        skip ? skipToken : undefined
+      );
+
+      return (
+        <>
+          <button onClick={() => setSkip((skip) => !skip)}>Toggle skip</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+  });
+
+  it("does not make network requests when `skip` is `true`", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    let fetchCount = 0;
+
+    const link = new ApolloLink((operation) => {
+      return new Observable((observer) => {
+        fetchCount++;
+
+        const mock = mocks.find(({ request }) =>
+          equal(request.query, operation.query)
+        );
+
+        if (!mock) {
+          throw new Error("Could not find mock for operation");
+        }
+
+        observer.next(mock.result);
+        observer.complete();
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [queryRef] = useBackgroundQuery(query, { skip });
+
+      return (
+        <>
+          <button onClick={() => setSkip((skip) => !skip)}>Toggle skip</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(fetchCount).toBe(0);
+
+    // Toggle skip to `false`
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(fetchCount).toBe(1);
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+
+    // Toggle skip to `true`
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(fetchCount).toBe(1);
+  });
+
+  it("does not make network requests when `skipToken` is used", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    let fetchCount = 0;
+
+    const link = new ApolloLink((operation) => {
+      return new Observable((observer) => {
+        fetchCount++;
+
+        const mock = mocks.find(({ request }) =>
+          equal(request.query, operation.query)
+        );
+
+        if (!mock) {
+          throw new Error("Could not find mock for operation");
+        }
+
+        observer.next(mock.result);
+        observer.complete();
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [queryRef] = useBackgroundQuery(
+        query,
+        skip ? skipToken : undefined
+      );
+
+      return (
+        <>
+          <button onClick={() => setSkip((skip) => !skip)}>Toggle skip</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(fetchCount).toBe(0);
+
+    // Toggle skip to `false`
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(fetchCount).toBe(1);
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+
+    // Toggle skip to `true`
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(fetchCount).toBe(1);
+  });
+
+  it("`skip` result is referentially stable", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    interface CurrentResult {
+      current: Data | undefined;
+    }
+
+    const user = userEvent.setup();
+
+    const result: CurrentResult = {
+      current: undefined,
+    };
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [queryRef] = useBackgroundQuery(query, { skip });
+
+      return (
+        <>
+          <button onClick={() => setSkip((skip) => !skip)}>Toggle skip</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      result.current = data;
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    const { rerender } = render(<App />);
+
+    const skipResult = result.current;
+
+    rerender(<App />);
+
+    expect(result.current).toBe(skipResult);
+
+    // Toggle skip to `false`
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+
+    const fetchedResult = result.current;
+
+    rerender(<App />);
+
+    expect(result.current).toBe(fetchedResult);
+  });
+
+  it("`skip` result is referentially stable when using `skipToken`", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    interface CurrentResult {
+      current: Data | undefined;
+    }
+
+    const user = userEvent.setup();
+
+    const result: CurrentResult = {
+      current: undefined,
+    };
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [queryRef] = useBackgroundQuery(
+        query,
+        skip ? skipToken : undefined
+      );
+
+      return (
+        <>
+          <button onClick={() => setSkip((skip) => !skip)}>Toggle skip</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      result.current = data;
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    const { rerender } = render(<App />);
+
+    const skipResult = result.current;
+
+    rerender(<App />);
+
+    expect(result.current).toBe(skipResult);
+
+    // Toggle skip to `false`
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+
+    const fetchedResult = result.current;
+
+    rerender(<App />);
+
+    expect(result.current).toBe(fetchedResult);
+  });
+
+  it("`skip` option works with `startTransition`", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [isPending, startTransition] = React.useTransition();
+      const [queryRef] = useBackgroundQuery(query, { skip });
+
+      return (
+        <>
+          <button
+            disabled={isPending}
+            onClick={() =>
+              startTransition(() => {
+                setSkip((skip) => !skip);
+              })
+            }
+          >
+            Toggle skip
+          </button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    const button = screen.getByText("Toggle skip");
+
+    // Toggle skip to `false`
+    await act(() => user.click(button));
+
+    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
+    expect(button).toBeDisabled();
+    expect(screen.queryByTestId("greeting")).not.toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+  });
+
+  it("`skipToken` works with `startTransition`", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [skip, setSkip] = React.useState(true);
+      const [isPending, startTransition] = React.useTransition();
+      const [queryRef] = useBackgroundQuery(
+        query,
+        skip ? skipToken : undefined
+      );
+
+      return (
+        <>
+          <button
+            disabled={isPending}
+            onClick={() =>
+              startTransition(() => {
+                setSkip((skip) => !skip);
+              })
+            }
+          >
+            Toggle skip
+          </button>
+          <Suspense fallback={<SuspenseFallback />}>
+            {queryRef && <Greeting queryRef={queryRef} />}
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="greeting">{data.greeting}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    const button = screen.getByText("Toggle skip");
+
+    // Toggle skip to `false`
+    await act(() => user.click(button));
+
+    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
+    expect(button).toBeDisabled();
+    expect(screen.queryByTestId("greeting")).not.toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("greeting")).toHaveTextContent("Hello");
+    });
+  });
+
+  it("applies `errorPolicy` on next fetch when it changes between renders", async () => {
+    interface Data {
+      greeting: string;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        greeting
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello" } },
+      },
+      {
+        request: { query },
+        result: {
+          errors: [new GraphQLError("oops")],
+        },
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [errorPolicy, setErrorPolicy] = React.useState<ErrorPolicy>("none");
+      const [queryRef, { refetch }] = useBackgroundQuery(query, {
+        errorPolicy,
+      });
+
+      return (
+        <>
+          <button onClick={() => setErrorPolicy("all")}>
+            Change error policy
+          </button>
+          <button onClick={() => refetch()}>Refetch greeting</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Greeting queryRef={queryRef} />
+          </Suspense>
+        </>
+      );
+    }
+
+    function Greeting({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data, error } = useReadQuery(queryRef);
+
+      return error ? (
+        <div data-testid="error">{error.message}</div>
+      ) : (
+        <div data-testid="greeting">{data.greeting}</div>
+      );
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <ErrorBoundary
+            fallback={<div data-testid="error">Error boundary</div>}
+          >
+            <Parent />
+          </ErrorBoundary>
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello");
+
+    await act(() => user.click(screen.getByText("Change error policy")));
+    await act(() => user.click(screen.getByText("Refetch greeting")));
+
+    // Ensure we aren't rendering the error boundary and instead rendering the
+    // error message in the Greeting component.
+    expect(await screen.findByTestId("error")).toHaveTextContent("oops");
+  });
+
+  it("applies `context` on next fetch when it changes between renders", async () => {
+    interface Data {
+      context: Record<string, any>;
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        context
+      }
+    `;
+
+    const link = new ApolloLink((operation) => {
+      return Observable.of({
+        data: {
+          context: operation.getContext(),
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [phase, setPhase] = React.useState("initial");
+      const [queryRef, { refetch }] = useBackgroundQuery(query, {
+        context: { phase },
+      });
+
+      return (
+        <>
+          <button onClick={() => setPhase("rerender")}>Update context</button>
+          <button onClick={() => refetch()}>Refetch</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Context queryRef={queryRef} />
+          </Suspense>
+        </>
+      );
+    }
+
+    function Context({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <div data-testid="context">{data.context.phase}</div>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    expect(await screen.findByTestId("context")).toHaveTextContent("initial");
+
+    await act(() => user.click(screen.getByText("Update context")));
+    await act(() => user.click(screen.getByText("Refetch")));
+
+    expect(await screen.findByTestId("context")).toHaveTextContent("rerender");
+  });
+
+  // NOTE: We only test the `false` -> `true` path here. If the option changes
+  // from `true` -> `false`, the data has already been canonized, so it has no
+  // effect on the output.
+  it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => {
+    interface Result {
+      __typename: string;
+      value: number;
+    }
+
+    interface Data {
+      results: Result[];
+    }
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Result: {
+          keyFields: false,
+        },
+      },
+    });
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        results {
+          value
+        }
+      }
+    `;
+
+    const results: Result[] = [
+      { __typename: "Result", value: 0 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 2 },
+      { __typename: "Result", value: 3 },
+      { __typename: "Result", value: 5 },
+    ];
+
+    const user = userEvent.setup();
+
+    cache.writeQuery({
+      query,
+      data: { results },
+    });
+
+    const client = new ApolloClient({
+      link: new MockLink([]),
+      cache,
+    });
+
+    const result: { current: Data | null } = {
+      current: null,
+    };
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [canonizeResults, setCanonizeResults] = React.useState(false);
+      const [queryRef] = useBackgroundQuery(query, {
+        canonizeResults,
+      });
+
+      return (
+        <>
+          <button onClick={() => setCanonizeResults(true)}>
+            Canonize results
+          </button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Results queryRef={queryRef} />
+          </Suspense>
+        </>
+      );
+    }
+
+    function Results({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      result.current = data;
+
+      return null;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    function verifyCanonicalResults(data: Data, canonized: boolean) {
+      const resultSet = new Set(data.results);
+      const values = Array.from(resultSet).map((item) => item.value);
+
+      expect(data).toEqual({ results });
+
+      if (canonized) {
+        expect(data.results.length).toBe(6);
+        expect(resultSet.size).toBe(5);
+        expect(values).toEqual([0, 1, 2, 3, 5]);
+      } else {
+        expect(data.results.length).toBe(6);
+        expect(resultSet.size).toBe(6);
+        expect(values).toEqual([0, 1, 1, 2, 3, 5]);
+      }
+    }
+
+    verifyCanonicalResults(result.current!, false);
+
+    await act(() => user.click(screen.getByText("Canonize results")));
+
+    verifyCanonicalResults(result.current!, true);
+  });
+
+  it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => {
+    interface Data {
+      primes: number[];
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data, { min: number; max: number }> = gql`
+      query GetPrimes($min: number, $max: number) {
+        primes(min: $min, max: $max)
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { min: 0, max: 12 } },
+        result: { data: { primes: [2, 3, 5, 7, 11] } },
+      },
+      {
+        request: { query, variables: { min: 12, max: 30 } },
+        result: { data: { primes: [13, 17, 19, 23, 29] } },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { min: 30, max: 50 } },
+        result: { data: { primes: [31, 37, 41, 43, 47] } },
+        delay: 10,
+      },
+    ];
+
+    const mergeParams: [number[] | undefined, number[]][] = [];
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            primes: {
+              keyArgs: false,
+              merge(existing: number[] | undefined, incoming: number[]) {
+                mergeParams.push([existing, incoming]);
+                return existing ? existing.concat(incoming) : incoming;
+              },
+            },
+          },
+        },
+      },
+    });
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache,
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [refetchWritePolicy, setRefetchWritePolicy] =
+        React.useState<RefetchWritePolicy>("merge");
+
+      const [queryRef, { refetch }] = useBackgroundQuery(query, {
+        refetchWritePolicy,
+        variables: { min: 0, max: 12 },
+      });
+
+      return (
+        <>
+          <button onClick={() => setRefetchWritePolicy("overwrite")}>
+            Change refetch write policy
+          </button>
+          <button onClick={() => refetch({ min: 12, max: 30 })}>
+            Refetch next
+          </button>
+          <button onClick={() => refetch({ min: 30, max: 50 })}>
+            Refetch last
+          </button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Primes queryRef={queryRef} />
+          </Suspense>
+        </>
+      );
+    }
+
+    function Primes({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <span data-testid="primes">{data.primes.join(", ")}</span>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    const primes = await screen.findByTestId("primes");
+
+    expect(primes).toHaveTextContent("2, 3, 5, 7, 11");
+    expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
+
+    await act(() => user.click(screen.getByText("Refetch next")));
+
+    await waitFor(() => {
+      expect(primes).toHaveTextContent("2, 3, 5, 7, 11, 13, 17, 19, 23, 29");
+    });
+
+    expect(mergeParams).toEqual([
+      [undefined, [2, 3, 5, 7, 11]],
+      [
+        [2, 3, 5, 7, 11],
+        [13, 17, 19, 23, 29],
+      ],
+    ]);
+
+    await act(() =>
+      user.click(screen.getByText("Change refetch write policy"))
+    );
+
+    await act(() => user.click(screen.getByText("Refetch last")));
+
+    await waitFor(() => {
+      expect(primes).toHaveTextContent("31, 37, 41, 43, 47");
+    });
+
+    expect(mergeParams).toEqual([
+      [undefined, [2, 3, 5, 7, 11]],
+      [
+        [2, 3, 5, 7, 11],
+        [13, 17, 19, 23, 29],
+      ],
+      [undefined, [31, 37, 41, 43, 47]],
+    ]);
+  });
+
+  it("applies `returnPartialData` on next fetch when it changes between renders", async () => {
+    interface Data {
+      character: {
+        __typename: "Character";
+        id: string;
+        name: string;
+      };
+    }
+
+    interface PartialData {
+      character: {
+        __typename: "Character";
+        id: string;
+      };
+    }
+
+    const user = userEvent.setup();
+
+    const fullQuery: TypedDocumentNode<Data> = gql`
+      query {
+        character {
+          __typename
+          id
+          name
+        }
+      }
+    `;
+
+    const partialQuery: TypedDocumentNode<PartialData> = gql`
+      query {
+        character {
+          __typename
+          id
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: fullQuery },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Doctor Strange",
+            },
+          },
+        },
+      },
+      {
+        request: { query: fullQuery },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Doctor Strange (refetched)",
+            },
+          },
+        },
+        delay: 100,
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { __typename: "Character", id: "1" } },
+    });
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache,
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [returnPartialData, setReturnPartialData] = React.useState(false);
+
+      const [queryRef] = useBackgroundQuery(fullQuery, {
+        returnPartialData,
+      });
+
+      return (
+        <>
+          <button onClick={() => setReturnPartialData(true)}>
+            Update partial data
+          </button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Character queryRef={queryRef} />
+          </Suspense>
+        </>
+      );
+    }
+
+    function Character({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return (
+        <span data-testid="character">{data.character.name ?? "unknown"}</span>
+      );
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    const character = await screen.findByTestId("character");
+
+    expect(character).toHaveTextContent("Doctor Strange");
+
+    await act(() => user.click(screen.getByText("Update partial data")));
+
+    cache.modify({
+      id: cache.identify({ __typename: "Character", id: "1" }),
+      fields: {
+        name: (_, { DELETE }) => DELETE,
+      },
+    });
+
+    await waitFor(() => {
+      expect(character).toHaveTextContent("unknown");
+    });
+
+    await waitFor(() => {
+      expect(character).toHaveTextContent("Doctor Strange (refetched)");
+    });
+  });
+
+  it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => {
+    interface Data {
+      character: {
+        __typename: "Character";
+        id: string;
+        name: string;
+      };
+    }
+
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        character {
+          __typename
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Doctor Strange",
+            },
+          },
+        },
+        delay: 10,
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: {
+        character: {
+          __typename: "Character",
+          id: "1",
+          name: "Doctor Strangecache",
+        },
+      },
+    });
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache,
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [fetchPolicy, setFetchPolicy] =
+        React.useState<SuspenseQueryHookFetchPolicy>("cache-first");
+
+      const [queryRef, { refetch }] = useBackgroundQuery(query, {
+        fetchPolicy,
+      });
+
+      return (
+        <>
+          <button onClick={() => setFetchPolicy("no-cache")}>
+            Change fetch policy
+          </button>
+          <button onClick={() => refetch()}>Refetch</button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Character queryRef={queryRef} />
+          </Suspense>
+        </>
+      );
+    }
+
+    function Character({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data } = useReadQuery(queryRef);
+
+      return <span data-testid="character">{data.character.name}</span>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    const character = await screen.findByTestId("character");
+
+    expect(character).toHaveTextContent("Doctor Strangecache");
+
+    await act(() => user.click(screen.getByText("Change fetch policy")));
+    await act(() => user.click(screen.getByText("Refetch")));
+    await waitFor(() => {
+      expect(character).toHaveTextContent("Doctor Strange");
+    });
+
+    // Because we switched to a `no-cache` fetch policy, we should not see the
+    // newly fetched data in the cache after the fetch occured.
+    expect(cache.readQuery({ query })).toEqual({
+      character: {
+        __typename: "Character",
+        id: "1",
+        name: "Doctor Strangecache",
+      },
+    });
+  });
+
+  it("properly handles changing options along with changing `variables`", async () => {
+    interface Data {
+      character: {
+        __typename: "Character";
+        id: string;
+        name: string;
+      };
+    }
+
+    const user = userEvent.setup();
+    const query: TypedDocumentNode<Data, { id: string }> = gql`
+      query ($id: ID!) {
+        character(id: $id) {
+          __typename
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          errors: [new GraphQLError("oops")],
+        },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "2",
+              name: "Hulk",
+            },
+          },
+        },
+        delay: 10,
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      variables: {
+        id: "1",
+      },
+      data: {
+        character: {
+          __typename: "Character",
+          id: "1",
+          name: "Doctor Strangecache",
+        },
+      },
+    });
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache,
+    });
+
+    function SuspenseFallback() {
+      return <div>Loading...</div>;
+    }
+
+    function Parent() {
+      const [id, setId] = React.useState("1");
+
+      const [queryRef, { refetch }] = useBackgroundQuery(query, {
+        errorPolicy: id === "1" ? "all" : "none",
+        variables: { id },
+      });
+
+      return (
+        <>
+          <button onClick={() => setId("1")}>Get first character</button>
+          <button onClick={() => setId("2")}>Get second character</button>
+          <button onClick={() => refetch()}>Refetch</button>
+          <ErrorBoundary
+            fallback={<div data-testid="error">Error boundary</div>}
+          >
+            <Suspense fallback={<SuspenseFallback />}>
+              <Character queryRef={queryRef} />
+            </Suspense>
+          </ErrorBoundary>
+        </>
+      );
+    }
+
+    function Character({ queryRef }: { queryRef: QueryReference<Data> }) {
+      const { data, error } = useReadQuery(queryRef);
+
+      return error ? (
+        <div data-testid="error">{error.message}</div>
+      ) : (
+        <span data-testid="character">{data.character.name}</span>
+      );
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Parent />
+        </ApolloProvider>
+      );
+    }
+
+    render(<App />);
+
+    const character = await screen.findByTestId("character");
+
+    expect(character).toHaveTextContent("Doctor Strangecache");
+
+    await act(() => user.click(screen.getByText("Get second character")));
+
+    await waitFor(() => {
+      expect(character).toHaveTextContent("Hulk");
+    });
+
+    await act(() => user.click(screen.getByText("Get first character")));
+
+    await waitFor(() => {
+      expect(character).toHaveTextContent("Doctor Strangecache");
+    });
+
+    await act(() => user.click(screen.getByText("Refetch")));
+
+    // Ensure we render the inline error instead of the error boundary, which
+    // tells us the error policy was properly applied.
+    expect(await screen.findByTestId("error")).toHaveTextContent("oops");
+  });
+
+  describe("refetch", () => {
+    it("re-suspends when calling `refetch`", async () => {
+      const { renders } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+      });
+
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument();
+
+      const button = screen.getByText("Refetch");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      // parent component re-suspends
+      expect(renders.suspenseCount).toBe(2);
+      expect(renders.count).toBe(2);
+
+      expect(
+        await screen.findByText("1 - Spider-Man (updated)")
+      ).toBeInTheDocument();
+    });
+    it("re-suspends when calling `refetch` with new variables", async () => {
+      interface QueryData {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      interface QueryVariables {
+        id: string;
+      }
+      const query: TypedDocumentNode<QueryData, QueryVariables> = gql`
+        query CharacterQuery($id: ID!) {
+          character(id: $id) {
+            id
+            name
+          }
+        }
+      `;
+
+      const mocks = [
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { character: { id: "1", name: "Captain Marvel" } },
+          },
+        },
+        {
+          request: { query, variables: { id: "2" } },
+          result: {
+            data: { character: { id: "2", name: "Captain America" } },
+          },
+        },
+      ];
+
+      const { renders } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+        mocks,
+      });
+
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument();
+
+      const newVariablesRefetchButton = screen.getByText(
+        "Set variables to id: 2"
+      );
+      const refetchButton = screen.getByText("Refetch");
+      const user = userEvent.setup();
+      await act(() => user.click(newVariablesRefetchButton));
+      await act(() => user.click(refetchButton));
+
+      expect(
+        await screen.findByText("2 - Captain America")
+      ).toBeInTheDocument();
+
+      // parent component re-suspends
+      expect(renders.suspenseCount).toBe(2);
+      expect(renders.count).toBe(3);
+
+      // extra render puts an additional frame into the array
+      expect(renders.frames).toMatchObject([
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          ...mocks[1].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    });
+    it("re-suspends multiple times when calling `refetch` multiple times", async () => {
+      const { renders } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+      });
+
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument();
+
+      const button = screen.getByText("Refetch");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      // parent component re-suspends
+      expect(renders.suspenseCount).toBe(2);
+      expect(renders.count).toBe(2);
+
+      expect(
+        await screen.findByText("1 - Spider-Man (updated)")
+      ).toBeInTheDocument();
+
+      await act(() => user.click(button));
+
+      // parent component re-suspends
+      expect(renders.suspenseCount).toBe(3);
+      expect(renders.count).toBe(3);
+
+      expect(
+        await screen.findByText("1 - Spider-Man (updated again)")
+      ).toBeInTheDocument();
+    });
+    it("throws errors when errors are returned after calling `refetch`", async () => {
+      const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+      interface QueryData {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      interface QueryVariables {
+        id: string;
+      }
+      const query: TypedDocumentNode<QueryData, QueryVariables> = gql`
+        query CharacterQuery($id: ID!) {
+          character(id: $id) {
+            id
+            name
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { character: { id: "1", name: "Captain Marvel" } },
+          },
+        },
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            errors: [new GraphQLError("Something went wrong")],
+          },
+        },
+      ];
+      const { renders } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+        mocks,
+      });
+
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument();
+
+      const button = screen.getByText("Refetch");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      await waitFor(() => {
+        expect(renders.errorCount).toBe(1);
+      });
+
+      expect(renders.errors).toEqual([
+        new ApolloError({
+          graphQLErrors: [new GraphQLError("Something went wrong")],
+        }),
+      ]);
+
+      consoleSpy.mockRestore();
+    });
+    it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => {
+      interface QueryData {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      interface QueryVariables {
+        id: string;
+      }
+      const query: TypedDocumentNode<QueryData, QueryVariables> = gql`
+        query CharacterQuery($id: ID!) {
+          character(id: $id) {
+            id
+            name
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { character: { id: "1", name: "Captain Marvel" } },
+          },
+        },
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            errors: [new GraphQLError("Something went wrong")],
+          },
+        },
+      ];
+
+      const { renders } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+        errorPolicy: "ignore",
+        mocks,
+      });
+
+      expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument();
+
+      const button = screen.getByText("Refetch");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      expect(renders.errorCount).toBe(0);
+      expect(renders.errors).toEqual([]);
+    });
+    it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => {
+      interface QueryData {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      interface QueryVariables {
+        id: string;
+      }
+      const query: TypedDocumentNode<QueryData, QueryVariables> = gql`
+        query CharacterQuery($id: ID!) {
+          character(id: $id) {
+            id
+            name
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { character: { id: "1", name: "Captain Marvel" } },
+          },
+        },
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            errors: [new GraphQLError("Something went wrong")],
+          },
+        },
+      ];
+
+      const { renders } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+        errorPolicy: "all",
+        mocks,
+      });
+
+      expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument();
+
+      const button = screen.getByText("Refetch");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      expect(renders.errorCount).toBe(0);
+      expect(renders.errors).toEqual([]);
+
+      expect(
+        await screen.findByText("Something went wrong")
+      ).toBeInTheDocument();
+    });
+    it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => {
+      interface QueryData {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      interface QueryVariables {
+        id: string;
+      }
+      const query: TypedDocumentNode<QueryData, QueryVariables> = gql`
+        query CharacterQuery($id: ID!) {
+          character(id: $id) {
+            id
+            name
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { character: { id: "1", name: "Captain Marvel" } },
+          },
+        },
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { character: { id: "1", name: null } },
+            errors: [new GraphQLError("Something went wrong")],
+          },
+        },
+      ];
+
+      const { renders } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+        errorPolicy: "all",
+        mocks,
+      });
+
+      expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument();
+
+      const button = screen.getByText("Refetch");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      expect(renders.errorCount).toBe(0);
+      expect(renders.errors).toEqual([]);
+
+      expect(
+        await screen.findByText("Something went wrong")
+      ).toBeInTheDocument();
+
+      const expectedError = new ApolloError({
+        graphQLErrors: [new GraphQLError("Something went wrong")],
+      });
+
+      expect(renders.frames).toMatchObject([
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          data: mocks[1].result.data,
+          networkStatus: NetworkStatus.error,
+          error: expectedError,
+        },
+      ]);
+    });
+    it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => {
+      type Variables = {
+        id: string;
+      };
+
+      interface Data {
+        todo: {
+          id: string;
+          name: string;
+          completed: boolean;
+        };
+      }
+      const user = userEvent.setup();
+
+      const query: TypedDocumentNode<Data, Variables> = gql`
+        query TodoItemQuery($id: ID!) {
+          todo(id: $id) {
+            id
+            name
+            completed
+          }
+        }
+      `;
+
+      const mocks: MockedResponse<Data, Variables>[] = [
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { todo: { id: "1", name: "Clean room", completed: false } },
+          },
+          delay: 10,
+        },
+        {
+          request: { query, variables: { id: "1" } },
+          result: {
+            data: { todo: { id: "1", name: "Clean room", completed: true } },
+          },
+          delay: 10,
+        },
+      ];
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache: new InMemoryCache(),
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [id, setId] = React.useState("1");
+        const [queryRef, { refetch }] = useBackgroundQuery(query, {
+          variables: { id },
+        });
+        const onRefetchHandler = () => {
+          refetch();
+        };
+        return (
+          <Todo
+            onRefetch={onRefetchHandler}
+            queryRef={queryRef}
+            onChange={setId}
+          />
+        );
+      }
+
+      function Todo({
+        queryRef,
+        onRefetch,
+      }: {
+        onRefetch: () => void;
+        queryRef: QueryReference<Data>;
+        onChange: (id: string) => void;
+      }) {
+        const { data } = useReadQuery(queryRef);
+        const [isPending, startTransition] = React.useTransition();
+        const { todo } = data;
+
+        return (
+          <>
+            <button
+              onClick={() => {
+                startTransition(() => {
+                  onRefetch();
+                });
+              }}
+            >
+              Refresh
+            </button>
+            <div data-testid="todo" aria-busy={isPending}>
+              {todo.name}
+              {todo.completed && " (completed)"}
+            </div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(screen.getByText("Loading")).toBeInTheDocument();
+
+      expect(await screen.findByTestId("todo")).toBeInTheDocument();
+
+      const todo = screen.getByTestId("todo");
+      const button = screen.getByText("Refresh");
+
+      expect(todo).toHaveTextContent("Clean room");
+
+      await act(() => user.click(button));
+
+      // startTransition will avoid rendering the suspense fallback for already
+      // revealed content if the state update inside the transition causes the
+      // component to suspend.
+      //
+      // Here we should not see the suspense fallback while the component suspends
+      // until the todo is finished loading. Seeing the suspense fallback is an
+      // indication that we are suspending the component too late in the process.
+      expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+
+      // We can ensure this works with isPending from useTransition in the process
+      expect(todo).toHaveAttribute("aria-busy", "true");
+
+      // Ensure we are showing the stale UI until the new todo has loaded
+      expect(todo).toHaveTextContent("Clean room");
+
+      // Eventually we should see the updated todo content once its done
+      // suspending.
+      await waitFor(() => {
+        expect(todo).toHaveTextContent("Clean room (completed)");
+      });
+    });
+  });
+
+  describe("fetchMore", () => {
+    function getItemTexts() {
+      return screen.getAllByTestId(/letter/).map(
+        // eslint-disable-next-line testing-library/no-node-access
+        (li) => li.firstChild!.textContent
+      );
+    }
+    it("re-suspends when calling `fetchMore` with different variables", async () => {
+      const { renders } = renderPaginatedIntegrationTest();
+
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      const items = await screen.findAllByTestId(/letter/i);
+      expect(items).toHaveLength(2);
+      expect(getItemTexts()).toStrictEqual(["A", "B"]);
+
+      const button = screen.getByText("Fetch more");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      // parent component re-suspends
+      expect(renders.suspenseCount).toBe(2);
+      await waitFor(() => {
+        expect(renders.count).toBe(2);
+      });
+
+      expect(getItemTexts()).toStrictEqual(["C", "D"]);
+    });
+    it("properly uses `updateQuery` when calling `fetchMore`", async () => {
+      const { renders } = renderPaginatedIntegrationTest({
+        updateQuery: true,
+      });
+
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      const items = await screen.findAllByTestId(/letter/i);
+
+      expect(items).toHaveLength(2);
+      expect(getItemTexts()).toStrictEqual(["A", "B"]);
+
+      const button = screen.getByText("Fetch more");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      // parent component re-suspends
+      expect(renders.suspenseCount).toBe(2);
+      await waitFor(() => {
+        expect(renders.count).toBe(2);
+      });
+
+      const moreItems = await screen.findAllByTestId(/letter/i);
+      expect(moreItems).toHaveLength(4);
+      expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]);
+    });
+    it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => {
+      const { renders } = renderPaginatedIntegrationTest({
+        fieldPolicies: true,
+      });
+      expect(renders.suspenseCount).toBe(1);
+      expect(screen.getByText("loading")).toBeInTheDocument();
+
+      const items = await screen.findAllByTestId(/letter/i);
+
+      expect(items).toHaveLength(2);
+      expect(getItemTexts()).toStrictEqual(["A", "B"]);
+
+      const button = screen.getByText("Fetch more");
+      const user = userEvent.setup();
+      await act(() => user.click(button));
+
+      // parent component re-suspends
+      expect(renders.suspenseCount).toBe(2);
+      await waitFor(() => {
+        expect(renders.count).toBe(2);
+      });
+
+      const moreItems = await screen.findAllByTestId(/letter/i);
+      expect(moreItems).toHaveLength(4);
+      expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]);
+    });
+    it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => {
+      type Variables = {
+        offset: number;
+      };
+
+      interface Todo {
+        __typename: "Todo";
+        id: string;
+        name: string;
+        completed: boolean;
+      }
+      interface Data {
+        todos: Todo[];
+      }
+      const user = userEvent.setup();
+
+      const query: TypedDocumentNode<Data, Variables> = gql`
+        query TodosQuery($offset: Int!) {
+          todos(offset: $offset) {
+            id
+            name
+            completed
+          }
+        }
+      `;
+
+      const mocks: MockedResponse<Data, Variables>[] = [
+        {
+          request: { query, variables: { offset: 0 } },
+          result: {
+            data: {
+              todos: [
+                {
+                  __typename: "Todo",
+                  id: "1",
+                  name: "Clean room",
+                  completed: false,
+                },
+              ],
+            },
+          },
+          delay: 10,
+        },
+        {
+          request: { query, variables: { offset: 1 } },
+          result: {
+            data: {
+              todos: [
+                {
+                  __typename: "Todo",
+                  id: "2",
+                  name: "Take out trash",
+                  completed: true,
+                },
+              ],
+            },
+          },
+          delay: 10,
+        },
+      ];
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache: new InMemoryCache({
+          typePolicies: {
+            Query: {
+              fields: {
+                todos: offsetLimitPagination(),
+              },
+            },
+          },
+        }),
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [queryRef, { fetchMore }] = useBackgroundQuery(query, {
+          variables: { offset: 0 },
+        });
+        const onFetchMoreHandler = (variables: Variables) => {
+          fetchMore({ variables });
+        };
+        return <Todo onFetchMore={onFetchMoreHandler} queryRef={queryRef} />;
+      }
+
+      function Todo({
+        queryRef,
+        onFetchMore,
+      }: {
+        onFetchMore: (variables: Variables) => void;
+        queryRef: QueryReference<Data>;
+      }) {
+        const { data } = useReadQuery(queryRef);
+        const [isPending, startTransition] = React.useTransition();
+        const { todos } = data;
+
+        return (
+          <>
+            <button
+              onClick={() => {
+                startTransition(() => {
+                  onFetchMore({ offset: 1 });
+                });
+              }}
+            >
+              Load more
+            </button>
+            <div data-testid="todos" aria-busy={isPending}>
+              {todos.map((todo) => (
+                <div data-testid={`todo:${todo.id}`} key={todo.id}>
+                  {todo.name}
+                  {todo.completed && " (completed)"}
+                </div>
+              ))}
+            </div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(screen.getByText("Loading")).toBeInTheDocument();
+
+      expect(await screen.findByTestId("todos")).toBeInTheDocument();
+
+      const todos = screen.getByTestId("todos");
+      const todo1 = screen.getByTestId("todo:1");
+      const button = screen.getByText("Load more");
+
+      expect(todo1).toBeInTheDocument();
+
+      await act(() => user.click(button));
+
+      // startTransition will avoid rendering the suspense fallback for already
+      // revealed content if the state update inside the transition causes the
+      // component to suspend.
+      //
+      // Here we should not see the suspense fallback while the component suspends
+      // until the todo is finished loading. Seeing the suspense fallback is an
+      // indication that we are suspending the component too late in the process.
+      expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+
+      // We can ensure this works with isPending from useTransition in the process
+      expect(todos).toHaveAttribute("aria-busy", "true");
+
+      // Ensure we are showing the stale UI until the new todo has loaded
+      expect(todo1).toHaveTextContent("Clean room");
+
+      // Eventually we should see the updated todos content once its done
+      // suspending.
+      await waitFor(() => {
+        expect(screen.getByTestId("todo:2")).toHaveTextContent(
+          "Take out trash (completed)"
+        );
+        expect(todo1).toHaveTextContent("Clean room");
+      });
+    });
+
+    it('honors refetchWritePolicy set to "merge"', async () => {
+      const user = userEvent.setup();
+
+      const query: TypedDocumentNode<
+        { primes: number[] },
+        { min: number; max: number }
+      > = gql`
+        query GetPrimes($min: number, $max: number) {
+          primes(min: $min, max: $max)
+        }
+      `;
+
+      interface QueryData {
+        primes: number[];
+      }
+
+      const mocks = [
+        {
+          request: { query, variables: { min: 0, max: 12 } },
+          result: { data: { primes: [2, 3, 5, 7, 11] } },
+        },
+        {
+          request: { query, variables: { min: 12, max: 30 } },
+          result: { data: { primes: [13, 17, 19, 23, 29] } },
+          delay: 10,
+        },
+      ];
+
+      const mergeParams: [number[] | undefined, number[]][] = [];
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              primes: {
+                keyArgs: false,
+                merge(existing: number[] | undefined, incoming: number[]) {
+                  mergeParams.push([existing, incoming]);
+                  return existing ? existing.concat(incoming) : incoming;
+                },
+              },
+            },
+          },
+        },
+      });
+
+      function SuspenseFallback() {
+        return <div>loading</div>;
+      }
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache,
+      });
+
+      function Child({
+        refetch,
+        queryRef,
+      }: {
+        refetch: (
+          variables?: Partial<OperationVariables> | undefined
+        ) => Promise<ApolloQueryResult<QueryData>>;
+        queryRef: QueryReference<QueryData>;
+      }) {
+        const { data, error, networkStatus } = useReadQuery(queryRef);
+
+        return (
+          <div>
+            <button
+              onClick={() => {
+                refetch({ min: 12, max: 30 });
+              }}
+            >
+              Refetch
+            </button>
+            <div data-testid="primes">{data?.primes.join(", ")}</div>
+            <div data-testid="network-status">{networkStatus}</div>
+            <div data-testid="error">{error?.message || "undefined"}</div>
+          </div>
+        );
+      }
+
+      function Parent() {
+        const [queryRef, { refetch }] = useBackgroundQuery(query, {
+          variables: { min: 0, max: 12 },
+          refetchWritePolicy: "merge",
+        });
+        return <Child refetch={refetch} queryRef={queryRef} />;
+      }
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      render(<App />);
+
+      await waitFor(() => {
+        expect(screen.getByTestId("primes")).toHaveTextContent(
+          "2, 3, 5, 7, 11"
+        );
+      });
+      expect(screen.getByTestId("network-status")).toHaveTextContent(
+        "7" // ready
+      );
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+      expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
+
+      await act(() => user.click(screen.getByText("Refetch")));
+
+      await waitFor(() => {
+        expect(screen.getByTestId("primes")).toHaveTextContent(
+          "2, 3, 5, 7, 11, 13, 17, 19, 23, 29"
+        );
+      });
+      expect(screen.getByTestId("network-status")).toHaveTextContent(
+        "7" // ready
+      );
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+      expect(mergeParams).toEqual([
+        [undefined, [2, 3, 5, 7, 11]],
+        [
+          [2, 3, 5, 7, 11],
+          [13, 17, 19, 23, 29],
+        ],
+      ]);
+    });
+
+    it('defaults refetchWritePolicy to "overwrite"', async () => {
+      const user = userEvent.setup();
+
+      const query: TypedDocumentNode<
+        { primes: number[] },
+        { min: number; max: number }
+      > = gql`
+        query GetPrimes($min: number, $max: number) {
+          primes(min: $min, max: $max)
+        }
+      `;
+
+      interface QueryData {
+        primes: number[];
+      }
+
+      const mocks = [
+        {
+          request: { query, variables: { min: 0, max: 12 } },
+          result: { data: { primes: [2, 3, 5, 7, 11] } },
+        },
+        {
+          request: { query, variables: { min: 12, max: 30 } },
+          result: { data: { primes: [13, 17, 19, 23, 29] } },
+          delay: 10,
+        },
+      ];
+
+      const mergeParams: [number[] | undefined, number[]][] = [];
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              primes: {
+                keyArgs: false,
+                merge(existing: number[] | undefined, incoming: number[]) {
+                  mergeParams.push([existing, incoming]);
+                  return existing ? existing.concat(incoming) : incoming;
+                },
+              },
+            },
+          },
+        },
+      });
+
+      function SuspenseFallback() {
+        return <div>loading</div>;
+      }
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache,
+      });
+
+      function Child({
+        refetch,
+        queryRef,
+      }: {
+        refetch: (
+          variables?: Partial<OperationVariables> | undefined
+        ) => Promise<ApolloQueryResult<QueryData>>;
+        queryRef: QueryReference<QueryData>;
+      }) {
+        const { data, error, networkStatus } = useReadQuery(queryRef);
+
+        return (
+          <div>
+            <button
+              onClick={() => {
+                refetch({ min: 12, max: 30 });
+              }}
+            >
+              Refetch
+            </button>
+            <div data-testid="primes">{data?.primes.join(", ")}</div>
+            <div data-testid="network-status">{networkStatus}</div>
+            <div data-testid="error">{error?.message || "undefined"}</div>
+          </div>
+        );
+      }
+
+      function Parent() {
+        const [queryRef, { refetch }] = useBackgroundQuery(query, {
+          variables: { min: 0, max: 12 },
+        });
+        return <Child refetch={refetch} queryRef={queryRef} />;
+      }
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      render(<App />);
+
+      await waitFor(() => {
+        expect(screen.getByTestId("primes")).toHaveTextContent(
+          "2, 3, 5, 7, 11"
+        );
+      });
+      expect(screen.getByTestId("network-status")).toHaveTextContent(
+        "7" // ready
+      );
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+      expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
+
+      await act(() => user.click(screen.getByText("Refetch")));
+
+      await waitFor(() => {
+        expect(screen.getByTestId("primes")).toHaveTextContent(
+          "13, 17, 19, 23, 29"
+        );
+      });
+      expect(screen.getByTestId("network-status")).toHaveTextContent(
+        "7" // ready
+      );
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+      expect(mergeParams).toEqual([
+        [undefined, [2, 3, 5, 7, 11]],
+        [undefined, [13, 17, 19, 23, 29]],
+      ]);
+    });
+
+    it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => {
+      interface Data {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      const fullQuery: TypedDocumentNode<Data> = gql`
+        query {
+          character {
+            id
+            name
+          }
+        }
+      `;
+
+      const partialQuery = gql`
+        query {
+          character {
+            id
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query: fullQuery },
+          result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+        },
+      ];
+
+      interface Renders {
+        errors: Error[];
+        errorCount: number;
+        suspenseCount: number;
+        count: number;
+      }
+      const renders: Renders = {
+        errors: [],
+        errorCount: 0,
+        suspenseCount: 0,
+        count: 0,
+      };
+
+      const cache = new InMemoryCache();
+
+      cache.writeQuery({
+        query: partialQuery,
+        data: { character: { id: "1" } },
+      });
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache,
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        renders.suspenseCount++;
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [queryRef] = useBackgroundQuery(fullQuery, {
+          fetchPolicy: "cache-first",
+          returnPartialData: true,
+        });
+        return <Todo queryRef={queryRef} />;
+      }
+
+      function Todo({
+        queryRef,
+      }: {
+        queryRef: QueryReference<DeepPartial<Data>>;
+      }) {
+        const { data, networkStatus, error } = useReadQuery(queryRef);
+        renders.count++;
+
+        return (
+          <>
+            <div data-testid="character-id">{data.character?.id}</div>
+            <div data-testid="character-name">{data.character?.name}</div>
+            <div data-testid="network-status">{networkStatus}</div>
+            <div data-testid="error">{error?.message || "undefined"}</div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(renders.suspenseCount).toBe(0);
+      expect(screen.getByTestId("character-id")).toHaveTextContent("1");
+      expect(screen.getByTestId("character-name")).toHaveTextContent("");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      await waitFor(() => {
+        expect(screen.getByTestId("character-name")).toHaveTextContent(
+          "Doctor Strange"
+        );
+      });
+      expect(screen.getByTestId("character-id")).toHaveTextContent("1");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      expect(renders.count).toBe(2);
+      expect(renders.suspenseCount).toBe(0);
+    });
+
+    it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => {
+      const partialQuery = gql`
+        query ($id: ID!) {
+          character(id: $id) {
+            id
+          }
+        }
+      `;
+
+      const cache = new InMemoryCache();
+
+      cache.writeQuery({
+        query: partialQuery,
+        data: { character: { id: "1" } },
+        variables: { id: "1" },
+      });
+
+      const { renders, mocks, rerender } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+        cache,
+        options: {
+          fetchPolicy: "cache-first",
+          returnPartialData: true,
+        },
+      });
+      expect(renders.suspenseCount).toBe(0);
+
+      expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument();
+
+      rerender({ variables: { id: "2" } });
+
+      expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument();
+
+      expect(renders.frames[2]).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+
+      expect(renders.count).toBe(3);
+      expect(renders.suspenseCount).toBe(1);
+      expect(renders.frames).toMatchObject([
+        {
+          data: { character: { id: "1" } },
+          networkStatus: NetworkStatus.loading,
+          error: undefined,
+        },
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          ...mocks[1].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    });
+
+    it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => {
+      interface Data {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      const fullQuery: TypedDocumentNode<Data> = gql`
+        query {
+          character {
+            id
+            name
+          }
+        }
+      `;
+
+      const partialQuery = gql`
+        query {
+          character {
+            id
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query: fullQuery },
+          result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+        },
+      ];
+
+      interface Renders {
+        errors: Error[];
+        errorCount: number;
+        suspenseCount: number;
+        count: number;
+        frames: {
+          data: DeepPartial<Data>;
+          networkStatus: NetworkStatus;
+          error: ApolloError | undefined;
+        }[];
+      }
+      const renders: Renders = {
+        errors: [],
+        errorCount: 0,
+        suspenseCount: 0,
+        count: 0,
+        frames: [],
+      };
+
+      const cache = new InMemoryCache();
+
+      cache.writeQuery({
+        query: partialQuery,
+        data: { character: { id: "1" } },
+      });
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache,
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        renders.suspenseCount++;
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [queryRef] = useBackgroundQuery(fullQuery, {
+          fetchPolicy: "network-only",
+          returnPartialData: true,
+        });
+
+        return <Todo queryRef={queryRef} />;
+      }
+
+      function Todo({
+        queryRef,
+      }: {
+        queryRef: QueryReference<DeepPartial<Data>>;
+      }) {
+        const { data, networkStatus, error } = useReadQuery(queryRef);
+        renders.frames.push({ data, networkStatus, error });
+        renders.count++;
+        return (
+          <>
+            <div data-testid="character-id">{data.character?.id}</div>
+            <div data-testid="character-name">{data.character?.name}</div>
+            <div data-testid="network-status">{networkStatus}</div>
+            <div data-testid="error">{error?.message || "undefined"}</div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(renders.suspenseCount).toBe(1);
+
+      await waitFor(() => {
+        expect(screen.getByTestId("character-name")).toHaveTextContent(
+          "Doctor Strange"
+        );
+      });
+      expect(screen.getByTestId("character-id")).toHaveTextContent("1");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      expect(renders.count).toBe(1);
+      expect(renders.suspenseCount).toBe(1);
+
+      expect(renders.frames).toMatchObject([
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    });
+
+    it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => {
+      const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
+      interface Data {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      const fullQuery: TypedDocumentNode<Data> = gql`
+        query {
+          character {
+            id
+            name
+          }
+        }
+      `;
+
+      const partialQuery = gql`
+        query {
+          character {
+            id
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query: fullQuery },
+          result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+        },
+      ];
+
+      interface Renders {
+        errors: Error[];
+        errorCount: number;
+        suspenseCount: number;
+        count: number;
+        frames: {
+          data: DeepPartial<Data>;
+          networkStatus: NetworkStatus;
+          error: ApolloError | undefined;
+        }[];
+      }
+      const renders: Renders = {
+        errors: [],
+        errorCount: 0,
+        suspenseCount: 0,
+        count: 0,
+        frames: [],
+      };
+
+      const cache = new InMemoryCache();
+
+      cache.writeQuery({
+        query: partialQuery,
+        data: { character: { id: "1" } },
+      });
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache,
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        renders.suspenseCount++;
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [queryRef] = useBackgroundQuery(fullQuery, {
+          fetchPolicy: "no-cache",
+          returnPartialData: true,
+        });
+
+        return <Todo queryRef={queryRef} />;
+      }
+
+      function Todo({
+        queryRef,
+      }: {
+        queryRef: QueryReference<DeepPartial<Data>>;
+      }) {
+        const { data, networkStatus, error } = useReadQuery(queryRef);
+        renders.frames.push({ data, networkStatus, error });
+        renders.count++;
+        return (
+          <>
+            <div data-testid="character-id">{data.character?.id}</div>
+            <div data-testid="character-name">{data.character?.name}</div>
+            <div data-testid="network-status">{networkStatus}</div>
+            <div data-testid="error">{error?.message || "undefined"}</div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(renders.suspenseCount).toBe(1);
+
+      await waitFor(() => {
+        expect(screen.getByTestId("character-name")).toHaveTextContent(
+          "Doctor Strange"
+        );
+      });
+      expect(screen.getByTestId("character-id")).toHaveTextContent("1");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      expect(renders.count).toBe(1);
+      expect(renders.suspenseCount).toBe(1);
+
+      expect(renders.frames).toMatchObject([
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+
+      consoleSpy.mockRestore();
+    });
+
+    it('warns when using returnPartialData with a "no-cache" fetch policy', async () => {
+      const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
+
+      const query: TypedDocumentNode<SimpleQueryData> = gql`
+        query UserQuery {
+          greeting
+        }
+      `;
+      const mocks = [
+        {
+          request: { query },
+          result: { data: { greeting: "Hello" } },
+        },
+      ];
+
+      renderSuspenseHook(
+        () =>
+          useBackgroundQuery(query, {
+            fetchPolicy: "no-cache",
+            returnPartialData: true,
+          }),
+        { mocks }
+      );
+
+      expect(console.warn).toHaveBeenCalledTimes(1);
+      expect(console.warn).toHaveBeenCalledWith(
+        "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy."
+      );
+
+      consoleSpy.mockRestore();
+    });
+
+    it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => {
+      interface Data {
+        character: {
+          id: string;
+          name: string;
+        };
+      }
+
+      const fullQuery: TypedDocumentNode<Data> = gql`
+        query {
+          character {
+            id
+            name
+          }
+        }
+      `;
+
+      const partialQuery = gql`
+        query {
+          character {
+            id
+          }
+        }
+      `;
+      const mocks = [
+        {
+          request: { query: fullQuery },
+          result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+        },
+      ];
+
+      interface Renders {
+        errors: Error[];
+        errorCount: number;
+        suspenseCount: number;
+        count: number;
+        frames: {
+          data: DeepPartial<Data>;
+          networkStatus: NetworkStatus;
+          error: ApolloError | undefined;
+        }[];
+      }
+      const renders: Renders = {
+        errors: [],
+        errorCount: 0,
+        suspenseCount: 0,
+        count: 0,
+        frames: [],
+      };
+
+      const cache = new InMemoryCache();
+
+      cache.writeQuery({
+        query: partialQuery,
+        data: { character: { id: "1" } },
+      });
+
+      const client = new ApolloClient({
+        link: new MockLink(mocks),
+        cache,
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        renders.suspenseCount++;
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [queryRef] = useBackgroundQuery(fullQuery, {
+          fetchPolicy: "cache-and-network",
+          returnPartialData: true,
+        });
+
+        return <Todo queryRef={queryRef} />;
+      }
+
+      function Todo({
+        queryRef,
+      }: {
+        queryRef: QueryReference<DeepPartial<Data>>;
+      }) {
+        const { data, networkStatus, error } = useReadQuery(queryRef);
+        renders.frames.push({ data, networkStatus, error });
+        renders.count++;
+        return (
+          <>
+            <div data-testid="character-id">{data.character?.id}</div>
+            <div data-testid="character-name">{data.character?.name}</div>
+            <div data-testid="network-status">{networkStatus}</div>
+            <div data-testid="error">{error?.message || "undefined"}</div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(renders.suspenseCount).toBe(0);
+      expect(screen.getByTestId("character-id")).toHaveTextContent("1");
+      // name is not present yet, since it's missing in partial data
+      expect(screen.getByTestId("character-name")).toHaveTextContent("");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      await waitFor(() => {
+        expect(screen.getByTestId("character-name")).toHaveTextContent(
+          "Doctor Strange"
+        );
+      });
+      expect(screen.getByTestId("character-id")).toHaveTextContent("1");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      expect(renders.count).toBe(2);
+      expect(renders.suspenseCount).toBe(0);
+
+      expect(renders.frames).toMatchObject([
+        {
+          data: { character: { id: "1" } },
+          networkStatus: NetworkStatus.loading,
+          error: undefined,
+        },
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    });
+
+    it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => {
+      const partialQuery = gql`
+        query ($id: ID!) {
+          character(id: $id) {
+            id
+          }
+        }
+      `;
+
+      const cache = new InMemoryCache();
+
+      cache.writeQuery({
+        query: partialQuery,
+        data: { character: { id: "1" } },
+        variables: { id: "1" },
+      });
+
+      const { renders, mocks, rerender } = renderVariablesIntegrationTest({
+        variables: { id: "1" },
+        cache,
+        options: {
+          fetchPolicy: "cache-and-network",
+          returnPartialData: true,
+        },
+      });
+
+      expect(renders.suspenseCount).toBe(0);
+
+      expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument();
+
+      rerender({ variables: { id: "2" } });
+
+      expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument();
+
+      expect(renders.count).toBe(3);
+      expect(renders.suspenseCount).toBe(1);
+      expect(renders.frames).toMatchObject([
+        {
+          data: { character: { id: "1" } },
+          networkStatus: NetworkStatus.loading,
+          error: undefined,
+        },
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          ...mocks[1].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    });
+
+    it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => {
+      interface QueryData {
+        greeting: {
+          __typename: string;
+          message?: string;
+          recipient?: {
+            __typename: string;
+            name: string;
+          };
+        };
+      }
+
+      const query: TypedDocumentNode<QueryData> = gql`
+        query {
+          greeting {
+            message
+            ... on Greeting @defer {
+              recipient {
+                name
+              }
+            }
+          }
+        }
+      `;
+
+      const link = new MockSubscriptionLink();
+      const cache = new InMemoryCache();
+
+      // We are intentionally writing partial data to the cache. Supress console
+      // warnings to avoid unnecessary noise in the test.
+      const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+      cache.writeQuery({
+        query,
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+      });
+      consoleSpy.mockRestore();
+
+      interface Renders {
+        errors: Error[];
+        errorCount: number;
+        suspenseCount: number;
+        count: number;
+        frames: {
+          data: DeepPartial<QueryData>;
+          networkStatus: NetworkStatus;
+          error: ApolloError | undefined;
+        }[];
+      }
+      const renders: Renders = {
+        errors: [],
+        errorCount: 0,
+        suspenseCount: 0,
+        count: 0,
+        frames: [],
+      };
+
+      const client = new ApolloClient({
+        link,
+        cache,
+      });
+
+      function App() {
+        return (
+          <ApolloProvider client={client}>
+            <Suspense fallback={<SuspenseFallback />}>
+              <Parent />
+            </Suspense>
+          </ApolloProvider>
+        );
+      }
+
+      function SuspenseFallback() {
+        renders.suspenseCount++;
+        return <p>Loading</p>;
+      }
+
+      function Parent() {
+        const [queryRef] = useBackgroundQuery(query, {
+          fetchPolicy: "cache-first",
+          returnPartialData: true,
+        });
+
+        return <Todo queryRef={queryRef} />;
+      }
+
+      function Todo({
+        queryRef,
+      }: {
+        queryRef: QueryReference<DeepPartial<QueryData>>;
+      }) {
+        const { data, networkStatus, error } = useReadQuery(queryRef);
+        renders.frames.push({ data, networkStatus, error });
+        renders.count++;
+        return (
+          <>
+            <div data-testid="message">{data.greeting?.message}</div>
+            <div data-testid="recipient">{data.greeting?.recipient?.name}</div>
+            <div data-testid="network-status">{networkStatus}</div>
+            <div data-testid="error">{error?.message || "undefined"}</div>
+          </>
+        );
+      }
+
+      render(<App />);
+
+      expect(renders.suspenseCount).toBe(0);
+      expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice");
+      // message is not present yet, since it's missing in partial data
+      expect(screen.getByTestId("message")).toHaveTextContent("");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      link.simulateResult({
+        result: {
+          data: {
+            greeting: { message: "Hello world", __typename: "Greeting" },
+          },
+          hasNext: true,
+        },
+      });
+
+      await waitFor(() => {
+        expect(screen.getByTestId("message")).toHaveTextContent("Hello world");
+      });
+      expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      link.simulateResult({
+        result: {
+          incremental: [
+            {
+              data: {
+                __typename: "Greeting",
+                recipient: { name: "Alice", __typename: "Person" },
+              },
+              path: ["greeting"],
+            },
+          ],
+          hasNext: false,
+        },
+      });
+
+      await waitFor(() => {
+        expect(screen.getByTestId("recipient").textContent).toEqual("Alice");
+      });
+      expect(screen.getByTestId("message")).toHaveTextContent("Hello world");
+      expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready
+      expect(screen.getByTestId("error")).toHaveTextContent("undefined");
+
+      expect(renders.count).toBe(3);
+      expect(renders.suspenseCount).toBe(0);
+      expect(renders.frames).toMatchObject([
+        {
+          data: {
+            greeting: {
+              __typename: "Greeting",
+              recipient: { __typename: "Person", name: "Cached Alice" },
+            },
+          },
+          networkStatus: NetworkStatus.loading,
+          error: undefined,
+        },
+        {
+          data: {
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Cached Alice" },
+            },
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          data: {
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    });
+  });
+
+  describe.skip("type tests", () => {
+    it("returns unknown when TData cannot be inferred", () => {
+      const query = gql`
+        query {
+          hello
+        }
+      `;
+
+      const [queryRef] = useBackgroundQuery(query);
+      const { data } = useReadQuery(queryRef);
+
+      expectTypeOf(data).toEqualTypeOf<unknown>();
+    });
+
+    it("disallows wider variables type than specified", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      // @ts-expect-error should not allow wider TVariables type
+      useBackgroundQuery(query, { variables: { id: "1", foo: "bar" } });
+    });
+
+    it("returns TData in default case", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query);
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData | undefined>();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query);
+
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData | undefined>();
+    });
+
+    it('returns TData | undefined with errorPolicy: "ignore"', () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        errorPolicy: "ignore",
+      });
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        errorPolicy: "ignore",
+      });
+
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it('returns TData | undefined with errorPolicy: "all"', () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        errorPolicy: "all",
+      });
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const [explicitQueryRef] = useBackgroundQuery(query, {
+        errorPolicy: "all",
+      });
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it('returns TData with errorPolicy: "none"', () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        errorPolicy: "none",
+      });
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData | undefined>();
+
+      const [explicitQueryRef] = useBackgroundQuery(query, {
+        errorPolicy: "none",
+      });
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData | undefined>();
+    });
+
+    it("returns DeepPartial<TData> with returnPartialData: true", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        returnPartialData: true,
+      });
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: true,
+      });
+
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns TData with returnPartialData: false", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        returnPartialData: false,
+      });
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: false,
+      });
+
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+    });
+
+    it("returns TData when passing an option that does not affect TData", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        fetchPolicy: "no-cache",
+      });
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        fetchPolicy: "no-cache",
+      });
+
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+    });
+
+    it("handles combinations of options", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredPartialDataIgnoreQueryRef] = useBackgroundQuery(query, {
+        returnPartialData: true,
+        errorPolicy: "ignore",
+      });
+      const { data: inferredPartialDataIgnore } = useReadQuery(
+        inferredPartialDataIgnoreQueryRef
+      );
+
+      expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(
+        inferredPartialDataIgnore
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const [explicitPartialDataIgnoreQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: true,
+        errorPolicy: "ignore",
+      });
+
+      const { data: explicitPartialDataIgnore } = useReadQuery(
+        explicitPartialDataIgnoreQueryRef
+      );
+
+      expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(
+        explicitPartialDataIgnore
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const [inferredPartialDataNoneQueryRef] = useBackgroundQuery(query, {
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      const { data: inferredPartialDataNone } = useReadQuery(
+        inferredPartialDataNoneQueryRef
+      );
+
+      expectTypeOf(inferredPartialDataNone).toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+      expectTypeOf(
+        inferredPartialDataNone
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const [explicitPartialDataNoneQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      const { data: explicitPartialDataNone } = useReadQuery(
+        explicitPartialDataNoneQueryRef
+      );
+
+      expectTypeOf(explicitPartialDataNone).toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+      expectTypeOf(
+        explicitPartialDataNone
+      ).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns correct TData type when combined options that do not affect TData", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        fetchPolicy: "no-cache",
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+      const { data: inferred } = useReadQuery(inferredQueryRef);
+
+      expectTypeOf(inferred).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        fetchPolicy: "no-cache",
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      const { data: explicit } = useReadQuery(explicitQueryRef);
+
+      expectTypeOf(explicit).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns QueryReference<TData> | undefined when `skip` is present", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, {
+        skip: true,
+      });
+
+      expectTypeOf(inferredQueryRef).toEqualTypeOf<
+        QueryReference<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(inferredQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData>
+      >();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, { skip: true });
+
+      expectTypeOf(explicitQueryRef).toEqualTypeOf<
+        QueryReference<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(explicitQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData>
+      >();
+
+      // TypeScript is too smart and using a `const` or `let` boolean variable
+      // for the `skip` option results in a false positive. Using an options
+      // object allows us to properly check for a dynamic case.
+      const options = {
+        skip: true,
+      };
+
+      const [dynamicQueryRef] = useBackgroundQuery(query, {
+        skip: options.skip,
+      });
+
+      expectTypeOf(dynamicQueryRef).toEqualTypeOf<
+        QueryReference<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(dynamicQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData>
+      >();
+    });
+
+    it("returns `undefined` when using `skipToken` unconditionally", () => {
+      const { query } = useVariablesIntegrationTestCase();
+
+      const [inferredQueryRef] = useBackgroundQuery(query, skipToken);
+
+      expectTypeOf(inferredQueryRef).toEqualTypeOf<undefined>();
+      expectTypeOf(inferredQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData> | undefined
+      >();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, skipToken);
+
+      expectTypeOf(explicitQueryRef).toEqualTypeOf<undefined>();
+      expectTypeOf(explicitQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData> | undefined
+      >();
+    });
+
+    it("returns QueryReference<TData> | undefined when using conditional `skipToken`", () => {
+      const { query } = useVariablesIntegrationTestCase();
+      const options = {
+        skip: true,
+      };
+
+      const [inferredQueryRef] = useBackgroundQuery(
+        query,
+        options.skip ? skipToken : undefined
+      );
+
+      expectTypeOf(inferredQueryRef).toEqualTypeOf<
+        QueryReference<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(inferredQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData>
+      >();
+
+      const [explicitQueryRef] = useBackgroundQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, options.skip ? skipToken : undefined);
+
+      expectTypeOf(explicitQueryRef).toEqualTypeOf<
+        QueryReference<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(explicitQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData>
+      >();
+    });
+
+    it("returns QueryReference<DeepPartial<TData>> | undefined when using `skipToken` with `returnPartialData`", () => {
+      const { query } = useVariablesIntegrationTestCase();
+      const options = {
+        skip: true,
+      };
+
+      const [inferredQueryRef] = useBackgroundQuery(
+        query,
+        options.skip ? skipToken : { returnPartialData: true }
+      );
+
+      expectTypeOf(inferredQueryRef).toEqualTypeOf<
+        QueryReference<DeepPartial<VariablesCaseData>> | undefined
+      >();
+      expectTypeOf(inferredQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData>
+      >();
+
+      const [explicitQueryRef] = useBackgroundQuery<VariablesCaseData>(
+        query,
+        options.skip ? skipToken : { returnPartialData: true }
+      );
+
+      expectTypeOf(explicitQueryRef).toEqualTypeOf<
+        QueryReference<DeepPartial<VariablesCaseData>> | undefined
+      >();
+      expectTypeOf(explicitQueryRef).not.toEqualTypeOf<
+        QueryReference<VariablesCaseData>
+      >();
+    });
+  });
+});
diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx
--- a/src/react/hooks/__tests__/useFragment.test.tsx
+++ b/src/react/hooks/__tests__/useFragment.test.tsx
@@ -1,9 +1,15 @@
 import * as React from "react";
-import { render, waitFor, screen, renderHook } from "@testing-library/react";
-import userEvent from '@testing-library/user-event';
+import {
+  render,
+  waitFor,
+  screen,
+  renderHook,
+  within,
+} from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
 import { act } from "react-dom/test-utils";
 
-import { useFragment_experimental as useFragment } from "../useFragment";
+import { UseFragmentOptions, useFragment } from "../useFragment";
 import { MockedProvider } from "../../../testing";
 import { ApolloProvider } from "../../context";
 import {
@@ -14,8 +20,15 @@ import {
   ApolloClient,
   Observable,
   ApolloLink,
+  StoreObject,
+  DocumentNode,
+  FetchResult,
 } from "../../../core";
 import { useQuery } from "../useQuery";
+import { concatPagination } from "../../../utilities";
+import assert from "assert";
+import { expectTypeOf } from "expect-type";
+import { SubscriptionObserver } from "zen-observable-ts";
 
 describe("useFragment", () => {
   it("is importable and callable", () => {
@@ -83,7 +96,7 @@ describe("useFragment", () => {
           { __typename: "Item", id: 5 },
         ],
       },
-    })
+    });
 
     const renders: string[] = [];
 
@@ -92,9 +105,7 @@ describe("useFragment", () => {
       const { loading, data } = useQuery(listQuery);
       expect(loading).toBe(false);
       return (
-        <ol>
-          {data!.list.map(item => <Item key={item.id} id={item.id}/>)}
-        </ol>
+        <ol>{data?.list.map((item) => <Item key={item.id} id={item.id} />)}</ol>
       );
     }
 
@@ -108,7 +119,7 @@ describe("useFragment", () => {
           id: props.id,
         },
       });
-      return <li>{complete ? data!.text : "incomplete"}</li>;
+      return <li>{complete ? data.text : "incomplete"}</li>;
     }
 
     render(
@@ -120,24 +131,15 @@ describe("useFragment", () => {
     function getItemTexts() {
       return screen.getAllByText(/^Item/).map(
         // eslint-disable-next-line testing-library/no-node-access
-        li => li.firstChild!.textContent
+        (li) => li.firstChild!.textContent
       );
     }
 
     await waitFor(() => {
-      expect(getItemTexts()).toEqual([
-        "Item #1",
-        "Item #2",
-        "Item #5",
-      ]);
+      expect(getItemTexts()).toEqual(["Item #1", "Item #2", "Item #5"]);
     });
 
-    expect(renders).toEqual([
-      "list",
-      "item 1",
-      "item 2",
-      "item 5",
-    ]);
+    expect(renders).toEqual(["list", "item 1", "item 2", "item 5"]);
 
     act(() => {
       cache.writeFragment({
@@ -151,11 +153,7 @@ describe("useFragment", () => {
     });
 
     await waitFor(() => {
-      expect(getItemTexts()).toEqual([
-        "Item #1",
-        "Item #2 updated",
-        "Item #5",
-      ]);
+      expect(getItemTexts()).toEqual(["Item #1", "Item #2 updated", "Item #5"]);
     });
 
     expect(renders).toEqual([
@@ -170,7 +168,7 @@ describe("useFragment", () => {
     act(() => {
       cache.modify({
         fields: {
-          list(list: Reference[], { readField }) {
+          list(list: readonly Reference[], { readField }) {
             return [
               ...list,
               cache.writeFragment({
@@ -189,10 +187,11 @@ describe("useFragment", () => {
                   text: "Item #4 from cache.modify",
                 },
               })!,
-            ].sort((ref1, ref2) => (
-              readField<Item["id"]>("id", ref1)! -
-              readField<Item["id"]>("id", ref2)!
-            ));
+            ].sort(
+              (ref1, ref2) =>
+                readField<Item["id"]>("id", ref1)! -
+                readField<Item["id"]>("id", ref2)!
+            );
           },
         },
       });
@@ -295,15 +294,416 @@ describe("useFragment", () => {
         ],
       },
       __META: {
-        extraRootIds: [
-          "Item:2",
-          "Item:3",
-          "Item:4",
-        ],
+        extraRootIds: ["Item:2", "Item:3", "Item:4"],
       },
     });
   });
 
+  it("returns data on first render", () => {
+    const ItemFragment: TypedDocumentNode<Item> = gql`
+      fragment ItemFragment on Item {
+        id
+        text
+      }
+    `;
+    const cache = new InMemoryCache();
+    const item = { __typename: "Item", id: 1, text: "Item #1" };
+    cache.writeFragment({
+      fragment: ItemFragment,
+      data: item,
+    });
+    const client = new ApolloClient({
+      cache,
+    });
+    function Component() {
+      const { data } = useFragment({
+        fragment: ItemFragment,
+        from: { __typename: "Item", id: 1 },
+      });
+      return <>{data.text}</>;
+    }
+    render(
+      <ApolloProvider client={client}>
+        <Component />
+      </ApolloProvider>
+    );
+
+    // would throw if not present synchronously
+    screen.getByText(/Item #1/);
+  });
+
+  it.each<TypedDocumentNode<{ list: Item[] }>>([
+    // This query uses a basic field-level @nonreactive directive.
+    gql`
+      query GetItems {
+        list {
+          id
+          text @nonreactive
+        }
+      }
+    `,
+    // This query uses @nonreactive on an anonymous/inline ...spread directive.
+    gql`
+      query GetItems {
+        list {
+          id
+          ... @nonreactive {
+            text
+          }
+        }
+      }
+    `,
+    // This query uses @nonreactive on a ...spread with a type condition.
+    gql`
+      query GetItems {
+        list {
+          id
+          ... on Item @nonreactive {
+            text
+          }
+        }
+      }
+    `,
+    // This query uses @nonreactive directive on a named fragment ...spread.
+    gql`
+      query GetItems {
+        list {
+          id
+          ...ItemText @nonreactive
+        }
+      }
+      fragment ItemText on Item {
+        text
+      }
+    `,
+  ])(
+    "Parent list component can use @nonreactive to avoid rerendering",
+    async (query) => {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              list: concatPagination(),
+            },
+          },
+          Item: {
+            keyFields: ["id"],
+            // Configuring keyArgs:false for Item.text is one way to prevent field
+            // keys like text@nonreactive, but it's not the only way. Since
+            // @nonreactive is now in the KNOWN_DIRECTIVES array defined in
+            // utilities/graphql/storeUtils.ts, the '@nonreactive' suffix won't be
+            // automatically appended to field keys by default.
+            // fields: {
+            //   text: {
+            //     keyArgs: false,
+            //   },
+            // },
+          },
+        },
+      });
+
+      const client = new ApolloClient({
+        cache,
+        link: ApolloLink.empty(),
+      });
+
+      const renders: string[] = [];
+
+      function List() {
+        const { data } = useQuery(query);
+
+        renders.push("list");
+
+        return (
+          <ul>
+            {data?.list.map((item) => <Item key={item.id} item={item} />)}
+          </ul>
+        );
+      }
+
+      function Item({ item }: { item: Item }) {
+        const { data } = useFragment({
+          fragment: ItemFragment,
+          fragmentName: "ItemFragment",
+          from: item,
+        });
+
+        renders.push(`item ${item.id}`);
+
+        if (!data) return null;
+
+        return <li>{`Item #${item.id}: ${data.text}`}</li>;
+      }
+
+      act(() => {
+        cache.writeQuery({
+          query,
+          data: {
+            list: [
+              { __typename: "Item", id: 1, text: "first" },
+              { __typename: "Item", id: 2, text: "second" },
+              { __typename: "Item", id: 3, text: "third" },
+            ],
+          },
+        });
+      });
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          list: [
+            { __ref: 'Item:{"id":1}' },
+            { __ref: 'Item:{"id":2}' },
+            { __ref: 'Item:{"id":3}' },
+          ],
+        },
+        'Item:{"id":1}': {
+          __typename: "Item",
+          id: 1,
+          text: "first",
+        },
+        'Item:{"id":2}': {
+          __typename: "Item",
+          id: 2,
+          text: "second",
+        },
+        'Item:{"id":3}': {
+          __typename: "Item",
+          id: 3,
+          text: "third",
+        },
+      });
+
+      render(
+        <ApolloProvider client={client}>
+          <List />
+        </ApolloProvider>
+      );
+
+      function getItemTexts() {
+        return screen.getAllByText(/Item #\d+/).map((el) => el.textContent);
+      }
+
+      await waitFor(() => {
+        expect(getItemTexts()).toEqual([
+          "Item #1: first",
+          "Item #2: second",
+          "Item #3: third",
+        ]);
+      });
+
+      expect(renders).toEqual(["list", "item 1", "item 2", "item 3"]);
+
+      function appendLyToText(id: number) {
+        act(() => {
+          cache.modify({
+            id: cache.identify({ __typename: "Item", id })!,
+            fields: {
+              text(existing) {
+                return existing + "ly";
+              },
+            },
+          });
+        });
+      }
+
+      appendLyToText(2);
+
+      await waitFor(() => {
+        expect(renders).toEqual([
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 2",
+        ]);
+
+        expect(getItemTexts()).toEqual([
+          "Item #1: first",
+          "Item #2: secondly",
+          "Item #3: third",
+        ]);
+      });
+
+      appendLyToText(1);
+
+      await waitFor(() => {
+        expect(renders).toEqual([
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 2",
+          "item 1",
+        ]);
+
+        expect(getItemTexts()).toEqual([
+          "Item #1: firstly",
+          "Item #2: secondly",
+          "Item #3: third",
+        ]);
+      });
+
+      appendLyToText(3);
+
+      await waitFor(() => {
+        expect(renders).toEqual([
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 2",
+          "item 1",
+          "item 3",
+        ]);
+
+        expect(getItemTexts()).toEqual([
+          "Item #1: firstly",
+          "Item #2: secondly",
+          "Item #3: thirdly",
+        ]);
+      });
+
+      act(() => {
+        cache.writeQuery({
+          query,
+          data: {
+            list: [
+              { __typename: "Item", id: 4, text: "fourth" },
+              { __typename: "Item", id: 5, text: "fifth" },
+            ],
+          },
+        });
+      });
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          list: [
+            { __ref: 'Item:{"id":1}' },
+            { __ref: 'Item:{"id":2}' },
+            { __ref: 'Item:{"id":3}' },
+            { __ref: 'Item:{"id":4}' },
+            { __ref: 'Item:{"id":5}' },
+          ],
+        },
+        'Item:{"id":1}': {
+          __typename: "Item",
+          id: 1,
+          text: "firstly",
+        },
+        'Item:{"id":2}': {
+          __typename: "Item",
+          id: 2,
+          text: "secondly",
+        },
+        'Item:{"id":3}': {
+          __typename: "Item",
+          id: 3,
+          text: "thirdly",
+        },
+        'Item:{"id":4}': {
+          __typename: "Item",
+          id: 4,
+          text: "fourth",
+        },
+        'Item:{"id":5}': {
+          __typename: "Item",
+          id: 5,
+          text: "fifth",
+        },
+      });
+
+      await waitFor(() => {
+        expect(renders).toEqual([
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 2",
+          "item 1",
+          "item 3",
+          // The whole list had to be rendered again to append 4 and 5
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 4",
+          "item 5",
+        ]);
+
+        expect(getItemTexts()).toEqual([
+          "Item #1: firstly",
+          "Item #2: secondly",
+          "Item #3: thirdly",
+          "Item #4: fourth",
+          "Item #5: fifth",
+        ]);
+      });
+
+      appendLyToText(5);
+
+      await waitFor(() => {
+        expect(renders).toEqual([
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 2",
+          "item 1",
+          "item 3",
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 4",
+          "item 5",
+          // A single new render:
+          "item 5",
+        ]);
+
+        expect(getItemTexts()).toEqual([
+          "Item #1: firstly",
+          "Item #2: secondly",
+          "Item #3: thirdly",
+          "Item #4: fourth",
+          "Item #5: fifthly",
+        ]);
+      });
+
+      appendLyToText(4);
+
+      await waitFor(() => {
+        expect(renders).toEqual([
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 2",
+          "item 1",
+          "item 3",
+          "list",
+          "item 1",
+          "item 2",
+          "item 3",
+          "item 4",
+          "item 5",
+          "item 5",
+          // A single new render:
+          "item 4",
+        ]);
+
+        expect(getItemTexts()).toEqual([
+          "Item #1: firstly",
+          "Item #2: secondly",
+          "Item #3: thirdly",
+          "Item #4: fourthly",
+          "Item #5: fifthly",
+        ]);
+      });
+    }
+  );
+
   it("List can use useFragment with ListFragment", async () => {
     const cache = new InMemoryCache({
       typePolicies: {
@@ -338,7 +738,7 @@ describe("useFragment", () => {
         ],
         extra: "from ListFragment",
       },
-    })
+    });
 
     const renders: string[] = [];
 
@@ -349,9 +749,12 @@ describe("useFragment", () => {
         from: { __typename: "Query" },
       });
       expect(complete).toBe(true);
+      assert(!!complete);
       return (
         <ol>
-          {data!.list.map(item => <Item key={item.id} id={item.id}/>)}
+          {data.list.map((item) => (
+            <Item key={item.id} id={item.id} />
+          ))}
         </ol>
       );
     }
@@ -365,7 +768,7 @@ describe("useFragment", () => {
           id: props.id,
         },
       });
-      return <li>{complete ? data!.text : "incomplete"}</li>;
+      return <li>{complete ? data.text : "incomplete"}</li>;
     }
 
     render(
@@ -377,24 +780,15 @@ describe("useFragment", () => {
     function getItemTexts() {
       return screen.getAllByText(/^Item/).map(
         // eslint-disable-next-line testing-library/no-node-access
-        li => li.firstChild!.textContent
+        (li) => li.firstChild!.textContent
       );
     }
 
     await waitFor(() => {
-      expect(getItemTexts()).toEqual([
-        "Item #1",
-        "Item #2",
-        "Item #5",
-      ]);
+      expect(getItemTexts()).toEqual(["Item #1", "Item #2", "Item #5"]);
     });
 
-    expect(renders).toEqual([
-      "list",
-      "item 1",
-      "item 2",
-      "item 5",
-    ]);
+    expect(renders).toEqual(["list", "item 1", "item 2", "item 5"]);
 
     act(() => {
       cache.writeFragment({
@@ -408,11 +802,7 @@ describe("useFragment", () => {
     });
 
     await waitFor(() => {
-      expect(getItemTexts()).toEqual([
-        "Item #1",
-        "Item #2 updated",
-        "Item #5",
-      ]);
+      expect(getItemTexts()).toEqual(["Item #1", "Item #2 updated", "Item #5"]);
     });
 
     expect(renders).toEqual([
@@ -427,7 +817,7 @@ describe("useFragment", () => {
     act(() => {
       cache.modify({
         fields: {
-          list(list: Reference[], { readField }) {
+          list(list: readonly Reference[], { readField }) {
             return [
               ...list,
               cache.writeFragment({
@@ -444,10 +834,11 @@ describe("useFragment", () => {
                   id: 4,
                 },
               })!,
-            ].sort((ref1, ref2) => (
-              readField<Item["id"]>("id", ref1)! -
-              readField<Item["id"]>("id", ref2)!
-            ));
+            ].sort(
+              (ref1, ref2) =>
+                readField<Item["id"]>("id", ref1)! -
+                readField<Item["id"]>("id", ref2)!
+            );
           },
         },
       });
@@ -550,11 +941,7 @@ describe("useFragment", () => {
         extra: "from ListFragment",
       },
       __META: {
-        extraRootIds: [
-          "Item:2",
-          "Item:3",
-          "Item:4",
-        ],
+        extraRootIds: ["Item:2", "Item:3", "Item:4"],
       },
     });
   });
@@ -572,9 +959,9 @@ describe("useFragment", () => {
               // filtering explicitly here, so this test won't be broken.
               return items && items.filter(canRead);
             },
-          }
-        }
-      }
+          },
+        },
+      },
     });
 
     const wrapper = ({ children }: any) => (
@@ -609,19 +996,18 @@ describe("useFragment", () => {
     `;
 
     const { result: renderResult } = renderHook(
-      () => useFragment({
-        fragment: ListAndItemFragments,
-        fragmentName: "ListFragment",
-        from: { __typename: "Query" },
-        returnPartialData: true,
-      }),
-      { wrapper },
+      () =>
+        useFragment({
+          fragment: ListAndItemFragments,
+          fragmentName: "ListFragment",
+          from: { __typename: "Query" },
+        }),
+      { wrapper }
     );
 
     function checkHistory(expectedResultCount: number) {
       // Temporarily disabling this check until we can come up with a better
       // (more opt-in) system than result.previousResult.previousResult...
-
       // function historyToArray(
       //   result: UseFragmentResult<QueryData>,
       // ): UseFragmentResult<QueryData>[] {
@@ -634,7 +1020,6 @@ describe("useFragment", () => {
       // const all = historyToArray(renderResult.current);
       // expect(all.length).toBe(expectedResultCount);
       // expect(all).toEqual(renderResult.all);
-
       // if (renderResult.current.complete) {
       //   expect(renderResult.current).toBe(
       //     renderResult.current.lastCompleteResult
@@ -669,8 +1054,8 @@ describe("useFragment", () => {
       });
     });
 
+    await waitFor(() => expect(renderResult.current.data).toEqual(data125));
     expect(renderResult.current.complete).toBe(false);
-    expect(renderResult.current.data).toEqual(data125);
     expect(renderResult.current.missing).toEqual({
       list: {
         // Even though Query.list is actually an array in the data, data paths
@@ -704,38 +1089,44 @@ describe("useFragment", () => {
       });
     });
 
+    await waitFor(() =>
+      expect(renderResult.current.data).toEqual(data182WithText)
+    );
     expect(renderResult.current.complete).toBe(true);
-    expect(renderResult.current.data).toEqual(data182WithText);
     expect(renderResult.current.missing).toBeUndefined();
 
     checkHistory(3);
 
-    await act(async () => cache.batch({
-      update(cache) {
-        cache.evict({
-          id: cache.identify({
-            __typename: "Item",
-            id: 8,
-          }),
-        });
-
-        cache.evict({
-          id: cache.identify({
-            __typename: "Item",
-            id: 2,
-          }),
-          fieldName: "text",
-        });
-      },
-    }));
+    await act(async () =>
+      cache.batch({
+        update(cache) {
+          cache.evict({
+            id: cache.identify({
+              __typename: "Item",
+              id: 8,
+            }),
+          });
+
+          cache.evict({
+            id: cache.identify({
+              __typename: "Item",
+              id: 2,
+            }),
+            fieldName: "text",
+          });
+        },
+      })
+    );
 
+    await waitFor(() =>
+      expect(renderResult.current.data).toEqual({
+        list: [
+          { __typename: "Item", id: 1, text: "oyez1" },
+          { __typename: "Item", id: 2 },
+        ],
+      })
+    );
     expect(renderResult.current.complete).toBe(false);
-    expect(renderResult.current.data).toEqual({
-      list: [
-        { __typename: "Item", id: 1, text: "oyez1" },
-        { __typename: "Item", id: 2 },
-      ],
-    });
     expect(renderResult.current.missing).toEqual({
       // TODO Figure out why Item:8 is not represented here. Likely because of
       // auto-filtering of dangling references from arrays, but that should
@@ -765,11 +1156,7 @@ describe("useFragment", () => {
       },
       ROOT_QUERY: {
         __typename: "Query",
-        list: [
-          { __ref: "Item:1" },
-          { __ref: "Item:8" },
-          { __ref: "Item:2" },
-        ],
+        list: [{ __ref: "Item:1" }, { __ref: "Item:8" }, { __ref: "Item:2" }],
       },
     });
 
@@ -799,27 +1186,29 @@ describe("useFragment", () => {
 
     const client = new ApolloClient({
       cache,
-      link: new ApolloLink(operation => new Observable(observer => {
-        if (operation.operationName === "ListQueryWithItemFragment") {
-          setTimeout(() => {
-            observer.next({
-              data: {
-                list: [
-                  { __typename: "Item", id: 1 },
-                  { __typename: "Item", id: 2 },
-                  { __typename: "Item", id: 5 },
-                ],
-              }
-            });
-            observer.complete();
-          }, 10);
-        } else {
-          observer.error(`unexpected query ${
-            operation.operationName ||
-            operation.query
-          }`);
-        }
-      })),
+      link: new ApolloLink(
+        (operation) =>
+          new Observable((observer) => {
+            if (operation.operationName === "ListQueryWithItemFragment") {
+              setTimeout(() => {
+                observer.next({
+                  data: {
+                    list: [
+                      { __typename: "Item", id: 1 },
+                      { __typename: "Item", id: 2 },
+                      { __typename: "Item", id: 5 },
+                    ],
+                  },
+                });
+                observer.complete();
+              }, 10);
+            } else {
+              observer.error(
+                `unexpected query ${operation.operationName || operation.query}`
+              );
+            }
+          })
+      ),
     });
 
     const listQuery: TypedDocumentNode<QueryData> = gql`
@@ -843,16 +1232,24 @@ describe("useFragment", () => {
 
       return complete ? (
         <>
-          <select onChange={(e) => {
-            setCurrentItem(parseInt(e.currentTarget.value))
-          }}>
-            {data!.list.map(item => <option key={item.id} value={item.id}>Select item {item.id}</option>)}
+          <select
+            onChange={(e) => {
+              setCurrentItem(parseInt(e.currentTarget.value));
+            }}
+          >
+            {data.list.map((item) => (
+              <option key={item.id} value={item.id}>
+                Select item {item.id}
+              </option>
+            ))}
           </select>
           <div>
             <Item id={currentItem} />
           </div>
           <ol>
-          {data!.list.map(item => <Item key={item.id} id={item.id}/>)}
+            {data.list.map((item) => (
+              <Item key={item.id} id={item.id} />
+            ))}
           </ol>
         </>
       ) : null;
@@ -866,7 +1263,7 @@ describe("useFragment", () => {
           id,
         },
       });
-      return <li>{complete ? data!.text : "incomplete"}</li>;
+      return <li>{complete ? data.text : "incomplete"}</li>;
     }
 
     render(
@@ -878,7 +1275,7 @@ describe("useFragment", () => {
     function getItemTexts() {
       return screen.getAllByText(/^Item/).map(
         // eslint-disable-next-line testing-library/no-node-access
-        li => li.firstChild!.textContent
+        (li) => li.firstChild!.textContent
       );
     }
 
@@ -896,8 +1293,8 @@ describe("useFragment", () => {
     // Select "Item #2" via <select />
     const user = userEvent.setup();
     await user.selectOptions(
-      screen.getByRole('combobox'),
-      screen.getByRole('option', { name: 'Select item 2' })
+      screen.getByRole("combobox"),
+      screen.getByRole("option", { name: "Select item 2" })
     );
 
     await waitFor(() => {
@@ -923,11 +1320,12 @@ describe("useFragment", () => {
 
     beforeEach(() => {
       cache = new InMemoryCache();
-
-      wrapper = ({ children }: any) => <MockedProvider cache={cache}>{children}</MockedProvider>;
+      wrapper = ({ children }: any) => (
+        <MockedProvider cache={cache}>{children}</MockedProvider>
+      );
 
       // silence the console for the incomplete fragment write
-      const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
+      const spy = jest.spyOn(console, "error").mockImplementation(() => {});
       cache.writeFragment({
         fragment: ItemFragment,
         data: {
@@ -951,54 +1349,353 @@ describe("useFragment", () => {
       expect(result.current.data).toEqual({ __typename: "Item", id: 5 });
       expect(result.current.complete).toBe(false);
     });
+  });
 
-    it("throws an exception with `returnPartialData: false` if only partial data is available", () => {
-      // this is actually not intended behavior, but it is the current behavior
-      // let's document it in a test until we remove `returnPartialData` in 3.8
+  describe("return value `complete` property", () => {
+    let cache: InMemoryCache, wrapper: React.FunctionComponent;
+    const ItemFragment = gql`
+      fragment ItemFragment on Item {
+        id
+        text
+      }
+    `;
 
-      let error: Error;
+    beforeEach(() => {
+      cache = new InMemoryCache();
+      wrapper = ({ children }: any) => (
+        <MockedProvider cache={cache}>{children}</MockedProvider>
+      );
+    });
 
-      renderHook(
-        () => {
-          // we can't just `expect(() => renderHook(...)).toThrow(...)` because it will render a second time, resulting in an uncaught exception
-          try {
-            useFragment({
-              fragment: ItemFragment,
-              from: { __typename: "Item", id: 5 },
-              returnPartialData: false,
-            });
-          } catch (e) {
-            error = e;
-          }
+    test("if all data is available, `complete` is `true`", () => {
+      cache.writeFragment({
+        fragment: ItemFragment,
+        data: {
+          __typename: "Item",
+          id: 5,
+          text: "Item #5",
+        },
+      });
+
+      const { result } = renderHook(
+        () =>
+          useFragment({
+            fragment: ItemFragment,
+            from: { __typename: "Item", id: 5 },
+          }),
+        { wrapper }
+      );
+
+      expect(result.current).toStrictEqual({
+        data: { __typename: "Item", id: 5, text: "Item #5" },
+        complete: true,
+      });
+    });
+
+    test("if only partial data is available, `complete` is `false`", () => {
+      cache.writeFragment({
+        fragment: ItemFragment,
+        data: {
+          __typename: "Item",
+          id: 5,
         },
+      });
+
+      const { result } = renderHook(
+        () =>
+          useFragment({
+            fragment: ItemFragment,
+            from: { __typename: "Item", id: 5 },
+          }),
         { wrapper }
       );
 
-      expect(error!.toString()).toMatch(`Error: Can't find field 'text' on Item:5 object`);
-    });
-
-    it("throws an exception with `returnPartialData: false` if no data is available", () => {
-      // this is actually not intended behavior, but it is the current behavior
-      // let's document it in a test until we remove `returnPartialData` in 3.8
-      let error: Error;
-
-      renderHook(
-        () => {
-          // we can't just `expect(() => renderHook(...)).toThrow(...)` because it will render a second time, resulting in an uncaught exception
-          try {
-            useFragment({
-              fragment: ItemFragment,
-              from: { __typename: "Item", id: 6 },
-              returnPartialData: false,
-            });
-          } catch (e) {
-            error = e;
-          }
+      expect(result.current).toStrictEqual({
+        data: { __typename: "Item", id: 5 },
+        complete: false,
+        missing: {
+          text: "Can't find field 'text' on Item:5 object",
         },
+      });
+    });
+
+    test("if no data is available, `complete` is `false`", () => {
+      const { result } = renderHook(
+        () =>
+          useFragment({
+            fragment: ItemFragment,
+            from: { __typename: "Item", id: 5 },
+          }),
         { wrapper }
       );
 
-      expect(error!.toString()).toMatch(`Error: Dangling reference to missing Item:6 object`);
+      expect(result.current).toStrictEqual({
+        data: {},
+        complete: false,
+        missing: "Dangling reference to missing Item:5 object",
+      });
+    });
+  });
+});
+
+describe("has the same timing as `useQuery`", () => {
+  const itemFragment = gql`
+    fragment ItemFragment on Item {
+      id
+      title
+    }
+  `;
+
+  it("both in same component", async () => {
+    const initialItem = { __typename: "Item", id: 1, title: "Item #initial" };
+    const updatedItem = { __typename: "Item", id: 1, title: "Item #updated" };
+
+    const query = gql`
+      query {
+        item {
+          ...ItemFragment
+        }
+      }
+      ${itemFragment}
+    `;
+    let observer: SubscriptionObserver<FetchResult>;
+    const cache = new InMemoryCache();
+    const client = new ApolloClient({
+      cache,
+      link: new ApolloLink(
+        (operation) => new Observable((o) => void (observer = o))
+      ),
+    });
+
+    function Component() {
+      const { data: queryData } = useQuery(query, { returnPartialData: true });
+      const { data: fragmentData, complete } = useFragment({
+        fragment: itemFragment,
+        from: initialItem,
+      });
+
+      if (!queryData) {
+        expect(fragmentData).toStrictEqual({});
+      } else {
+        expect({ item: fragmentData }).toStrictEqual(queryData);
+      }
+      return complete ? JSON.stringify(fragmentData) : "loading";
+    }
+    render(<Component />, {
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
+    });
+    await screen.findByText(/loading/);
+    assert(observer!);
+    observer.next({ data: { item: initialItem } });
+    observer.complete();
+    await screen.findByText(/Item #initial/);
+    cache.writeQuery({ query, data: { item: updatedItem } });
+    await screen.findByText(/Item #updated/);
+    await new Promise((resolve) => setTimeout(resolve, 50));
+  });
+
+  it("`useQuery` in parent, `useFragment` in child", async () => {
+    const item1 = { __typename: "Item", id: 1, title: "Item #1" };
+    const item2 = { __typename: "Item", id: 2, title: "Item #2" };
+    const query: TypedDocumentNode<{ items: Array<typeof item1> }> = gql`
+      query {
+        items {
+          ...ItemFragment
+        }
+      }
+      ${itemFragment}
+    `;
+    const cache = new InMemoryCache();
+    const client = new ApolloClient({
+      cache,
+    });
+    cache.writeQuery({ query, data: { items: [item1, item2] } });
+
+    const valuePairs: Array<
+      [item: string, parentCount: number, childCount: number]
+    > = [];
+    function captureDOMState() {
+      const parent = screen.getByTestId("parent");
+      const children = screen.getByTestId("children");
+      valuePairs.push([
+        "Item 1",
+        within(parent).queryAllByText(/Item #1/).length,
+        within(children).queryAllByText(/Item #1/).length,
+      ]);
+      valuePairs.push([
+        "Item 2",
+        within(parent).queryAllByText(/Item #2/).length,
+        within(children).queryAllByText(/Item #2/).length,
+      ]);
+    }
+
+    function Parent() {
+      const { data } = useQuery(query);
+      if (!data) throw new Error("should never happen");
+      React.useEffect(captureDOMState);
+      return (
+        <>
+          <div data-testid="parent">
+            <p>{JSON.stringify(data)}</p>
+          </div>
+          <div data-testid="children">
+            {data.items.map((item, i) => (
+              <p key={i}>
+                <Child id={item.id} />
+              </p>
+            ))}
+          </div>
+        </>
+      );
+    }
+    function Child({ id }: { id: number }) {
+      const { data } = useFragment({
+        fragment: itemFragment,
+        from: { __typename: "Item", id },
+      });
+      React.useEffect(captureDOMState);
+      return <>{JSON.stringify({ item: data })}</>;
+    }
+
+    render(<Parent />, {
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
+    });
+    cache.evict({
+      id: cache.identify(item2),
+    });
+    await waitFor(() => {
+      expect(() => screen.getByText(/Item #2/)).toThrow();
+    });
+
+    for (const [_item, parentChount, childCount] of valuePairs) {
+      expect(parentChount).toBe(childCount);
+    }
+  });
+
+  /**
+   * This would be optimal, but would only work if `useFragment` and
+   * `useQuery` had exactly the same timing, which is not the case with
+   * the current implementation.
+   * The best we can do is to make sure that `useFragment` is not
+   * faster than `useQuery` in reasonable cases (of course, `useQuery`
+   * could trigger a network request on cache update, which would be slower
+   * than `useFragment`, no matter how much we delay it).
+   * If we change the core implementation into a more synchronous one,
+   * we should try to get this test to work, too.
+   */
+  it.failing("`useFragment` in parent, `useQuery` in child", async () => {
+    const item1 = { __typename: "Item", id: 1, title: "Item #1" };
+    const item2 = { __typename: "Item", id: 2, title: "Item #2" };
+    const query: TypedDocumentNode<{ items: Array<typeof item1> }> = gql`
+      query {
+        items {
+          ...ItemFragment
+        }
+      }
+      ${itemFragment}
+    `;
+    const cache = new InMemoryCache();
+    const client = new ApolloClient({
+      cache,
+    });
+    cache.writeQuery({ query, data: { items: [item1, item2] } });
+
+    const valuePairs: Array<
+      [item: string, parentCount: number, childCount: number]
+    > = [];
+    function captureDOMState() {
+      const parent = screen.getByTestId("parent");
+      const children = screen.getByTestId("children");
+      valuePairs.push([
+        "Item 1",
+        within(parent).queryAllByText(/Item #1/).length,
+        within(children).queryAllByText(/Item #1/).length,
+      ]);
+      valuePairs.push([
+        "Item 2",
+        within(parent).queryAllByText(/Item #2/).length,
+        within(children).queryAllByText(/Item #2/).length,
+      ]);
+    }
+
+    function Parent() {
+      const { data: data1 } = useFragment({
+        fragment: itemFragment,
+        from: { __typename: "Item", id: 1 },
+      });
+      const { data: data2 } = useFragment({
+        fragment: itemFragment,
+        from: { __typename: "Item", id: 2 },
+      });
+      React.useEffect(captureDOMState);
+      return (
+        <>
+          <div data-testid="parent">
+            <p>{JSON.stringify(data1)}</p>
+            <p>{JSON.stringify(data2)}</p>
+          </div>
+          <div data-testid="children">
+            <p>
+              <Child />
+            </p>
+          </div>
+        </>
+      );
+    }
+    function Child() {
+      const { data } = useQuery(query);
+      if (!data) throw new Error("should never happen");
+      React.useEffect(captureDOMState);
+      return <>{JSON.stringify(data)}</>;
+    }
+
+    render(<Parent />, {
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
     });
+    act(
+      () =>
+        void cache.evict({
+          id: cache.identify(item2),
+        })
+    );
+    await waitFor(() => {
+      expect(() => screen.getByText(/Item #2/)).toThrow();
+    });
+
+    for (const [_item, parentChount, childCount] of valuePairs) {
+      expect(parentChount).toBe(childCount);
+    }
+  });
+});
+
+describe.skip("Type Tests", () => {
+  test("NoInfer prevents adding arbitrary additional variables", () => {
+    const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;
+    useFragment({
+      fragment: typedNode,
+      from: { __typename: "Query" },
+      variables: {
+        bar: 4,
+        // @ts-expect-error
+        nonExistingVariable: "string",
+      },
+    });
+  });
+
+  test("UseFragmentOptions interface shape", <TData, TVars>() => {
+    expectTypeOf<UseFragmentOptions<TData, TVars>>().toEqualTypeOf<{
+      from: string | StoreObject | Reference;
+      fragment: DocumentNode | TypedDocumentNode<TData, TVars>;
+      fragmentName?: string;
+      optimistic?: boolean;
+      variables?: TVars;
+      canonizeResults?: boolean;
+    }>();
   });
 });
diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx
--- a/src/react/hooks/__tests__/useLazyQuery.test.tsx
+++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx
@@ -1,137 +1,135 @@
-import React from 'react';
-import { GraphQLError } from 'graphql';
-import gql from 'graphql-tag';
-import { act, renderHook, waitFor } from '@testing-library/react';
+import React from "react";
+import { GraphQLError } from "graphql";
+import gql from "graphql-tag";
+import { act, renderHook, waitFor } from "@testing-library/react";
 
-import { 
+import {
   ApolloClient,
   ApolloLink,
   ErrorPolicy,
   InMemoryCache,
   NetworkStatus,
-  TypedDocumentNode 
-} from '../../../core';
-import { Observable } from '../../../utilities';
-import { ApolloProvider, resetApolloContext } from '../../../react';
-import { 
+  TypedDocumentNode,
+} from "../../../core";
+import { Observable } from "../../../utilities";
+import { ApolloProvider } from "../../../react";
+import {
   MockedProvider,
   mockSingleLink,
   wait,
   tick,
-  MockSubscriptionLink 
-} from '../../../testing';
-import { useLazyQuery } from '../useLazyQuery';
-import { QueryResult } from '../../types/types';
+  MockSubscriptionLink,
+} from "../../../testing";
+import { useLazyQuery } from "../useLazyQuery";
+import { QueryResult } from "../../types/types";
 
 const IS_REACT_18 = React.version.startsWith("18");
 
-describe('useLazyQuery Hook', () => {
-  afterEach(() => {
-    resetApolloContext();
-  });
+describe("useLazyQuery Hook", () => {
   const helloQuery: TypedDocumentNode<{
     hello: string;
-  }> = gql`query { hello }`;
+  }> = gql`
+    query {
+      hello
+    }
+  `;
 
-  it('should hold query execution until manually triggered', async () => {
+  it("should hold query execution until manually triggered", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world' } },
+        result: { data: { hello: "world" } },
         delay: 20,
       },
     ];
-    const { result } = renderHook(
-      () => useLazyQuery(helloQuery),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useLazyQuery(helloQuery), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     expect(result.current[1].loading).toBe(false);
     expect(result.current[1].data).toBe(undefined);
     const execute = result.current[0];
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world" });
   });
 
-  it('should set `called` to false by default', async () => {
+  it("should set `called` to false by default", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world' } },
+        result: { data: { hello: "world" } },
         delay: 20,
       },
     ];
-    const { result } = renderHook(
-      () => useLazyQuery(helloQuery),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useLazyQuery(helloQuery), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     expect(result.current[1].loading).toBe(false);
     expect(result.current[1].data).toBe(undefined);
     expect(result.current[1].called).toBe(false);
   });
 
-  it('should set `called` to true after calling the lazy execute function', async () => {
+  it("should set `called` to true after calling the lazy execute function", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world' } },
+        result: { data: { hello: "world" } },
         delay: 20,
       },
     ];
 
-    const { result } = renderHook(
-      () => useLazyQuery(helloQuery),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useLazyQuery(helloQuery), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     expect(result.current[1].loading).toBe(false);
     expect(result.current[1].called).toBe(false);
     const execute = result.current[0];
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].called).toBe(true);
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].called).toBe(true);
   });
 
-  it('should override `skip` if lazy mode execution function is called', async () => {
+  it("should override `skip` if lazy mode execution function is called", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world' } },
+        result: { data: { hello: "world" } },
         delay: 20,
       },
     ];
@@ -141,11 +139,9 @@ describe('useLazyQuery Hook', () => {
       () => useLazyQuery(helloQuery, { skip: true } as any),
       {
         wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
         ),
-      },
+      }
     );
 
     expect(result.current[1].loading).toBe(false);
@@ -153,20 +149,26 @@ describe('useLazyQuery Hook', () => {
     const execute = result.current[0];
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].called).toBe(true);
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].called).toBe(true);
   });
 
-  it('should use variables defined in hook options (if any), when running the lazy execution function', async () => {
+  it("should use variables defined in hook options (if any), when running the lazy execution function", async () => {
     const query = gql`
-      query($id: number) {
+      query ($id: number) {
         hello(id: $id)
       }
     `;
@@ -174,41 +176,46 @@ describe('useLazyQuery Hook', () => {
     const mocks = [
       {
         request: { query, variables: { id: 1 } },
-        result: { data: { hello: 'world 1' } },
+        result: { data: { hello: "world 1" } },
         delay: 20,
       },
     ];
 
     const { result } = renderHook(
-      () => useLazyQuery(query, {
-        variables: { id: 1 },
-      }),
+      () =>
+        useLazyQuery(query, {
+          variables: { id: 1 },
+        }),
       {
         wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
         ),
-      },
+      }
     );
 
     const execute = result.current[0];
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
 
-    expect(result.current[1].data).toEqual({ hello: 'world 1' });
+    expect(result.current[1].data).toEqual({ hello: "world 1" });
   });
 
-  it('should use variables passed into lazy execution function, overriding similar variables defined in Hook options', async () => {
+  it("should use variables passed into lazy execution function, overriding similar variables defined in Hook options", async () => {
     const query = gql`
-      query($id: number) {
+      query ($id: number) {
         hello(id: $id)
       }
     `;
@@ -216,52 +223,60 @@ describe('useLazyQuery Hook', () => {
     const mocks = [
       {
         request: { query, variables: { id: 1 } },
-        result: { data: { hello: 'world 1' } },
+        result: { data: { hello: "world 1" } },
         delay: 20,
       },
       {
         request: { query, variables: { id: 2 } },
-        result: { data: { hello: 'world 2' } },
+        result: { data: { hello: "world 2" } },
         delay: 20,
       },
     ];
 
     const { result } = renderHook(
-      () => useLazyQuery(query, {
-        variables: { id: 1 },
-      }),
+      () =>
+        useLazyQuery(query, {
+          variables: { id: 1 },
+        }),
       {
         wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
         ),
-      },
+      }
     );
 
     const execute = result.current[0];
     setTimeout(() => execute({ variables: { id: 2 } }));
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world 2' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world 2" });
   });
 
-  it('should merge variables from original hook and execution function', async () => {
-    const counterQuery: TypedDocumentNode<{
-      counter: number;
-    }, {
-      hookVar?: boolean;
-      execVar?: boolean;
-      localDefaultVar?: boolean;
-      globalDefaultVar?: boolean;
-    }> = gql`
-      query GetCounter (
+  it("should merge variables from original hook and execution function", async () => {
+    const counterQuery: TypedDocumentNode<
+      {
+        counter: number;
+      },
+      {
+        hookVar?: boolean;
+        execVar?: boolean;
+        localDefaultVar?: boolean;
+        globalDefaultVar?: boolean;
+      }
+    > = gql`
+      query GetCounter(
         $hookVar: Boolean
         $execVar: Boolean
         $localDefaultVar: Boolean
@@ -282,23 +297,28 @@ describe('useLazyQuery Hook', () => {
         },
       },
       cache: new InMemoryCache(),
-      link: new ApolloLink(request => new Observable(observer => {
-        if (request.operationName === "GetCounter") {
-          observer.next({
-            data: {
-              counter: ++count,
-              vars: request.variables,
-            },
-          });
-          setTimeout(() => {
-            observer.complete();
-          }, 10);
-        } else {
-          observer.error(new Error(`Unknown query: ${
-            request.operationName || request.query
-          }`));
-        }
-      })),
+      link: new ApolloLink(
+        (request) =>
+          new Observable((observer) => {
+            if (request.operationName === "GetCounter") {
+              observer.next({
+                data: {
+                  counter: ++count,
+                  vars: request.variables,
+                },
+              });
+              setTimeout(() => {
+                observer.complete();
+              }, 10);
+            } else {
+              observer.error(
+                new Error(
+                  `Unknown query: ${request.operationName || request.query}`
+                )
+              );
+            }
+          })
+      ),
     });
 
     const { result } = renderHook(
@@ -321,22 +341,29 @@ describe('useLazyQuery Hook', () => {
       },
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
-    await waitFor(() => {
-      expect(result.current.query.loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current.query.called).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current.query.data).toBeUndefined();
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.query.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current.query.called).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current.query.data).toBeUndefined();
+      },
+      { interval: 1 }
+    );
 
     const expectedFinalData = {
       counter: 1,
@@ -348,32 +375,48 @@ describe('useLazyQuery Hook', () => {
       },
     };
 
-    const execResult = await result.current.exec(
-      {
-        variables: {
-          execVar: true
-        }
-      }
-    );
+    const execResult = await result.current.exec({
+      variables: {
+        execVar: true,
+      },
+    });
 
-    await waitFor(() => {
-      expect(execResult.loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(execResult.called).toBe(true);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(execResult.networkStatus).toBe(NetworkStatus.ready);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(execResult.data).toEqual(expectedFinalData);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current.query.called).toBe(true);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current.query.loading).toBe(false);
-    }, { interval: 10 });
+    await waitFor(
+      () => {
+        expect(execResult.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(execResult.called).toBe(true);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(execResult.networkStatus).toBe(NetworkStatus.ready);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(execResult.data).toEqual(expectedFinalData);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current.query.called).toBe(true);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current.query.loading).toBe(false);
+      },
+      { interval: 10 }
+    );
 
     expect(result.current.query.called).toBe(true);
     expect(result.current.query.data).toEqual(expectedFinalData);
@@ -393,20 +436,29 @@ describe('useLazyQuery Hook', () => {
       },
     });
 
-    await waitFor(() => {
-      expect(result.current.query.loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current.query.called).toBe(true);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current.query.data).toEqual({
-        counter: 2,
-        vars: {
-          execVar: false,
-        },
-      });
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.query.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current.query.called).toBe(true);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current.query.data).toEqual({
+          counter: 2,
+          vars: {
+            execVar: false,
+          },
+        });
+      },
+      { interval: 1 }
+    );
 
     const execResult2 = await result.current.exec({
       fetchPolicy: "cache-and-network",
@@ -416,29 +468,44 @@ describe('useLazyQuery Hook', () => {
       },
     });
 
-    await waitFor(() => {
-      expect(execResult2.loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(execResult2.called).toBe(true);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(execResult2.data).toEqual({
-        counter: 3,
-        vars: {
-          ...expectedFinalData.vars,
-          execVar: true,
-        },
-      });
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(execResult2.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(execResult2.called).toBe(true);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(execResult2.data).toEqual({
+          counter: 3,
+          vars: {
+            ...expectedFinalData.vars,
+            execVar: true,
+          },
+        });
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current.query.called).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.query.called).toBe(true);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current.query.loading).toBe(false);
-    }, { interval: 10 });
+    await waitFor(
+      () => {
+        expect(result.current.query.loading).toBe(false);
+      },
+      { interval: 10 }
+    );
     expect(result.current.query.called).toBe(true);
     expect(result.current.query.data).toEqual({
       counter: 3,
@@ -449,7 +516,6 @@ describe('useLazyQuery Hook', () => {
     });
   });
 
-
   it("changing queries", async () => {
     const query1 = gql`
       query {
@@ -465,12 +531,12 @@ describe('useLazyQuery Hook', () => {
       {
         request: { query: query1 },
         result: { data: { hello: "world" } },
-        delay: 20
+        delay: 20,
       },
       {
         request: { query: query2 },
         result: { data: { name: "changed" } },
-        delay: 20
+        delay: 20,
       },
     ];
 
@@ -526,80 +592,90 @@ describe('useLazyQuery Hook', () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world 1' } },
+        result: { data: { hello: "world 1" } },
         delay: 20,
       },
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world 2' } },
+        result: { data: { hello: "world 2" } },
         delay: 20,
       },
     ];
 
     const { result } = renderHook(
-      () => useLazyQuery(helloQuery, {
-        fetchPolicy: 'network-only',
-      }),
+      () =>
+        useLazyQuery(helloQuery, {
+          fetchPolicy: "network-only",
+        }),
       {
         wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
         ),
-      },
+      }
     );
 
     expect(result.current[1].loading).toBe(false);
     const execute = result.current[0];
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world 1' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world 1" });
 
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world 1' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world 1" });
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world 2' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world 2" });
   });
 
-  it('should persist previous data when a query is re-run', async () => {
+  it("should persist previous data when a query is re-run", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world 1' } },
+        result: { data: { hello: "world 1" } },
         delay: 20,
       },
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world 2' } },
+        result: { data: { hello: "world 2" } },
         delay: 20,
       },
     ];
 
     const { result } = renderHook(
-      () => useLazyQuery(helloQuery, {
-        notifyOnNetworkStatusChange: true,
-      }),
+      () =>
+        useLazyQuery(helloQuery, {
+          notifyOnNetworkStatusChange: true,
+        }),
       {
         wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
         ),
-      },
+      }
     );
 
     expect(result.current[1].loading).toBe(false);
@@ -608,35 +684,47 @@ describe('useLazyQuery Hook', () => {
     const execute = result.current[0];
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toBe(undefined);
     expect(result.current[1].previousData).toBe(undefined);
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world 1' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world 1" });
     expect(result.current[1].previousData).toBe(undefined);
 
     const refetch = result.current[1].refetch;
     setTimeout(() => refetch!());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world 1' });
-    expect(result.current[1].previousData).toEqual({ hello: 'world 1' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world 1" });
+    expect(result.current[1].previousData).toEqual({ hello: "world 1" });
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'world 2' });
-    expect(result.current[1].previousData).toEqual({ hello: 'world 1' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "world 2" });
+    expect(result.current[1].previousData).toEqual({ hello: "world 1" });
   });
 
-  it('should allow for the query to start with polling', async () => {
+  it("should allow for the query to start with polling", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
@@ -657,58 +745,79 @@ describe('useLazyQuery Hook', () => {
       <MockedProvider mocks={mocks}>{children}</MockedProvider>
     );
 
-    const { result } = renderHook(
-      () => useLazyQuery(helloQuery),
-      { wrapper },
+    const { result } = renderHook(() => useLazyQuery(helloQuery), { wrapper });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
     );
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
 
     expect(result.current[1].data).toBe(undefined);
 
     await tick();
     result.current[1].startPolling(10);
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      if (IS_REACT_18) {
-        expect(result.current[1].data).toEqual({ hello: "world 1" });
-      } else {
-        expect(result.current[1].data).toEqual({ hello: "world 3" });
-      }
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        if (IS_REACT_18) {
+          expect(result.current[1].data).toEqual({ hello: "world 1" });
+        } else {
+          expect(result.current[1].data).toEqual({ hello: "world 3" });
+        }
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      if (IS_REACT_18) {
-        expect(result.current[1].data).toEqual({ hello: "world 2" });
-      } else {
-        expect(result.current[1].data).toEqual({ hello: "world 3" });
-      }
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        if (IS_REACT_18) {
+          expect(result.current[1].data).toEqual({ hello: "world 2" });
+        } else {
+          expect(result.current[1].data).toEqual({ hello: "world 3" });
+        }
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current[1].data).toEqual({ hello: "world 3" });
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current[1].data).toEqual({ hello: "world 3" });
+      },
+      { interval: 1 }
+    );
 
     result.current[1].stopPolling();
   });
 
-  it('should persist previous data when a query is re-run and variable changes', async () => {
+  it("should persist previous data when a query is re-run and variable changes", async () => {
     const CAR_QUERY_BY_ID = gql`
-      query($id: Int) {
+      query ($id: Int) {
         car(id: $id) {
           make
           model
@@ -718,17 +827,17 @@ describe('useLazyQuery Hook', () => {
 
     const data1 = {
       car: {
-        make: 'Audi',
-        model: 'A4',
-        __typename: 'Car',
+        make: "Audi",
+        model: "A4",
+        __typename: "Car",
       },
     };
 
     const data2 = {
       car: {
-        make: 'Audi',
-        model: 'RS8',
-        __typename: 'Car',
+        make: "Audi",
+        model: "RS8",
+        __typename: "Car",
       },
     };
 
@@ -745,76 +854,79 @@ describe('useLazyQuery Hook', () => {
       },
     ];
 
-    const { result } = renderHook(
-      () => useLazyQuery(CAR_QUERY_BY_ID),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      }
-    );
+    const { result } = renderHook(() => useLazyQuery(CAR_QUERY_BY_ID), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     expect(result.current[1].loading).toBe(false);
     expect(result.current[1].data).toBe(undefined);
     expect(result.current[1].previousData).toBe(undefined);
     const execute = result.current[0];
-    setTimeout(() => execute({ variables: { id: 1 }}));
+    setTimeout(() => execute({ variables: { id: 1 } }));
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toBe(undefined);
     expect(result.current[1].previousData).toBe(undefined);
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toEqual(data1);
     expect(result.current[1].previousData).toBe(undefined);
 
-    setTimeout(() => execute({ variables: { id: 2 }}));
+    setTimeout(() => execute({ variables: { id: 2 } }));
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toBe(undefined);
     expect(result.current[1].previousData).toEqual(data1);
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toEqual(data2);
     expect(result.current[1].previousData).toEqual(data1);
   });
 
-  it('should work with cache-and-network fetch policy', async () => {
-    const cache = new InMemoryCache();
-    const link = mockSingleLink(
-      {
-        request: { query: helloQuery },
-        result: { data: { hello: 'from link' } },
-        delay: 20,
-      },
-    );
+  it("should work with cache-and-network fetch policy", async () => {
+    const cache = new InMemoryCache();
+    const link = mockSingleLink({
+      request: { query: helloQuery },
+      result: { data: { hello: "from link" } },
+      delay: 20,
+    });
 
     const client = new ApolloClient({
       link,
       cache,
     });
 
-    cache.writeQuery({ query: helloQuery, data: { hello: 'from cache' }});
+    cache.writeQuery({ query: helloQuery, data: { hello: "from cache" } });
 
     const { result } = renderHook(
-      () => useLazyQuery(helloQuery, { fetchPolicy: 'cache-and-network' }),
+      () => useLazyQuery(helloQuery, { fetchPolicy: "cache-and-network" }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(result.current[1].loading).toBe(false);
@@ -822,49 +934,53 @@ describe('useLazyQuery Hook', () => {
     const execute = result.current[0];
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
     // TODO: FIXME
-    expect(result.current[1].data).toEqual({ hello: 'from cache' });
+    expect(result.current[1].data).toEqual({ hello: "from cache" });
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    expect(result.current[1].data).toEqual({ hello: 'from link' });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    expect(result.current[1].data).toEqual({ hello: "from link" });
   });
 
-  it('should return a promise from the execution function which resolves with the result', async () => {
+  it("should return a promise from the execution function which resolves with the result", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'world' } },
+        result: { data: { hello: "world" } },
         delay: 20,
       },
     ];
-    const { result } = renderHook(
-      () => useLazyQuery(helloQuery),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useLazyQuery(helloQuery), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     expect(result.current[1].loading).toBe(false);
     expect(result.current[1].data).toBe(undefined);
     const execute = result.current[0];
 
-    const executeResult = new Promise<QueryResult<any, any>>(resolve => {
+    const executeResult = new Promise<QueryResult<any, any>>((resolve) => {
       setTimeout(() => resolve(execute()));
     });
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
     let latestRenderResult: QueryResult;
     await waitFor(() => {
@@ -873,15 +989,15 @@ describe('useLazyQuery Hook', () => {
     });
     await waitFor(() => {
       latestRenderResult = result.current[1];
-      expect(latestRenderResult.data).toEqual({ hello: 'world' });
+      expect(latestRenderResult.data).toEqual({ hello: "world" });
     });
 
-    return executeResult.then(finalResult => {
+    return executeResult.then((finalResult) => {
       expect(finalResult).toEqual(latestRenderResult);
     });
   });
 
-  it('should have matching results from execution function and hook', async () => {
+  it("should have matching results from execution function and hook", async () => {
     const query = gql`
       query GetCountries($filter: String) {
         countries(filter: $filter) {
@@ -928,16 +1044,11 @@ describe('useLazyQuery Hook', () => {
       },
     ];
 
-    const { result } = renderHook(
-      () => useLazyQuery(query),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useLazyQuery(query), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     expect(result.current[1].loading).toBe(false);
     expect(result.current[1].data).toBe(undefined);
@@ -947,13 +1058,19 @@ describe('useLazyQuery Hook', () => {
       executeResult = execute({ variables: { filter: "PA" } });
     });
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toEqual({
       countries: {
         code: "PA",
@@ -973,17 +1090,23 @@ describe('useLazyQuery Hook', () => {
       executeResult = execute({ variables: { filter: "BA" } });
     });
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current[1].data).toEqual({
-        countries: {
-          code: "BA",
-          name: "Bahamas",
-        },
-      });
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current[1].data).toEqual({
+          countries: {
+            code: "BA",
+            name: "Bahamas",
+          },
+        });
+      },
+      { interval: 1 }
+    );
 
     expect(executeResult).toBeInstanceOf(Promise);
     expect((await executeResult).data).toEqual({
@@ -994,34 +1117,29 @@ describe('useLazyQuery Hook', () => {
     });
   });
 
-  it('the promise should reject with errors the “way useMutation does”', async () => {
+  it("the promise should reject with errors the “way useMutation does”", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
         result: {
-          errors: [new GraphQLError('error 1')],
+          errors: [new GraphQLError("error 1")],
         },
         delay: 20,
       },
       {
         request: { query: helloQuery },
         result: {
-          errors: [new GraphQLError('error 2')],
+          errors: [new GraphQLError("error 2")],
         },
         delay: 20,
       },
     ];
 
-    const { result } = renderHook(
-      () => useLazyQuery(helloQuery),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useLazyQuery(helloQuery), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     const execute = result.current[0];
     expect(result.current[1].loading).toBe(false);
@@ -1029,129 +1147,140 @@ describe('useLazyQuery Hook', () => {
 
     const executePromise = Promise.resolve().then(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toBeUndefined();
     expect(result.current[1].error).toBe(undefined);
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toBeUndefined();
-    expect(result.current[1].error).toEqual(new Error('error 1'));
+    expect(result.current[1].error).toEqual(new Error("error 1"));
 
-    await executePromise.then(result => {
+    await executePromise.then((result) => {
       expect(result.loading).toBe(false);
       expect(result.data).toBeUndefined();
-      expect(result.error!.message).toBe('error 1');
+      expect(result.error!.message).toBe("error 1");
     });
 
     setTimeout(() => execute());
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(true);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(true);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toBeUndefined();
-    expect(result.current[1].error).toEqual(new Error('error 1'));
+    expect(result.current[1].error).toEqual(new Error("error 1"));
 
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current[1].data).toBe(undefined);
-    expect(result.current[1].error).toEqual(new Error('error 2'));
+    expect(result.current[1].error).toEqual(new Error("error 2"));
   });
 
-  it('the promise should not cause an unhandled rejection', async () => {
+  it("the promise should not cause an unhandled rejection", async () => {
     const mocks = [
       {
         request: { query: helloQuery },
         result: {
-          errors: [new GraphQLError('error 1')],
+          errors: [new GraphQLError("error 1")],
         },
       },
     ];
 
-    const { result } = renderHook(
-      () => useLazyQuery(helloQuery),
-      {
-        wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useLazyQuery(helloQuery), {
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
+    });
 
     const execute = result.current[0];
-    await waitFor(() => {
-      expect(result.current[1].loading).toBe(false);
-      execute();
-    }, { interval: 1 });
-    await waitFor(() => {
-      expect(result.current[1].data).toBe(undefined);
-      execute();
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current[1].loading).toBe(false);
+        execute();
+      },
+      { interval: 1 }
+    );
+    await waitFor(
+      () => {
+        expect(result.current[1].data).toBe(undefined);
+        execute();
+      },
+      { interval: 1 }
+    );
 
     // Making sure the rejection triggers a test failure.
     await wait(50);
   });
 
-  it('allows in-flight requests to resolve when component unmounts', async () => {
+  it("allows in-flight requests to resolve when component unmounts", async () => {
     const link = new MockSubscriptionLink();
-    const client = new ApolloClient({ link, cache: new InMemoryCache() })
+    const client = new ApolloClient({ link, cache: new InMemoryCache() });
 
     const { result, unmount } = renderHook(() => useLazyQuery(helloQuery), {
-      wrapper: ({ children }) =>
-        <ApolloProvider client={client}>
-          {children}
-        </ApolloProvider>
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
     });
 
     const [execute] = result.current;
 
-    let promise: Promise<QueryResult<{ hello: string }>>
+    let promise: Promise<QueryResult<{ hello: string }>>;
     act(() => {
       promise = execute();
-    })
+    });
 
     unmount();
 
-    link.simulateResult({ result: { data: { hello: 'Greetings' }}}, true);
+    link.simulateResult({ result: { data: { hello: "Greetings" } } }, true);
 
     const queryResult = await promise!;
 
-    expect(queryResult.data).toEqual({ hello: 'Greetings' });
+    expect(queryResult.data).toEqual({ hello: "Greetings" });
     expect(queryResult.loading).toBe(false);
     expect(queryResult.networkStatus).toBe(NetworkStatus.ready);
   });
 
-  it('handles resolving multiple in-flight requests when component unmounts', async () => {
+  it("handles resolving multiple in-flight requests when component unmounts", async () => {
     const link = new MockSubscriptionLink();
-    const client = new ApolloClient({ link, cache: new InMemoryCache() })
+    const client = new ApolloClient({ link, cache: new InMemoryCache() });
 
     const { result, unmount } = renderHook(() => useLazyQuery(helloQuery), {
-      wrapper: ({ children }) =>
-        <ApolloProvider client={client}>
-          {children}
-        </ApolloProvider>
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
     });
 
     const [execute] = result.current;
 
-    let promise1: Promise<QueryResult<{ hello: string }>>
-    let promise2: Promise<QueryResult<{ hello: string }>>
+    let promise1: Promise<QueryResult<{ hello: string }>>;
+    let promise2: Promise<QueryResult<{ hello: string }>>;
     act(() => {
       promise1 = execute();
       promise2 = execute();
-    })
+    });
 
     unmount();
 
-    link.simulateResult({ result: { data: { hello: 'Greetings' }}}, true);
+    link.simulateResult({ result: { data: { hello: "Greetings" } } }, true);
 
     const expectedResult = {
-      data: { hello: 'Greetings' },
+      data: { hello: "Greetings" },
       loading: false,
       networkStatus: NetworkStatus.ready,
     };
@@ -1161,13 +1290,13 @@ describe('useLazyQuery Hook', () => {
   });
 
   // https://github.com/apollographql/apollo-client/issues/9755
-  it('resolves each execution of the query with the appropriate result and renders with the result from the latest execution', async () => {
+  it("resolves each execution of the query with the appropriate result and renders with the result from the latest execution", async () => {
     interface Data {
-      user: { id: string, name: string }
+      user: { id: string; name: string };
     }
 
     interface Variables {
-      id: string
+      id: string;
     }
 
     const query: TypedDocumentNode<Data, Variables> = gql`
@@ -1181,39 +1310,38 @@ describe('useLazyQuery Hook', () => {
 
     const mocks = [
       {
-        request: { query, variables: { id: '1' } },
-        result: { data: { user: { id: '1', name: 'John Doe' }}},
-        delay: 20
+        request: { query, variables: { id: "1" } },
+        result: { data: { user: { id: "1", name: "John Doe" } } },
+        delay: 20,
       },
       {
-        request: { query, variables: { id: '2' } },
-        result: { data: { user: { id: '2', name: 'Jane Doe' }}},
-        delay: 20
+        request: { query, variables: { id: "2" } },
+        result: { data: { user: { id: "2", name: "Jane Doe" } } },
+        delay: 20,
       },
-    ]
+    ];
 
     const { result } = renderHook(() => useLazyQuery(query), {
-      wrapper: ({ children }) =>
-        <MockedProvider mocks={mocks}>
-          {children}
-        </MockedProvider>
+      wrapper: ({ children }) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      ),
     });
 
     const [execute] = result.current;
 
     await act(async () => {
-      const promise1 = execute({ variables: { id: '1' }});
-      const promise2 = execute({ variables: { id: '2' }});
+      const promise1 = execute({ variables: { id: "1" } });
+      const promise2 = execute({ variables: { id: "2" } });
 
       await expect(promise1).resolves.toMatchObject({
         ...mocks[0].result,
-        loading: false ,
+        loading: false,
         called: true,
       });
 
       await expect(promise2).resolves.toMatchObject({
         ...mocks[1].result,
-        loading: false ,
+        loading: false,
         called: true,
       });
     });
@@ -1225,13 +1353,13 @@ describe('useLazyQuery Hook', () => {
     });
   });
 
-  it('uses the most recent options when the hook rerenders before execution', async () => {
+  it("uses the most recent options when the hook rerenders before execution", async () => {
     interface Data {
-      user: { id: string, name: string }
+      user: { id: string; name: string };
     }
 
     interface Variables {
-      id: string
+      id: string;
     }
 
     const query: TypedDocumentNode<Data, Variables> = gql`
@@ -1245,29 +1373,28 @@ describe('useLazyQuery Hook', () => {
 
     const mocks = [
       {
-        request: { query, variables: { id: '1' } },
-        result: { data: { user: { id: '1', name: 'John Doe' }}},
-        delay: 30
+        request: { query, variables: { id: "1" } },
+        result: { data: { user: { id: "1", name: "John Doe" } } },
+        delay: 30,
       },
       {
-        request: { query, variables: { id: '2' } },
-        result: { data: { user: { id: '2', name: 'Jane Doe' }}},
-        delay: 20
+        request: { query, variables: { id: "2" } },
+        result: { data: { user: { id: "2", name: "Jane Doe" } } },
+        delay: 20,
       },
-    ]
+    ];
 
     const { result, rerender } = renderHook(
-      ({ id }) => useLazyQuery(query, { variables: { id } }), 
+      ({ id }) => useLazyQuery(query, { variables: { id } }),
       {
-        initialProps: { id: '1' },
-        wrapper: ({ children }) =>
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
+        initialProps: { id: "1" },
+        wrapper: ({ children }) => (
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
+        ),
       }
     );
 
-    rerender({ id: '2' });
+    rerender({ id: "2" });
 
     const [execute] = result.current;
 
@@ -1278,15 +1405,15 @@ describe('useLazyQuery Hook', () => {
 
     await waitFor(() => {
       expect(result.current[1].data).toEqual(mocks[1].result.data);
-    })
+    });
 
-    await expect(promise!).resolves.toMatchObject(
-      { data: mocks[1].result.data }
-    );
+    await expect(promise!).resolves.toMatchObject({
+      data: mocks[1].result.data,
+    });
   });
 
   // https://github.com/apollographql/apollo-client/issues/10198
-  it('uses the most recent query document when the hook rerenders before execution', async () => {
+  it("uses the most recent query document when the hook rerenders before execution", async () => {
     const query = gql`
       query DummyQuery {
         shouldNotBeUsed
@@ -1296,19 +1423,18 @@ describe('useLazyQuery Hook', () => {
     const mocks = [
       {
         request: { query: helloQuery },
-        result: { data: { hello: 'Greetings' } },
-        delay: 20
+        result: { data: { hello: "Greetings" } },
+        delay: 20,
       },
-    ]
+    ];
 
     const { result, rerender } = renderHook(
-      ({ query }) => useLazyQuery(query), 
+      ({ query }) => useLazyQuery(query),
       {
         initialProps: { query },
-        wrapper: ({ children }) =>
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
+        wrapper: ({ children }) => (
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
+        ),
       }
     );
 
@@ -1322,21 +1448,21 @@ describe('useLazyQuery Hook', () => {
     });
 
     await waitFor(() => {
-      expect(result.current[1].data).toEqual({ hello: 'Greetings' });
-    })
+      expect(result.current[1].data).toEqual({ hello: "Greetings" });
+    });
 
-    await expect(promise!).resolves.toMatchObject(
-      { data: { hello: 'Greetings' } }
-    );
+    await expect(promise!).resolves.toMatchObject({
+      data: { hello: "Greetings" },
+    });
   });
 
-  it('does not refetch when rerendering after executing query', async () => {
+  it("does not refetch when rerendering after executing query", async () => {
     interface Data {
-      user: { id: string, name: string }
+      user: { id: string; name: string };
     }
 
     interface Variables {
-      id: string
+      id: string;
     }
 
     const query: TypedDocumentNode<Data, Variables> = gql`
@@ -1354,30 +1480,29 @@ describe('useLazyQuery Hook', () => {
       fetchCount++;
       return new Observable((observer) => {
         setTimeout(() => {
-          observer.next({ 
-            data: { user: { id: operation.variables.id, name: 'John Doe' } }
+          observer.next({
+            data: { user: { id: operation.variables.id, name: "John Doe" } },
           });
           observer.complete();
-        }, 20)
+        }, 20);
       });
     });
 
     const client = new ApolloClient({ link, cache: new InMemoryCache() });
 
     const { result, rerender } = renderHook(
-      () => useLazyQuery(query, { variables: { id: '1' }}), 
+      () => useLazyQuery(query, { variables: { id: "1" } }),
       {
-        initialProps: { id: '1' },
-        wrapper: ({ children }) =>
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+        initialProps: { id: "1" },
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
       }
     );
 
     const [execute] = result.current;
 
-    await act(() => execute({ variables: { id: '2' }}));
+    await act(() => execute({ variables: { id: "2" } }));
 
     expect(fetchCount).toBe(1);
 
@@ -1386,7 +1511,7 @@ describe('useLazyQuery Hook', () => {
     await wait(10);
 
     expect(fetchCount).toBe(1);
-  })
+  });
 
   describe("network errors", () => {
     async function check(errorPolicy: ErrorPolicy) {
@@ -1394,24 +1519,26 @@ describe('useLazyQuery Hook', () => {
 
       const client = new ApolloClient({
         cache: new InMemoryCache(),
-        link: new ApolloLink(request => new Observable(observer => {
-          setTimeout(() => {
-            observer.error(networkError);
-          }, 20);
-        })),
+        link: new ApolloLink(
+          (request) =>
+            new Observable((observer) => {
+              setTimeout(() => {
+                observer.error(networkError);
+              }, 20);
+            })
+        ),
       });
 
       const { result } = renderHook(
-        () => useLazyQuery(helloQuery, {
-          errorPolicy,
-        }),
+        () =>
+          useLazyQuery(helloQuery, {
+            errorPolicy,
+          }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       const execute = result.current[0];
@@ -1421,32 +1548,53 @@ describe('useLazyQuery Hook', () => {
 
       setTimeout(execute);
 
-      await waitFor(() => {
-        expect(result.current[1].loading).toBe(true);
-      }, { interval: 1 });
-      await waitFor(() => {
-        if (IS_REACT_18) {
-          expect(result.current[1].networkStatus).toBe(NetworkStatus.loading);
-        } else {
-          expect(result.current[1].networkStatus).toBe(NetworkStatus.error);
-        }
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].data).toBeUndefined();
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current[1].loading).toBe(true);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          if (IS_REACT_18) {
+            expect(result.current[1].networkStatus).toBe(NetworkStatus.loading);
+          } else {
+            expect(result.current[1].networkStatus).toBe(NetworkStatus.error);
+          }
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].data).toBeUndefined();
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        expect(result.current[1].loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].networkStatus).toBe(NetworkStatus.error);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].data).toBeUndefined();
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].error!.message).toBe("from the network");
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current[1].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].networkStatus).toBe(NetworkStatus.error);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].data).toBeUndefined();
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].error!.message).toBe("from the network");
+        },
+        { interval: 1 }
+      );
     }
 
     // For errorPolicy:"none", we expect result.error to be defined and
@@ -1477,22 +1625,27 @@ describe('useLazyQuery Hook', () => {
       let count = 0;
       const client = new ApolloClient({
         cache: new InMemoryCache(),
-        link: new ApolloLink(request => new Observable(observer => {
-          if (request.operationName === "GetCounter") {
-            observer.next({
-              data: {
-                counter: ++count,
-              },
-            });
-            setTimeout(() => {
-              observer.complete();
-            }, 10);
-          } else {
-            observer.error(new Error(`Unknown query: ${
-              request.operationName || request.query
-            }`));
-          }
-        })),
+        link: new ApolloLink(
+          (request) =>
+            new Observable((observer) => {
+              if (request.operationName === "GetCounter") {
+                observer.next({
+                  data: {
+                    counter: ++count,
+                  },
+                });
+                setTimeout(() => {
+                  observer.complete();
+                }, 10);
+              } else {
+                observer.error(
+                  new Error(
+                    `Unknown query: ${request.operationName || request.query}`
+                  )
+                );
+              }
+            })
+        ),
       });
 
       const defaultFetchPolicy = "network-only";
@@ -1512,50 +1665,91 @@ describe('useLazyQuery Hook', () => {
         },
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.query.called).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.query.data).toBeUndefined();
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.query.called).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.query.data).toBeUndefined();
+        },
+        { interval: 1 }
+      );
 
       const execResult = await result.current.exec();
       expect(execResult.loading).toBe(false);
       expect(execResult.called).toBe(true);
       expect(execResult.data).toEqual({ counter: 1 });
 
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.query.data).toMatchObject({ counter: 1 });
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.query.called).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.query.data).toMatchObject({ counter: 1 });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.query.called).toBe(true);
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.query.called).toBe(true);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.query.data).toEqual({ counter: 1 });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.query.called).toBe(true);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.query.data).toEqual({ counter: 1 });
+        },
+        { interval: 1 }
+      );
 
       const { options } = result.current.query.observable;
       expect(options.fetchPolicy).toBe(defaultFetchPolicy);
     });
   });
 });
+
+describe.skip("Type Tests", () => {
+  test("NoInfer prevents adding arbitrary additional variables", () => {
+    const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;
+    const [_, { variables }] = useLazyQuery(typedNode, {
+      variables: {
+        bar: 4,
+        // @ts-expect-error
+        nonExistingVariable: "string",
+      },
+    });
+    variables?.bar;
+    // @ts-expect-error
+    variables?.nonExistingVariable;
+  });
+});
diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx
--- a/src/react/hooks/__tests__/useMutation.test.tsx
+++ b/src/react/hooks/__tests__/useMutation.test.tsx
@@ -1,24 +1,36 @@
-import React, { useEffect } from 'react';
-import { GraphQLError } from 'graphql';
-import gql from 'graphql-tag';
-import { act } from 'react-dom/test-utils';
-import { render, waitFor, screen, renderHook } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
+import React, { useEffect } from "react";
+import { GraphQLError } from "graphql";
+import gql from "graphql-tag";
+import { act } from "react-dom/test-utils";
+import { render, waitFor, screen, renderHook } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
 import fetchMock from "fetch-mock";
 
-import { ApolloClient, ApolloLink, ApolloQueryResult, Cache, NetworkStatus, Observable, ObservableQuery, TypedDocumentNode } from '../../../core';
-import { InMemoryCache } from '../../../cache';
-import { itAsync, MockedProvider, MockSubscriptionLink, mockSingleLink, subscribeAndCount } from '../../../testing';
-import { ApolloProvider, resetApolloContext } from '../../context';
-import { useQuery } from '../useQuery';
-import { useMutation } from '../useMutation';
-import { BatchHttpLink } from '../../../link/batch-http';
-import { FetchResult } from '../../../link/core';
-
-describe('useMutation Hook', () => {
-  afterEach(() => {
-    resetApolloContext();
-  });
+import {
+  ApolloClient,
+  ApolloLink,
+  ApolloQueryResult,
+  Cache,
+  NetworkStatus,
+  Observable,
+  ObservableQuery,
+  TypedDocumentNode,
+} from "../../../core";
+import { InMemoryCache } from "../../../cache";
+import {
+  itAsync,
+  MockedProvider,
+  MockSubscriptionLink,
+  mockSingleLink,
+  subscribeAndCount,
+} from "../../../testing";
+import { ApolloProvider } from "../../context";
+import { useQuery } from "../useQuery";
+import { useMutation } from "../useMutation";
+import { BatchHttpLink } from "../../../link/batch-http";
+import { FetchResult } from "../../../link/core";
+
+describe("useMutation Hook", () => {
   interface Todo {
     id: number;
     description: string;
@@ -38,38 +50,35 @@ describe('useMutation Hook', () => {
   const CREATE_TODO_RESULT = {
     createTodo: {
       id: 1,
-      description: 'Get milk!',
-      priority: 'High',
-      __typename: 'Todo'
-    }
+      description: "Get milk!",
+      priority: "High",
+      __typename: "Todo",
+    },
   };
 
-  const CREATE_TODO_ERROR = 'Failed to create item';
+  const CREATE_TODO_ERROR = "Failed to create item";
 
-  describe('General use', () => {
-    it('should handle a simple mutation properly', async () => {
+  describe("General use", () => {
+    it("should handle a simple mutation properly", async () => {
       const variables = {
-        description: 'Get milk!'
+        description: "Get milk!",
       };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
+          result: { data: CREATE_TODO_RESULT },
+        },
       ];
 
-      const { result } = renderHook(
-        () => useMutation(CREATE_TODO_MUTATION),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
-      );
+      const { result } = renderHook(() => useMutation(CREATE_TODO_MUTATION), {
+        wrapper: ({ children }) => (
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
+        ),
+      });
 
       expect(result.current[1].loading).toBe(false);
       expect(result.current[1].data).toBe(undefined);
@@ -78,31 +87,33 @@ describe('useMutation Hook', () => {
       expect(result.current[1].loading).toBe(true);
       expect(result.current[1].data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current[1].loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current[1].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current[1].data).toEqual(CREATE_TODO_RESULT);
     });
 
-    it('should be able to call mutations as an effect', async () => {
+    it("should be able to call mutations as an effect", async () => {
       const variables = {
-        description: 'Get milk!'
+        description: "Get milk!",
       };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
+          result: { data: CREATE_TODO_RESULT },
+        },
       ];
 
       const useCreateTodo = () => {
-        const [createTodo, { loading, data }] = useMutation(
-          CREATE_TODO_MUTATION
-        );
+        const [createTodo, { loading, data }] =
+          useMutation(CREATE_TODO_MUTATION);
         useEffect(() => {
           createTodo({ variables });
         }, [variables]);
@@ -110,48 +121,48 @@ describe('useMutation Hook', () => {
         return { loading, data };
       };
 
-      const { result } = renderHook(
-        () => useCreateTodo(),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
-      );
+      const { result } = renderHook(() => useCreateTodo(), {
+        wrapper: ({ children }) => (
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(CREATE_TODO_RESULT);
     });
 
-    it('should ensure the mutation callback function has a stable identity no matter what', async () => {
+    it("should ensure the mutation callback function has a stable identity no matter what", async () => {
       const variables1 = {
-        description: 'Get milk',
+        description: "Get milk",
       };
 
       const data1 = {
         createTodo: {
           id: 1,
-          description: 'Get milk!',
-          priority: 'High',
-          __typename: 'Todo',
-        }
+          description: "Get milk!",
+          priority: "High",
+          __typename: "Todo",
+        },
       };
 
       const variables2 = {
-        description: 'Write blog post',
+        description: "Write blog post",
       };
 
       const data2 = {
         createTodo: {
           id: 1,
-          description: 'Write blog post',
-          priority: 'High',
-          __typename: 'Todo',
+          description: "Write blog post",
+          priority: "High",
+          __typename: "Todo",
         },
       };
 
@@ -176,14 +187,12 @@ describe('useMutation Hook', () => {
         ({ variables }) => useMutation(CREATE_TODO_MUTATION, { variables }),
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
           ),
           initialProps: {
             variables: variables1,
           },
-        },
+        }
       );
 
       const createTodo = result.current[0];
@@ -195,88 +204,86 @@ describe('useMutation Hook', () => {
       expect(result.current[1].loading).toBe(true);
       expect(result.current[1].data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current[1].loading).toBe(false);
-      }, { interval: 1 })
+      await waitFor(
+        () => {
+          expect(result.current[1].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current[0]).toBe(createTodo);
       expect(result.current[1].data).toEqual(data1);
 
       rerender({ variables: variables2 });
       act(() => void createTodo());
 
-      await waitFor(() => {
-        expect(result.current[1].loading).toBe(false);
-      }, { interval: 1 })
+      await waitFor(
+        () => {
+          expect(result.current[1].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current[0]).toBe(createTodo);
       expect(result.current[1].data).toEqual(data2);
     });
 
-    it('should not call setResult on an unmounted component', async () => {
+    it("should not call setResult on an unmounted component", async () => {
       const errorSpy = jest.spyOn(console, "error");
       const variables = {
-        description: 'Get milk!'
+        description: "Get milk!",
       };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
+          result: { data: CREATE_TODO_RESULT },
+        },
       ];
 
       const useCreateTodo = () => {
-        const [createTodo, { reset }] = useMutation(
-          CREATE_TODO_MUTATION
-        );
+        const [createTodo, { reset }] = useMutation(CREATE_TODO_MUTATION);
         return { reset, createTodo };
       };
 
-      const { result, unmount } = renderHook(
-        () => useCreateTodo(),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
-      );
+      const { result, unmount } = renderHook(() => useCreateTodo(), {
+        wrapper: ({ children }) => (
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
+        ),
+      });
 
       unmount();
 
       await act(async () => {
         await result.current.createTodo({ variables });
         await result.current.reset();
-      })
+      });
 
       expect(errorSpy).not.toHaveBeenCalled();
       errorSpy.mockRestore();
     });
 
-    it('should resolve mutate function promise with mutation results', async () => {
+    it("should resolve mutate function promise with mutation results", async () => {
       const variables = {
-        description: 'Get milk!'
+        description: "Get milk!",
       };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
+          result: { data: CREATE_TODO_RESULT },
+        },
       ];
 
-      const { result } = renderHook(
-        () => useMutation(CREATE_TODO_MUTATION),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
-      );
+      const { result } = renderHook(() => useMutation(CREATE_TODO_MUTATION), {
+        wrapper: ({ children }) => (
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
+        ),
+      });
 
       await act(async () => {
         await expect(result.current[0]({ variables })).resolves.toEqual({
@@ -285,10 +292,10 @@ describe('useMutation Hook', () => {
       });
     });
 
-    describe('mutate function upon error', () => {
-      it('resolves with the resulting data and errors', async () => {
+    describe("mutate function upon error", () => {
+      it("resolves with the resulting data and errors", async () => {
         const variables = {
-          description: 'Get milk!'
+          description: "Get milk!",
         };
 
         const mocks = [
@@ -301,17 +308,17 @@ describe('useMutation Hook', () => {
               data: CREATE_TODO_RESULT,
               errors: [new GraphQLError(CREATE_TODO_ERROR)],
             },
-          }
+          },
         ];
 
         const onError = jest.fn();
         const { result } = renderHook(
           () => useMutation(CREATE_TODO_MUTATION, { onError }),
-          { wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
-          )},
+          {
+            wrapper: ({ children }) => (
+              <MockedProvider mocks={mocks}>{children}</MockedProvider>
+            ),
+          }
         );
 
         const createTodo = result.current[0];
@@ -326,9 +333,9 @@ describe('useMutation Hook', () => {
         expect(onError.mock.calls[0][0].message).toBe(CREATE_TODO_ERROR);
       });
 
-      it('should reject when there’s only an error and no error policy is set', async () => {
+      it("should reject when there’s only an error and no error policy is set", async () => {
         const variables = {
-          description: 'Get milk!'
+          description: "Get milk!",
         };
 
         const mocks = [
@@ -340,17 +347,14 @@ describe('useMutation Hook', () => {
             result: {
               errors: [new GraphQLError(CREATE_TODO_ERROR)],
             },
-          }
+          },
         ];
 
-        const { result } = renderHook(
-          () => useMutation(CREATE_TODO_MUTATION),
-          { wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
-          )},
-        );
+        const { result } = renderHook(() => useMutation(CREATE_TODO_MUTATION), {
+          wrapper: ({ children }) => (
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
+          ),
+        });
 
         const createTodo = result.current[0];
         let fetchError: any;
@@ -371,62 +375,110 @@ describe('useMutation Hook', () => {
 
       it(`should reject when errorPolicy is 'none'`, async () => {
         const variables = {
-          description: 'Get milk!'
+          description: "Get milk!",
         };
 
         const mocks = [
           {
             request: {
               query: CREATE_TODO_MUTATION,
-              variables
+              variables,
             },
             result: {
               data: CREATE_TODO_RESULT,
               errors: [new GraphQLError(CREATE_TODO_ERROR)],
             },
-          }
+          },
         ];
 
         const { result } = renderHook(
-          () => useMutation(CREATE_TODO_MUTATION, { errorPolicy: 'none' }),
-          { wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
-          )},
+          () => useMutation(CREATE_TODO_MUTATION, { errorPolicy: "none" }),
+          {
+            wrapper: ({ children }) => (
+              <MockedProvider mocks={mocks}>{children}</MockedProvider>
+            ),
+          }
         );
 
         const createTodo = result.current[0];
         await act(async () => {
-          await expect(createTodo({ variables })).rejects.toThrow(CREATE_TODO_ERROR);
+          await expect(createTodo({ variables })).rejects.toThrow(
+            CREATE_TODO_ERROR
+          );
         });
       });
 
       it(`should resolve with 'data' and 'error' properties when errorPolicy is 'all'`, async () => {
         const variables = {
-          description: 'Get milk!'
+          description: "Get milk!",
         };
 
         const mocks = [
           {
             request: {
               query: CREATE_TODO_MUTATION,
-              variables
+              variables,
             },
             result: {
               data: CREATE_TODO_RESULT,
               errors: [new GraphQLError(CREATE_TODO_ERROR)],
             },
+          },
+        ];
+
+        const { result } = renderHook(
+          () => useMutation(CREATE_TODO_MUTATION, { errorPolicy: "all" }),
+          {
+            wrapper: ({ children }) => (
+              <MockedProvider mocks={mocks}>{children}</MockedProvider>
+            ),
           }
+        );
+
+        const createTodo = result.current[0];
+
+        let fetchResult: any;
+        await act(async () => {
+          fetchResult = await createTodo({ variables });
+        });
+
+        expect(fetchResult.data).toEqual(CREATE_TODO_RESULT);
+        expect(fetchResult.errors[0].message).toEqual(CREATE_TODO_ERROR);
+      });
+
+      it(`should call onError when errorPolicy is 'all'`, async () => {
+        const variables = {
+          description: "Get milk!",
+        };
+
+        const mocks = [
+          {
+            request: {
+              query: CREATE_TODO_MUTATION,
+              variables,
+            },
+            result: {
+              data: CREATE_TODO_RESULT,
+              errors: [new GraphQLError(CREATE_TODO_ERROR)],
+            },
+          },
         ];
 
+        const onError = jest.fn();
+        const onCompleted = jest.fn();
+
         const { result } = renderHook(
-          () => useMutation(CREATE_TODO_MUTATION, { errorPolicy: 'all' }),
-          { wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
-          )},
+          () =>
+            useMutation(CREATE_TODO_MUTATION, {
+              errorPolicy: "all",
+              onError,
+              onCompleted,
+            }),
+          {
+            wrapper: ({ children }) => (
+              <MockedProvider mocks={mocks}>{children}</MockedProvider>
+            ),
+          }
         );
 
         const createTodo = result.current[0];
@@ -438,13 +490,17 @@ describe('useMutation Hook', () => {
 
         expect(fetchResult.data).toEqual(CREATE_TODO_RESULT);
         expect(fetchResult.errors[0].message).toEqual(CREATE_TODO_ERROR);
+        expect(onError).toHaveBeenCalledTimes(1);
+        expect(onError.mock.calls[0][0].message).toBe(CREATE_TODO_ERROR);
+        expect(onCompleted).not.toHaveBeenCalled();
       });
 
       it(`should ignore errors when errorPolicy is 'ignore'`, async () => {
-        const errorMock = jest.spyOn(console, "error")
+        const errorMock = jest
+          .spyOn(console, "error")
           .mockImplementation(() => {});
         const variables = {
-          description: 'Get milk!'
+          description: "Get milk!",
         };
 
         const mocks = [
@@ -456,16 +512,16 @@ describe('useMutation Hook', () => {
             result: {
               errors: [new GraphQLError(CREATE_TODO_ERROR)],
             },
-          }
+          },
         ];
 
         const { result } = renderHook(
           () => useMutation(CREATE_TODO_MUTATION, { errorPolicy: "ignore" }),
-          { wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
-          )},
+          {
+            wrapper: ({ children }) => (
+              <MockedProvider mocks={mocks}>{children}</MockedProvider>
+            ),
+          }
         );
 
         const createTodo = result.current[0];
@@ -479,41 +535,74 @@ describe('useMutation Hook', () => {
         expect(errorMock.mock.calls[0][0]).toMatch("Missing field");
         errorMock.mockRestore();
       });
+
+      it(`should not call onError when errorPolicy is 'ignore'`, async () => {
+        const variables = {
+          description: "Get milk!",
+        };
+
+        const mocks = [
+          {
+            request: {
+              query: CREATE_TODO_MUTATION,
+              variables,
+            },
+            result: {
+              errors: [new GraphQLError(CREATE_TODO_ERROR)],
+            },
+          },
+        ];
+
+        const onError = jest.fn();
+
+        const { result } = renderHook(
+          () =>
+            useMutation(CREATE_TODO_MUTATION, {
+              errorPolicy: "ignore",
+              onError,
+            }),
+          {
+            wrapper: ({ children }) => (
+              <MockedProvider mocks={mocks}>{children}</MockedProvider>
+            ),
+          }
+        );
+
+        const createTodo = result.current[0];
+        let fetchResult: any;
+        await act(async () => {
+          fetchResult = await createTodo({ variables });
+        });
+
+        expect(fetchResult).toEqual({});
+        expect(onError).not.toHaveBeenCalled();
+      });
     });
 
-    it('should return the current client instance in the result object', async () => {
-      const { result } = renderHook(
-        () => useMutation(CREATE_TODO_MUTATION),
-        { wrapper: ({ children }) => (
-          <MockedProvider>
-            {children}
-          </MockedProvider>
-        )},
-      );
+    it("should return the current client instance in the result object", async () => {
+      const { result } = renderHook(() => useMutation(CREATE_TODO_MUTATION), {
+        wrapper: ({ children }) => <MockedProvider>{children}</MockedProvider>,
+      });
       expect(result.current[1].client).toBeInstanceOf(ApolloClient);
     });
 
-    it ('should call client passed to execute function', async () => {
-      const { result } = renderHook(
-        () => useMutation(CREATE_TODO_MUTATION),
-        { wrapper: ({ children }) => (
-          <MockedProvider>
-            {children}
-          </MockedProvider>
-        )},
-      );
+    it("should call client passed to execute function", async () => {
+      const { result } = renderHook(() => useMutation(CREATE_TODO_MUTATION), {
+        wrapper: ({ children }) => <MockedProvider>{children}</MockedProvider>,
+      });
 
       const link = mockSingleLink();
       const cache = new InMemoryCache();
       const client = new ApolloClient({
         cache,
-        link
+        link,
       });
 
-      const mutateSpy = jest.spyOn(client, 'mutate').mockImplementation(
-        () => new Promise((resolve) => {
-          resolve({ data: CREATE_TODO_RESULT })
-        })
+      const mutateSpy = jest.spyOn(client, "mutate").mockImplementation(
+        () =>
+          new Promise((resolve) => {
+            resolve({ data: CREATE_TODO_RESULT });
+          })
       );
 
       const createTodo = result.current[0];
@@ -524,13 +613,13 @@ describe('useMutation Hook', () => {
       expect(mutateSpy).toHaveBeenCalledTimes(1);
     });
 
-    it('should merge provided variables', async () => {
+    it("should merge provided variables", async () => {
       const CREATE_TODO_DATA = {
         createTodo: {
           id: 1,
-          description: 'Get milk!',
-          priority: 'Low',
-          __typename: 'Todo',
+          description: "Get milk!",
+          priority: "Low",
+          __typename: "Todo",
         },
       };
       const mocks = [
@@ -538,46 +627,49 @@ describe('useMutation Hook', () => {
           request: {
             query: CREATE_TODO_MUTATION,
             variables: {
-              priority: 'Low',
-              description: 'Get milk.'
-            }
+              priority: "Low",
+              description: "Get milk.",
+            },
           },
           result: {
             data: CREATE_TODO_DATA,
-          }
-        }
+          },
+        },
       ];
 
       const { result } = renderHook(
-        () => useMutation<
-          { createTodo: Todo },
-          { priority?: string, description?: string }
-        >(CREATE_TODO_MUTATION, {
-          variables: { priority: 'Low' }
-        }),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
+        () =>
+          useMutation<
+            { createTodo: Todo },
+            { priority?: string; description?: string }
+          >(CREATE_TODO_MUTATION, {
+            variables: { priority: "Low" },
+          }),
+        {
+          wrapper: ({ children }) => (
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
+          ),
+        }
       );
 
       const createTodo = result.current[0];
       let fetchResult: any;
       await act(async () => {
-        fetchResult = await createTodo({ variables: { description: 'Get milk.' }});
+        fetchResult = await createTodo({
+          variables: { description: "Get milk." },
+        });
       });
 
       expect(fetchResult).toEqual({ data: CREATE_TODO_DATA });
     });
 
-    it('should be possible to reset the mutation', async () => {
+    it("should be possible to reset the mutation", async () => {
       const CREATE_TODO_DATA = {
         createTodo: {
           id: 1,
-          priority: 'Low',
-          description: 'Get milk!',
-          __typename: 'Todo',
+          priority: "Low",
+          description: "Get milk!",
+          __typename: "Todo",
         },
       };
 
@@ -586,33 +678,34 @@ describe('useMutation Hook', () => {
           request: {
             query: CREATE_TODO_MUTATION,
             variables: {
-              priority: 'Low',
-              description: 'Get milk.',
-            }
+              priority: "Low",
+              description: "Get milk.",
+            },
           },
           result: {
             data: CREATE_TODO_DATA,
-          }
-        }
+          },
+        },
       ];
 
       const { result } = renderHook(
-        () => useMutation<
-          { createTodo: Todo },
-          { priority: string, description: string }
-        >(CREATE_TODO_MUTATION),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
+        () =>
+          useMutation<
+            { createTodo: Todo },
+            { priority: string; description: string }
+          >(CREATE_TODO_MUTATION),
+        {
+          wrapper: ({ children }) => (
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
+          ),
+        }
       );
 
       const createTodo = result.current[0];
       let fetchResult: any;
       await act(async () => {
         fetchResult = await createTodo({
-          variables: { priority: 'Low', description: 'Get milk.' },
+          variables: { priority: "Low", description: "Get milk." },
         });
       });
 
@@ -622,27 +715,30 @@ describe('useMutation Hook', () => {
         result.current[1].reset();
       });
 
-      await waitFor(() => {
-        expect(result.current[1].data).toBe(undefined);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current[1].data).toBe(undefined);
+        },
+        { interval: 1 }
+      );
     });
   });
 
-  describe('Callbacks', () => {
-    it('should allow passing an onCompleted handler to the execution function', async () => {
+  describe("Callbacks", () => {
+    it("should allow passing an onCompleted handler to the execution function", async () => {
       const CREATE_TODO_DATA = {
         createTodo: {
           id: 1,
-          priority: 'Low',
-          description: 'Get milk!',
-          __typename: 'Todo',
+          priority: "Low",
+          description: "Get milk!",
+          __typename: "Todo",
         },
       };
 
       const variables = {
-        priority: 'Low',
-        description: 'Get milk.',
-      }
+        priority: "Low",
+        description: "Get milk.",
+      };
 
       const mocks = [
         {
@@ -653,19 +749,20 @@ describe('useMutation Hook', () => {
           result: {
             data: CREATE_TODO_DATA,
           },
-        }
+        },
       ];
 
       const { result } = renderHook(
-        () => useMutation<
-          { createTodo: Todo },
-          { priority: string, description: string }
-        >(CREATE_TODO_MUTATION),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
+        () =>
+          useMutation<
+            { createTodo: Todo },
+            { priority: string; description: string }
+          >(CREATE_TODO_MUTATION),
+        {
+          wrapper: ({ children }) => (
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
+          ),
+        }
       );
 
       const createTodo = result.current[0];
@@ -683,23 +780,26 @@ describe('useMutation Hook', () => {
       expect(fetchResult).toEqual({ data: CREATE_TODO_DATA });
       expect(result.current[1].data).toEqual(CREATE_TODO_DATA);
       expect(onCompleted).toHaveBeenCalledTimes(1);
-      expect(onCompleted).toHaveBeenCalledWith(CREATE_TODO_DATA, expect.objectContaining({variables}));
+      expect(onCompleted).toHaveBeenCalledWith(
+        CREATE_TODO_DATA,
+        expect.objectContaining({ variables })
+      );
       expect(onError).toHaveBeenCalledTimes(0);
     });
 
-    it('prefers the onCompleted handler passed to the execution function rather than the hook', async () => {
+    it("prefers the onCompleted handler passed to the execution function rather than the hook", async () => {
       const CREATE_TODO_DATA = {
         createTodo: {
           id: 1,
-          priority: 'Low',
-          description: 'Get milk!',
-          __typename: 'Todo',
+          priority: "Low",
+          description: "Get milk!",
+          __typename: "Todo",
         },
       };
       const variables = {
-        priority: 'Low',
-        description: 'Get milk.',
-      }
+        priority: "Low",
+        description: "Get milk.",
+      };
       const mocks = [
         {
           request: {
@@ -707,22 +807,21 @@ describe('useMutation Hook', () => {
             variables,
           },
           result: {
-            data: CREATE_TODO_DATA
+            data: CREATE_TODO_DATA,
           },
-        }
+        },
       ];
 
       const hookOnCompleted = jest.fn();
 
       const { result } = renderHook(
-        () => useMutation(CREATE_TODO_MUTATION, { onCompleted: hookOnCompleted }),
+        () =>
+          useMutation(CREATE_TODO_MUTATION, { onCompleted: hookOnCompleted }),
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
-          )
-        },
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
+          ),
+        }
       );
 
       const [createTodo] = result.current;
@@ -735,12 +834,12 @@ describe('useMutation Hook', () => {
       expect(hookOnCompleted).not.toHaveBeenCalled();
     });
 
-    it('should allow passing an onError handler to the execution function', async () => {
+    it("should allow passing an onError handler to the execution function", async () => {
       const errors = [new GraphQLError(CREATE_TODO_ERROR)];
       const variables = {
-        priority: 'Low',
-        description: 'Get milk.',
-      }
+        priority: "Low",
+        description: "Get milk.",
+      };
       const mocks = [
         {
           request: {
@@ -750,19 +849,20 @@ describe('useMutation Hook', () => {
           result: {
             errors,
           },
-        }
+        },
       ];
 
       const { result } = renderHook(
-        () => useMutation<
-          { createTodo: Todo },
-          { priority: string, description: string }
-        >(CREATE_TODO_MUTATION),
-        { wrapper: ({ children }) => (
-          <MockedProvider mocks={mocks}>
-            {children}
-          </MockedProvider>
-        )},
+        () =>
+          useMutation<
+            { createTodo: Todo },
+            { priority: string; description: string }
+          >(CREATE_TODO_MUTATION),
+        {
+          wrapper: ({ children }) => (
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
+          ),
+        }
       );
 
       const createTodo = result.current[0];
@@ -785,14 +885,17 @@ describe('useMutation Hook', () => {
 
       expect(onCompleted).toHaveBeenCalledTimes(0);
       expect(onError).toHaveBeenCalledTimes(1);
-      expect(onError).toHaveBeenCalledWith(errors[0], expect.objectContaining({variables}));
+      expect(onError).toHaveBeenCalledWith(
+        errors[0],
+        expect.objectContaining({ variables })
+      );
     });
 
-    it('prefers the onError handler passed to the execution function instead of the hook', async () => {
+    it("prefers the onError handler passed to the execution function instead of the hook", async () => {
       const variables = {
-        priority: 'Low',
-        description: 'Get milk.',
-      }
+        priority: "Low",
+        description: "Get milk.",
+      };
       const mocks = [
         {
           request: {
@@ -802,7 +905,7 @@ describe('useMutation Hook', () => {
           result: {
             errors: [new GraphQLError(CREATE_TODO_ERROR)],
           },
-        }
+        },
       ];
 
       const hookOnError = jest.fn();
@@ -811,11 +914,9 @@ describe('useMutation Hook', () => {
         () => useMutation(CREATE_TODO_MUTATION, { onError: hookOnError }),
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
-          )
-        },
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
+          ),
+        }
       );
 
       const [createTodo] = result.current;
@@ -828,12 +929,12 @@ describe('useMutation Hook', () => {
       expect(hookOnError).not.toHaveBeenCalled();
     });
 
-    it('should allow updating onError while mutation is executing', async () => {
+    it("should allow updating onError while mutation is executing", async () => {
       const errors = [new GraphQLError(CREATE_TODO_ERROR)];
       const variables = {
-        priority: 'Low',
-        description: 'Get milk.',
-      }
+        priority: "Low",
+        description: "Get milk.",
+      };
       const mocks = [
         {
           request: {
@@ -843,7 +944,7 @@ describe('useMutation Hook', () => {
           result: {
             errors,
           },
-        }
+        },
       ];
 
       const onCompleted = jest.fn();
@@ -853,17 +954,15 @@ describe('useMutation Hook', () => {
         ({ onCompleted, onError }) => {
           return useMutation<
             { createTodo: Todo },
-            { priority: string, description: string }
+            { priority: string; description: string }
           >(CREATE_TODO_MUTATION, { onCompleted, onError });
         },
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
           ),
           initialProps: { onCompleted, onError },
-        },
+        }
       );
 
       const createTodo = result.current[0];
@@ -886,34 +985,37 @@ describe('useMutation Hook', () => {
       expect(onCompleted).toHaveBeenCalledTimes(0);
       expect(onError).toHaveBeenCalledTimes(0);
       expect(onError1).toHaveBeenCalledTimes(1);
-      expect(onError1).toHaveBeenCalledWith(errors[0], expect.objectContaining({variables}));
+      expect(onError1).toHaveBeenCalledWith(
+        errors[0],
+        expect.objectContaining({ variables })
+      );
     });
 
-    it('should never allow onCompleted handler to be stale', async () => {
+    it("should never allow onCompleted handler to be stale", async () => {
       const CREATE_TODO_DATA = {
         createTodo: {
           id: 1,
-          priority: 'Low',
-          description: 'Get milk!',
-          __typename: 'Todo',
+          priority: "Low",
+          description: "Get milk!",
+          __typename: "Todo",
         },
       };
 
       const variables = {
-        priority: 'Low',
-        description: 'Get milk2.',
-      }
+        priority: "Low",
+        description: "Get milk2.",
+      };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
           result: {
             data: CREATE_TODO_DATA,
           },
-        }
+        },
       ];
 
       const onCompleted = jest.fn();
@@ -921,17 +1023,15 @@ describe('useMutation Hook', () => {
         ({ onCompleted }) => {
           return useMutation<
             { createTodo: Todo },
-            { priority: string, description: string }
+            { priority: string; description: string }
           >(CREATE_TODO_MUTATION, { onCompleted });
         },
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
           ),
           initialProps: { onCompleted },
-        },
+        }
       );
 
       const onCompleted1 = jest.fn();
@@ -948,34 +1048,37 @@ describe('useMutation Hook', () => {
       expect(result.current[1].data).toEqual(CREATE_TODO_DATA);
       expect(onCompleted).toHaveBeenCalledTimes(0);
       expect(onCompleted1).toHaveBeenCalledTimes(1);
-      expect(onCompleted1).toHaveBeenCalledWith(CREATE_TODO_DATA, expect.objectContaining({variables}));
+      expect(onCompleted1).toHaveBeenCalledWith(
+        CREATE_TODO_DATA,
+        expect.objectContaining({ variables })
+      );
     });
 
-    it('should allow updating onCompleted while mutation is executing', async () => {
+    it("should allow updating onCompleted while mutation is executing", async () => {
       const CREATE_TODO_DATA = {
         createTodo: {
           id: 1,
-          priority: 'Low',
-          description: 'Get milk!',
-          __typename: 'Todo',
+          priority: "Low",
+          description: "Get milk!",
+          __typename: "Todo",
         },
       };
 
       const variables = {
-        priority: 'Low',
-        description: 'Get milk2.',
-      }
+        priority: "Low",
+        description: "Get milk2.",
+      };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
           result: {
             data: CREATE_TODO_DATA,
           },
-        }
+        },
       ];
 
       const onCompleted = jest.fn();
@@ -984,17 +1087,15 @@ describe('useMutation Hook', () => {
         ({ onCompleted }) => {
           return useMutation<
             { createTodo: Todo },
-            { priority: string, description: string }
+            { priority: string; description: string }
           >(CREATE_TODO_MUTATION, { onCompleted });
         },
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
           ),
           initialProps: { onCompleted },
-        },
+        }
       );
 
       const createTodo = result.current[0];
@@ -1013,24 +1114,30 @@ describe('useMutation Hook', () => {
       expect(result.current[1].data).toEqual(CREATE_TODO_DATA);
       expect(onCompleted).toHaveBeenCalledTimes(0);
       expect(onCompleted1).toHaveBeenCalledTimes(1);
-      expect(onCompleted1).toHaveBeenCalledWith(CREATE_TODO_DATA, expect.objectContaining({variables}));
+      expect(onCompleted1).toHaveBeenCalledWith(
+        CREATE_TODO_DATA,
+        expect.objectContaining({ variables })
+      );
     });
   });
 
-  describe('ROOT_MUTATION cache data', () => {
+  describe("ROOT_MUTATION cache data", () => {
     const startTime = Date.now();
-    const link = new ApolloLink(operation => new Observable(observer => {
-      observer.next({
-        data: {
-          __typename: "Mutation",
-          doSomething: {
-            __typename: "MutationPayload",
-            time: startTime,
-          },
-        },
-      });
-      observer.complete();
-    }));
+    const link = new ApolloLink(
+      (operation) =>
+        new Observable((observer) => {
+          observer.next({
+            data: {
+              __typename: "Mutation",
+              doSomething: {
+                __typename: "MutationPayload",
+                time: startTime,
+              },
+            },
+          });
+          observer.complete();
+        })
+    );
 
     const mutation = gql`
       mutation DoSomething {
@@ -1040,7 +1147,7 @@ describe('useMutation Hook', () => {
       }
     `;
 
-    it('should be removed by default after the mutation', async () => {
+    it("should be removed by default after the mutation", async () => {
       let timeReadCount = 0;
       let timeMergeCount = 0;
       const client = new ApolloClient({
@@ -1066,31 +1173,28 @@ describe('useMutation Hook', () => {
         }),
       });
 
-      const { result } = renderHook(
-        () => useMutation(mutation),
-        { wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        )},
-      );
+      const { result } = renderHook(() => useMutation(mutation), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current[1].loading).toBe(false);
       expect(result.current[1].called).toBe(false);
       expect(result.current[1].data).toBe(undefined);
       const mutate = result.current[0];
 
-      let mutationResult: any
+      let mutationResult: any;
       act(() => {
         mutationResult = mutate({
-          update(cache, {
-            data: {
-              doSomething: {
-                __typename,
-                time,
+          update(
+            cache,
+            {
+              data: {
+                doSomething: { __typename, time },
               },
-            },
-          }) {
+            }
+          ) {
             expect(__typename).toBe("MutationPayload");
             expect(time).toBeInstanceOf(Date);
             expect(time.getTime()).toBe(startTime);
@@ -1109,28 +1213,27 @@ describe('useMutation Hook', () => {
               },
             });
           },
-        }).then(({
-          data: {
-            doSomething: {
-              __typename,
-              time,
-            },
-          },
-        }) => {
-          expect(__typename).toBe("MutationPayload");
-          expect(time).toBeInstanceOf(Date);
-          expect(time.getTime()).toBe(startTime);
-          expect(timeReadCount).toBe(1);
-          expect(timeMergeCount).toBe(1);
-          // The contents of the ROOT_MUTATION object exist only briefly,
-          // for the duration of the mutation update, and are removed after
-          // the mutation write is finished.
-          expect(client.cache.extract()).toEqual({
-            ROOT_MUTATION: {
-              __typename: "Mutation",
+        }).then(
+          ({
+            data: {
+              doSomething: { __typename, time },
             },
-          });
-        });
+          }) => {
+            expect(__typename).toBe("MutationPayload");
+            expect(time).toBeInstanceOf(Date);
+            expect(time.getTime()).toBe(startTime);
+            expect(timeReadCount).toBe(1);
+            expect(timeMergeCount).toBe(1);
+            // The contents of the ROOT_MUTATION object exist only briefly,
+            // for the duration of the mutation update, and are removed after
+            // the mutation write is finished.
+            expect(client.cache.extract()).toEqual({
+              ROOT_MUTATION: {
+                __typename: "Mutation",
+              },
+            });
+          }
+        );
         mutationResult.catch(() => {});
       });
 
@@ -1145,19 +1248,16 @@ describe('useMutation Hook', () => {
       expect(result.current[1].data).toBeDefined();
 
       const {
-        doSomething: {
-          __typename,
-          time,
-        },
+        doSomething: { __typename, time },
       } = result.current[1].data;
-      expect(__typename).toBe('MutationPayload');
+      expect(__typename).toBe("MutationPayload");
       expect(time).toBeInstanceOf(Date);
       expect(time.getTime()).toBe(startTime);
 
       await expect(mutationResult).resolves.toBe(undefined);
     });
 
-    it('can be preserved by passing keepRootFields: true', async () => {
+    it("can be preserved by passing keepRootFields: true", async () => {
       let timeReadCount = 0;
       let timeMergeCount = 0;
 
@@ -1185,14 +1285,15 @@ describe('useMutation Hook', () => {
       });
 
       const { result } = renderHook(
-        () => useMutation(mutation, {
-          keepRootFields: true,
-        }),
-        { wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        )},
+        () =>
+          useMutation(mutation, {
+            keepRootFields: true,
+          }),
+        {
+          wrapper: ({ children }) => (
+            <ApolloProvider client={client}>{children}</ApolloProvider>
+          ),
+        }
       );
 
       expect(result.current[1].loading).toBe(false);
@@ -1200,17 +1301,17 @@ describe('useMutation Hook', () => {
       expect(result.current[1].data).toBe(undefined);
       const mutate = result.current[0];
 
-      let mutationResult: any
+      let mutationResult: any;
       act(() => {
         mutationResult = mutate({
-          update(cache, {
-            data: {
-              doSomething: {
-                __typename,
-                time,
+          update(
+            cache,
+            {
+              data: {
+                doSomething: { __typename, time },
               },
-            },
-          }) {
+            }
+          ) {
             expect(__typename).toBe("MutationPayload");
             expect(time).toBeInstanceOf(Date);
             expect(time.getTime()).toBe(startTime);
@@ -1226,29 +1327,28 @@ describe('useMutation Hook', () => {
               },
             });
           },
-        }).then(({
-          data: {
-            doSomething: {
-              __typename,
-              time,
+        }).then(
+          ({
+            data: {
+              doSomething: { __typename, time },
             },
-          },
-        }) => {
-          expect(__typename).toBe("MutationPayload");
-          expect(time).toBeInstanceOf(Date);
-          expect(time.getTime()).toBe(startTime);
-          expect(timeReadCount).toBe(1);
-          expect(timeMergeCount).toBe(1);
-          expect(client.cache.extract()).toEqual({
-            ROOT_MUTATION: {
-              __typename: "Mutation",
-              doSomething: {
-                __typename: "MutationPayload",
-                time: startTime,
+          }) => {
+            expect(__typename).toBe("MutationPayload");
+            expect(time).toBeInstanceOf(Date);
+            expect(time.getTime()).toBe(startTime);
+            expect(timeReadCount).toBe(1);
+            expect(timeMergeCount).toBe(1);
+            expect(client.cache.extract()).toEqual({
+              ROOT_MUTATION: {
+                __typename: "Mutation",
+                doSomething: {
+                  __typename: "MutationPayload",
+                  time: startTime,
+                },
               },
-            },
-          });
-        });
+            });
+          }
+        );
       });
 
       mutationResult.catch(() => {});
@@ -1263,12 +1363,9 @@ describe('useMutation Hook', () => {
       expect(result.current[1].data).toBeDefined();
 
       const {
-        doSomething: {
-          __typename,
-          time,
-        },
+        doSomething: { __typename, time },
       } = result.current[1].data;
-      expect(__typename).toBe('MutationPayload');
+      expect(__typename).toBe("MutationPayload");
       expect(time).toBeInstanceOf(Date);
       expect(time.getTime()).toBe(startTime);
 
@@ -1276,31 +1373,28 @@ describe('useMutation Hook', () => {
     });
   });
 
-  describe('Update function', () => {
-    it('should be called with the provided variables', async () => {
-      const variables = { description: 'Get milk!' };
+  describe("Update function", () => {
+    it("should be called with the provided variables", async () => {
+      const variables = { description: "Get milk!" };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
+          result: { data: CREATE_TODO_RESULT },
+        },
       ];
 
       let variablesMatched = false;
       const Component = () => {
-        const [createTodo] = useMutation(
-          CREATE_TODO_MUTATION,
-          {
-            update(_, __, options) {
-              expect(options.variables).toEqual(variables);
-              variablesMatched = true;
-            }
-          }
-        );
+        const [createTodo] = useMutation(CREATE_TODO_MUTATION, {
+          update(_, __, options) {
+            expect(options.variables).toEqual(variables);
+            variablesMatched = true;
+          },
+        });
 
         useEffect(() => {
           createTodo({ variables });
@@ -1318,35 +1412,36 @@ describe('useMutation Hook', () => {
       await waitFor(() => expect(variablesMatched).toBe(true));
     });
 
-    itAsync('should be called with the provided context', (resolve, reject) => {
+    itAsync("should be called with the provided context", (resolve, reject) => {
       const context = { id: 3 };
 
       const variables = {
-        description: 'Get milk!'
+        description: "Get milk!",
       };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
+          result: { data: CREATE_TODO_RESULT },
+        },
       ];
 
       let foundContext = false;
       const Component = () => {
-        const [createTodo] = useMutation<Todo, { description: string }, { id: number }>(
-          CREATE_TODO_MUTATION,
-          {
-            context,
-            update(_, __, options) {
-              expect(options.context).toEqual(context);
-              foundContext = true;
-            }
-          }
-        );
+        const [createTodo] = useMutation<
+          Todo,
+          { description: string },
+          { id: number }
+        >(CREATE_TODO_MUTATION, {
+          context,
+          update(_, __, options) {
+            expect(options.context).toEqual(context);
+            foundContext = true;
+          },
+        });
 
         useEffect(() => {
           createTodo({ variables });
@@ -1366,33 +1461,30 @@ describe('useMutation Hook', () => {
       }).then(resolve, reject);
     });
 
-    describe('If context is not provided', () => {
-      itAsync('should be undefined', (resolve, reject) => {
+    describe("If context is not provided", () => {
+      itAsync("should be undefined", (resolve, reject) => {
         const variables = {
-          description: 'Get milk!'
+          description: "Get milk!",
         };
 
         const mocks = [
           {
             request: {
               query: CREATE_TODO_MUTATION,
-              variables
+              variables,
             },
-            result: { data: CREATE_TODO_RESULT }
-          }
+            result: { data: CREATE_TODO_RESULT },
+          },
         ];
 
         let checkedContext = false;
         const Component = () => {
-          const [createTodo] = useMutation(
-            CREATE_TODO_MUTATION,
-            {
-              update(_, __, options) {
-                expect(options.context).toBeUndefined();
-                checkedContext = true;
-              }
-            }
-          );
+          const [createTodo] = useMutation(CREATE_TODO_MUTATION, {
+            update(_, __, options) {
+              expect(options.context).toBeUndefined();
+              checkedContext = true;
+            },
+          });
 
           useEffect(() => {
             createTodo({ variables });
@@ -1414,123 +1506,123 @@ describe('useMutation Hook', () => {
     });
   });
 
-  describe('Optimistic response', () => {
-    itAsync('should support optimistic response handling', async (resolve, reject) => {
-      const optimisticResponse = {
-        __typename: 'Mutation',
-        createTodo: {
-          id: 1,
-          description: 'TEMPORARY',
-          priority: 'High',
-          __typename: 'Todo'
-        }
-      };
+  describe("Optimistic response", () => {
+    itAsync(
+      "should support optimistic response handling",
+      async (resolve, reject) => {
+        const optimisticResponse = {
+          __typename: "Mutation",
+          createTodo: {
+            id: 1,
+            description: "TEMPORARY",
+            priority: "High",
+            __typename: "Todo",
+          },
+        };
 
-      const variables = {
-        description: 'Get milk!'
-      };
+        const variables = {
+          description: "Get milk!",
+        };
 
-      const mocks = [
-        {
-          request: {
-            query: CREATE_TODO_MUTATION,
-            variables
+        const mocks = [
+          {
+            request: {
+              query: CREATE_TODO_MUTATION,
+              variables,
+            },
+            result: { data: CREATE_TODO_RESULT },
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
-      ];
-
-      const link = mockSingleLink(...mocks).setOnError(reject);
-      const cache = new InMemoryCache();
-      const client = new ApolloClient({
-        cache,
-        link
-      });
+        ];
 
-      let renderCount = 0;
-      const Component = () => {
-        const [createTodo, { loading, data }] = useMutation(
-          CREATE_TODO_MUTATION,
-          { optimisticResponse }
-        );
+        const link = mockSingleLink(...mocks).setOnError(reject);
+        const cache = new InMemoryCache();
+        const client = new ApolloClient({
+          cache,
+          link,
+        });
 
-        switch (renderCount) {
-          case 0:
-            expect(loading).toBeFalsy();
-            expect(data).toBeUndefined();
-            createTodo({ variables });
+        let renderCount = 0;
+        const Component = () => {
+          const [createTodo, { loading, data }] = useMutation(
+            CREATE_TODO_MUTATION,
+            { optimisticResponse }
+          );
 
-            const dataInStore = client.cache.extract(true);
-            expect(dataInStore['Todo:1']).toEqual(
-              optimisticResponse.createTodo
-            );
-
-            break;
-          case 1:
-            expect(loading).toBeTruthy();
-            expect(data).toBeUndefined();
-            break;
-          case 2:
-            expect(loading).toBeFalsy();
-            expect(data).toEqual(CREATE_TODO_RESULT);
-            break;
-          default:
-        }
-        renderCount += 1;
-        return null;
-      };
+          switch (renderCount) {
+            case 0:
+              expect(loading).toBeFalsy();
+              expect(data).toBeUndefined();
+              createTodo({ variables });
+
+              const dataInStore = client.cache.extract(true);
+              expect(dataInStore["Todo:1"]).toEqual(
+                optimisticResponse.createTodo
+              );
+
+              break;
+            case 1:
+              expect(loading).toBeTruthy();
+              expect(data).toBeUndefined();
+              break;
+            case 2:
+              expect(loading).toBeFalsy();
+              expect(data).toEqual(CREATE_TODO_RESULT);
+              break;
+            default:
+          }
+          renderCount += 1;
+          return null;
+        };
 
-      render(
-        <ApolloProvider client={client}>
-          <Component />
-        </ApolloProvider>
-      );
+        render(
+          <ApolloProvider client={client}>
+            <Component />
+          </ApolloProvider>
+        );
 
-      return waitFor(() => {
-        expect(renderCount).toBe(3);
-      }).then(resolve, reject);
-    });
+        return waitFor(() => {
+          expect(renderCount).toBe(3);
+        }).then(resolve, reject);
+      }
+    );
 
-    it('should be called with the provided context', async () => {
+    it("should be called with the provided context", async () => {
       const optimisticResponse = {
-        __typename: 'Mutation',
+        __typename: "Mutation",
         createTodo: {
           id: 1,
-          description: 'TEMPORARY',
-          priority: 'High',
-          __typename: 'Todo'
-        }
+          description: "TEMPORARY",
+          priority: "High",
+          __typename: "Todo",
+        },
       };
 
       const context = { id: 3 };
 
       const variables = {
-        description: 'Get milk!'
+        description: "Get milk!",
       };
 
       const mocks = [
         {
           request: {
             query: CREATE_TODO_MUTATION,
-            variables
+            variables,
           },
-          result: { data: CREATE_TODO_RESULT }
-        }
+          result: { data: CREATE_TODO_RESULT },
+        },
       ];
 
       const contextFn = jest.fn();
 
       const Component = () => {
-        const [createTodo] = useMutation(
-          CREATE_TODO_MUTATION,
-          {
-            optimisticResponse,
-            context,
-            update(_, __, options) {
-              contextFn(options.context);
-            }
-          }
-        );
+        const [createTodo] = useMutation(CREATE_TODO_MUTATION, {
+          optimisticResponse,
+          context,
+          update(_, __, options) {
+            contextFn(options.context);
+          },
+        });
 
         useEffect(() => {
           createTodo({ variables });
@@ -1552,7 +1644,7 @@ describe('useMutation Hook', () => {
     });
   });
 
-  describe('refetching queries', () => {
+  describe("refetching queries", () => {
     const GET_TODOS_QUERY = gql`
       query getTodos {
         todos {
@@ -1567,15 +1659,15 @@ describe('useMutation Hook', () => {
       todos: [
         {
           id: 2,
-          description: 'Walk the dog',
-          priority: 'Medium',
-          __typename: 'Todo'
+          description: "Walk the dog",
+          priority: "Medium",
+          __typename: "Todo",
         },
         {
           id: 3,
-          description: 'Call mom',
-          priority: 'Low',
-          __typename: 'Todo'
+          description: "Call mom",
+          priority: "Low",
+          __typename: "Todo",
         },
       ],
     };
@@ -1584,45 +1676,47 @@ describe('useMutation Hook', () => {
       todos: [
         {
           id: 1,
-          description: 'Get milk!',
-          priority: 'High',
-          __typename: 'Todo'
+          description: "Get milk!",
+          priority: "High",
+          __typename: "Todo",
         },
         {
           id: 2,
-          description: 'Walk the dog',
-          priority: 'Medium',
-          __typename: 'Todo'
+          description: "Walk the dog",
+          priority: "Medium",
+          __typename: "Todo",
         },
         {
           id: 3,
-          description: 'Call mom',
-          priority: 'Low',
-          __typename: 'Todo'
+          description: "Call mom",
+          priority: "Low",
+          __typename: "Todo",
         },
       ],
     };
 
-    it('can pass onQueryUpdated to useMutation', async () => {
+    it("can pass onQueryUpdated to useMutation", async () => {
       interface TData {
         todoCount: number;
       }
       const countQuery: TypedDocumentNode<TData> = gql`
-        query Count { todoCount @client }
+        query Count {
+          todoCount @client
+        }
       `;
 
       const optimisticResponse = {
-        __typename: 'Mutation',
+        __typename: "Mutation",
         createTodo: {
           id: 1,
-          description: 'TEMPORARY',
-          priority: 'High',
-          __typename: 'Todo'
-        }
+          description: "TEMPORARY",
+          priority: "High",
+          __typename: "Todo",
+        },
       };
 
       const variables = {
-        description: 'Get milk!'
+        description: "Get milk!",
       };
 
       const client = new ApolloClient({
@@ -1657,7 +1751,7 @@ describe('useMutation Hook', () => {
         result: ApolloQueryResult<TData>;
       }
       let resolveOnUpdate: (results: OnQueryUpdatedResults) => any;
-      const onUpdatePromise = new Promise<OnQueryUpdatedResults>(resolve => {
+      const onUpdatePromise = new Promise<OnQueryUpdatedResults>((resolve) => {
         resolveOnUpdate = resolve;
       }).then((onUpdateResult) => {
         expect(finishedReobserving).toBe(true);
@@ -1679,28 +1773,29 @@ describe('useMutation Hook', () => {
       onUpdatePromise.catch(() => {});
 
       let finishedReobserving = false;
-      const { result } = renderHook(() => ({
-        query: useQuery(countQuery),
-        mutation: useMutation(CREATE_TODO_MUTATION, {
-          optimisticResponse,
-          update(cache) {
-            const result = cache.readQuery({ query: countQuery });
+      const { result } = renderHook(
+        () => ({
+          query: useQuery(countQuery),
+          mutation: useMutation(CREATE_TODO_MUTATION, {
+            optimisticResponse,
+            update(cache) {
+              const result = cache.readQuery({ query: countQuery });
 
-            cache.writeQuery({
-              query: countQuery,
-              data: {
-                todoCount: (result ? result.todoCount : 0) + 1,
-              },
-            });
-          },
+              cache.writeQuery({
+                query: countQuery,
+                data: {
+                  todoCount: (result ? result.todoCount : 0) + 1,
+                },
+              });
+            },
+          }),
         }),
-      }), {
-        wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        ),
-      });
+        {
+          wrapper: ({ children }) => (
+            <ApolloProvider client={client}>{children}</ApolloProvider>
+          ),
+        }
+      );
 
       expect(result.current.query.loading).toBe(false);
       expect(result.current.query.data).toEqual({ todoCount: 0 });
@@ -1725,16 +1820,22 @@ describe('useMutation Hook', () => {
       expect(result.current.mutation[1].data).toBe(undefined);
       expect(finishedReobserving).toBe(false);
 
-      await waitFor(() => {
-        expect(result.current.query.data).toEqual({ todoCount: 1 });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.data).toEqual({ todoCount: 1 });
+        },
+        { interval: 1 }
+      );
       expect(result.current.query.loading).toBe(false);
       expect(result.current.mutation[1].loading).toBe(true);
       expect(result.current.mutation[1].data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.mutation[1].loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.mutation[1].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.query.loading).toBe(false);
       expect(result.current.query.data).toEqual({ todoCount: 1 });
       expect(result.current.mutation[1].data).toEqual(CREATE_TODO_RESULT);
@@ -1743,8 +1844,8 @@ describe('useMutation Hook', () => {
       await expect(onUpdatePromise).resolves.toBe(undefined);
     });
 
-    it('refetchQueries with operation names should update cache', async () => {
-      const variables = { description: 'Get milk!' };
+    it("refetchQueries with operation names should update cache", async () => {
+      const variables = { description: "Get milk!" };
       const mocks = [
         {
           request: {
@@ -1782,39 +1883,44 @@ describe('useMutation Hook', () => {
         }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.query.loading).toBe(true);
       expect(result.current.query.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.query.data).toEqual(mocks[0].result.data);
       const mutate = result.current.mutation[0];
       act(() => {
         mutate({
           variables,
-          refetchQueries: ['getTodos'],
+          refetchQueries: ["getTodos"],
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.query.data).toEqual(mocks[2].result.data);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.data).toEqual(mocks[2].result.data);
+        },
+        { interval: 1 }
+      );
       expect(result.current.query.loading).toBe(false);
 
-      expect(client.readQuery({ query: GET_TODOS_QUERY}))
-        .toEqual(mocks[2].result.data);
+      expect(client.readQuery({ query: GET_TODOS_QUERY })).toEqual(
+        mocks[2].result.data
+      );
     });
 
-    it('refetchQueries with document nodes should update cache', async () => {
-      const variables = { description: 'Get milk!' };
+    it("refetchQueries with document nodes should update cache", async () => {
+      const variables = { description: "Get milk!" };
       const mocks = [
         {
           request: {
@@ -1854,18 +1960,19 @@ describe('useMutation Hook', () => {
         }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.query.loading).toBe(true);
       expect(result.current.query.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.query.data).toEqual(mocks[0].result.data);
       const mutate = result.current.mutation[0];
@@ -1885,16 +1992,20 @@ describe('useMutation Hook', () => {
       expect(result.current.query.loading).toBe(false);
       expect(result.current.query.data).toEqual(mocks[0].result.data);
 
-      await waitFor(() => {
-        expect(result.current.query.data).toEqual(mocks[2].result.data);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.data).toEqual(mocks[2].result.data);
+        },
+        { interval: 1 }
+      );
       expect(result.current.query.loading).toBe(false);
-      expect(client.readQuery({ query: GET_TODOS_QUERY }))
-        .toEqual(mocks[2].result.data);
+      expect(client.readQuery({ query: GET_TODOS_QUERY })).toEqual(
+        mocks[2].result.data
+      );
     });
 
-    it('refetchQueries should update cache after unmount', async () => {
-      const variables = { description: 'Get milk!' };
+    it("refetchQueries should update cache after unmount", async () => {
+      const variables = { description: "Get milk!" };
       const mocks = [
         {
           request: {
@@ -1932,33 +2043,36 @@ describe('useMutation Hook', () => {
         }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.query.loading).toBe(true);
       expect(result.current.query.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.query.data).toEqual(mocks[0].result.data);
       const mutate = result.current.mutation[0];
       let onMutationDone: Function;
-      const mutatePromise = new Promise((resolve) => (onMutationDone = resolve));
+      const mutatePromise = new Promise(
+        (resolve) => (onMutationDone = resolve)
+      );
 
       setTimeout(() => {
         act(() => {
           mutate({
             variables,
-            refetchQueries: ['getTodos'],
+            refetchQueries: ["getTodos"],
             update() {
               unmount();
             },
-          }).then(result => {
+          }).then((result) => {
             expect(result.data).toEqual(CREATE_TODO_RESULT);
             onMutationDone();
           });
@@ -1971,304 +2085,322 @@ describe('useMutation Hook', () => {
       await mutatePromise;
 
       await waitFor(() => {
-        expect(
-          client.readQuery({ query: GET_TODOS_QUERY })
-        ).toEqual(mocks[2].result.data);
+        expect(client.readQuery({ query: GET_TODOS_QUERY })).toEqual(
+          mocks[2].result.data
+        );
       });
     });
 
-    itAsync("using onQueryUpdated callback should not prevent cache broadcast", async (resolve, reject) => {
-      // Mutating this array makes the tests below much more difficult to reason
-      // about, so instead we reassign the numbersArray variable to remove
-      // elements, without mutating the previous array object.
-      let numbersArray: ReadonlyArray<{ id: string; value: number }> = [
-        { id: '1', value: 324 },
-        { id: '2', value: 729 },
-        { id: '3', value: 987 },
-        { id: '4', value: 344 },
-        { id: '5', value: 72 },
-        { id: '6', value: 899 },
-        { id: '7', value: 222 },
-      ];
+    itAsync(
+      "using onQueryUpdated callback should not prevent cache broadcast",
+      async (resolve, reject) => {
+        // Mutating this array makes the tests below much more difficult to reason
+        // about, so instead we reassign the numbersArray variable to remove
+        // elements, without mutating the previous array object.
+        let numbersArray: ReadonlyArray<{ id: string; value: number }> = [
+          { id: "1", value: 324 },
+          { id: "2", value: 729 },
+          { id: "3", value: 987 },
+          { id: "4", value: 344 },
+          { id: "5", value: 72 },
+          { id: "6", value: 899 },
+          { id: "7", value: 222 },
+        ];
 
-      type TNumbersQuery = {
-        numbers: {
-          __typename: "NumbersResult";
-          id: string;
-          sum: number;
-          numbersArray: ReadonlyArray<{
+        type TNumbersQuery = {
+          numbers: {
+            __typename: "NumbersResult";
             id: string;
-            value: number;
-          }>;
+            sum: number;
+            numbersArray: ReadonlyArray<{
+              id: string;
+              value: number;
+            }>;
+          };
         };
-      };
 
-      function getNumbersData(): TNumbersQuery {
-        return {
-          numbers: {
-            __typename: "NumbersResult",
-            id: "numbersId",
-            numbersArray,
-            sum: numbersArray.reduce((sum, b) => sum + b.value, 0),
-          },
-        };
-      }
+        function getNumbersData(): TNumbersQuery {
+          return {
+            numbers: {
+              __typename: "NumbersResult",
+              id: "numbersId",
+              numbersArray,
+              sum: numbersArray.reduce((sum, b) => sum + b.value, 0),
+            },
+          };
+        }
 
-      const link = new ApolloLink((operation) => {
-        return new Observable(observer => {
-          const { operationName } = operation;
-          if (operationName === "NumbersQuery") {
-            observer.next({
-              data: getNumbersData(),
-            });
-          } else if (operationName === "RemoveNumberMutation") {
-            const last = numbersArray[numbersArray.length - 1];
-            numbersArray = numbersArray.slice(0, -1);
-            observer.next({
-              data: {
-                removeLastNumber: last,
-              },
-            });
-          }
-          setTimeout(() => {
-            observer.complete();
-          }, 50);
+        const link = new ApolloLink((operation) => {
+          return new Observable((observer) => {
+            const { operationName } = operation;
+            if (operationName === "NumbersQuery") {
+              observer.next({
+                data: getNumbersData(),
+              });
+            } else if (operationName === "RemoveNumberMutation") {
+              const last = numbersArray[numbersArray.length - 1];
+              numbersArray = numbersArray.slice(0, -1);
+              observer.next({
+                data: {
+                  removeLastNumber: last,
+                },
+              });
+            }
+            setTimeout(() => {
+              observer.complete();
+            }, 50);
+          });
         });
-      });
 
-      const client = new ApolloClient({
-        link,
-        cache: new InMemoryCache({
-          typePolicies: {
-            NumbersResult: {
-              fields: {
-                numbersArray: { merge: false },
-                sum(_, { readField }) {
-                  const numbersArray =
-                    readField<TNumbersQuery["numbers"]["numbersArray"]>("numbersArray");
-                  return (numbersArray || []).reduce(
-                    (sum, item) => sum + item.value,
-                    0,
-                  );
+        const client = new ApolloClient({
+          link,
+          cache: new InMemoryCache({
+            typePolicies: {
+              NumbersResult: {
+                fields: {
+                  numbersArray: { merge: false },
+                  sum(_, { readField }) {
+                    const numbersArray =
+                      readField<TNumbersQuery["numbers"]["numbersArray"]>(
+                        "numbersArray"
+                      );
+                    return (numbersArray || []).reduce(
+                      (sum, item) => sum + item.value,
+                      0
+                    );
+                  },
                 },
               },
             },
-          },
-        }),
-      });
+          }),
+        });
 
-      const NumbersQuery: TypedDocumentNode<TNumbersQuery> = gql`
-        query NumbersQuery {
-          numbers {
-            id
-            sum
-            numbersArray {
+        const NumbersQuery: TypedDocumentNode<TNumbersQuery> = gql`
+          query NumbersQuery {
+            numbers {
               id
-              value
+              sum
+              numbersArray {
+                id
+                value
+              }
             }
           }
-        }
-      `;
+        `;
 
-      const RemoveNumberMutation = gql`
-        mutation RemoveNumberMutation {
-          removeLastNumber {
-            id
+        const RemoveNumberMutation = gql`
+          mutation RemoveNumberMutation {
+            removeLastNumber {
+              id
+            }
           }
-        }
-      `;
-
-      const { result } = renderHook(() => ({
-        query: useQuery(NumbersQuery, {
-          notifyOnNetworkStatusChange: true,
-        }),
+        `;
 
-        mutation: useMutation(RemoveNumberMutation, {
-          update(cache) {
-            const oldData = cache.readQuery({ query: NumbersQuery });
-            cache.writeQuery({
-              query: NumbersQuery,
-              data: oldData ? {
-                ...oldData,
-                numbers: {
-                  ...oldData.numbers,
-                  numbersArray: oldData.numbers.numbersArray.slice(0, -1),
-                },
-              } : {
-                numbers: {
-                  __typename: "NumbersResult",
-                  id: "numbersId",
-                  sum: 0,
-                  numbersArray: [],
-                },
+        const { result } = renderHook(
+          () => ({
+            query: useQuery(NumbersQuery, {
+              notifyOnNetworkStatusChange: true,
+            }),
+
+            mutation: useMutation(RemoveNumberMutation, {
+              update(cache) {
+                const oldData = cache.readQuery({ query: NumbersQuery });
+                cache.writeQuery({
+                  query: NumbersQuery,
+                  data: oldData
+                    ? {
+                        ...oldData,
+                        numbers: {
+                          ...oldData.numbers,
+                          numbersArray: oldData.numbers.numbersArray.slice(
+                            0,
+                            -1
+                          ),
+                        },
+                      }
+                    : {
+                        numbers: {
+                          __typename: "NumbersResult",
+                          id: "numbersId",
+                          sum: 0,
+                          numbersArray: [],
+                        },
+                      },
+                });
               },
-            });
-          },
-        }),
-      }), {
-        wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        ),
-      });
-
-      const obsQueryMap = client.getObservableQueries();
-      expect(obsQueryMap.size).toBe(1);
-      const observedResults: Array<{ data: TNumbersQuery }> = [];
-      subscribeAndCount(reject, obsQueryMap.values().next().value, (
-        count,
-        result: { data: TNumbersQuery },
-      ) => {
-        observedResults.push(result);
-        expect(observedResults.length).toBe(count);
-        const data = getNumbersData();
-
-        if (count === 1) {
-          expect(result).toEqual({
-            loading: true,
-            networkStatus: NetworkStatus.loading,
-            partial: true,
-          });
-
-        } else if (count === 2) {
-          expect(data.numbers.numbersArray.length).toBe(7);
-          expect(result).toEqual({
-            loading: false,
-            networkStatus: NetworkStatus.ready,
-            data,
-          });
-
-        } else if (count === 3) {
-          expect(data.numbers.numbersArray.length).toBe(6);
-          expect(result).toEqual({
-            loading: false,
-            networkStatus: NetworkStatus.ready,
-            data,
-          });
-
-        } else if (count === 4) {
-          expect(data.numbers.numbersArray.length).toBe(5);
-          expect(result).toEqual({
-            loading: false,
-            networkStatus: NetworkStatus.ready,
-            data,
-          });
-
-          // This line is the only way to finish this test successfully.
-          setTimeout(resolve, 50);
+            }),
+          }),
+          {
+            wrapper: ({ children }) => (
+              <ApolloProvider client={client}>{children}</ApolloProvider>
+            ),
+          }
+        );
 
-        } else {
-          // If we did not return false from the final onQueryUpdated function,
-          // we would receive an additional result here.
-          reject(`too many renders (${count}); final result: ${
-            JSON.stringify(result)
-          }`);
-        }
-      });
+        const obsQueryMap = client.getObservableQueries();
+        expect(obsQueryMap.size).toBe(1);
+        const observedResults: Array<{ data: TNumbersQuery }> = [];
+        subscribeAndCount(
+          reject,
+          obsQueryMap.values().next().value,
+          (count, result: { data: TNumbersQuery }) => {
+            observedResults.push(result);
+            expect(observedResults.length).toBe(count);
+            const data = getNumbersData();
+
+            if (count === 1) {
+              expect(result).toEqual({
+                loading: true,
+                networkStatus: NetworkStatus.loading,
+                partial: true,
+              });
+            } else if (count === 2) {
+              expect(data.numbers.numbersArray.length).toBe(7);
+              expect(result).toEqual({
+                loading: false,
+                networkStatus: NetworkStatus.ready,
+                data,
+              });
+            } else if (count === 3) {
+              expect(data.numbers.numbersArray.length).toBe(6);
+              expect(result).toEqual({
+                loading: false,
+                networkStatus: NetworkStatus.ready,
+                data,
+              });
+            } else if (count === 4) {
+              expect(data.numbers.numbersArray.length).toBe(5);
+              expect(result).toEqual({
+                loading: false,
+                networkStatus: NetworkStatus.ready,
+                data,
+              });
+
+              // This line is the only way to finish this test successfully.
+              setTimeout(resolve, 50);
+            } else {
+              // If we did not return false from the final onQueryUpdated function,
+              // we would receive an additional result here.
+              reject(
+                `too many renders (${count}); final result: ${JSON.stringify(
+                  result
+                )}`
+              );
+            }
+          }
+        );
 
-      expect(observedResults).toEqual([]);
+        expect(observedResults).toEqual([]);
 
-      expect(result.current.query.loading).toBe(true);
-      expect(result.current.query.networkStatus).toBe(NetworkStatus.loading);
-      expect(result.current.mutation[1].loading).toBe(false);
-      expect(result.current.mutation[1].called).toBe(false);
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
+        expect(result.current.query.loading).toBe(true);
+        expect(result.current.query.networkStatus).toBe(NetworkStatus.loading);
+        expect(result.current.mutation[1].loading).toBe(false);
+        expect(result.current.mutation[1].called).toBe(false);
+        await waitFor(
+          () => {
+            expect(result.current.query.loading).toBe(false);
+          },
+          { interval: 1 }
+        );
 
-      expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
-      expect(result.current.mutation[1].loading).toBe(false);
-      expect(result.current.mutation[1].called).toBe(false);
+        expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
+        expect(result.current.mutation[1].loading).toBe(false);
+        expect(result.current.mutation[1].called).toBe(false);
 
-      expect(numbersArray[numbersArray.length - 1]).toEqual({
-        id: '7',
-        value: 222,
-      });
+        expect(numbersArray[numbersArray.length - 1]).toEqual({
+          id: "7",
+          value: 222,
+        });
 
-      const [mutate] = result.current.mutation;
-      await act(async () => {
-        expect(await mutate(
-          // Not passing an onQueryUpdated callback should allow cache
-          // broadcasts to propagate as normal. The point of this test is to
-          // demonstrate that *adding* onQueryUpdated should not prevent cache
-          // broadcasts (see below for where we test that).
-        )).toEqual({
-          data: {
-            removeLastNumber: {
-              id: '7',
+        const [mutate] = result.current.mutation;
+        await act(async () => {
+          expect(
+            await mutate()
+            // Not passing an onQueryUpdated callback should allow cache
+            // broadcasts to propagate as normal. The point of this test is to
+            // demonstrate that *adding* onQueryUpdated should not prevent cache
+            // broadcasts (see below for where we test that).
+          ).toEqual({
+            data: {
+              removeLastNumber: {
+                id: "7",
+              },
             },
-          },
+          });
         });
-      });
 
-      expect(numbersArray[numbersArray.length - 1]).toEqual({
-        id: '6',
-        value: 899,
-      });
+        expect(numbersArray[numbersArray.length - 1]).toEqual({
+          id: "6",
+          value: 899,
+        });
 
-      expect(result.current.query.loading).toBe(false);
-      expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
-      expect(result.current.mutation[1].loading).toBe(false);
-      expect(result.current.mutation[1].called).toBe(true);
+        expect(result.current.query.loading).toBe(false);
+        expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
+        expect(result.current.mutation[1].loading).toBe(false);
+        expect(result.current.mutation[1].called).toBe(true);
 
-      await act(async () => {
-        expect(await mutate({
-          // Adding this onQueryUpdated callback, which merely examines the
-          // updated query and its DiffResult, should not change the broadcast
-          // behavior of the ObservableQuery.
-          onQueryUpdated(oq, diff) {
-            expect(oq.queryName).toBe("NumbersQuery");
-            expect(diff.result.numbers.numbersArray.length).toBe(5);
-            expect(diff.result.numbers.sum).toBe(2456);
-          },
-        })).toEqual({
-          data: {
-            removeLastNumber: {
-              id: '6',
+        await act(async () => {
+          expect(
+            await mutate({
+              // Adding this onQueryUpdated callback, which merely examines the
+              // updated query and its DiffResult, should not change the broadcast
+              // behavior of the ObservableQuery.
+              onQueryUpdated(oq, diff) {
+                expect(oq.queryName).toBe("NumbersQuery");
+                expect(diff.result.numbers.numbersArray.length).toBe(5);
+                expect(diff.result.numbers.sum).toBe(2456);
+              },
+            })
+          ).toEqual({
+            data: {
+              removeLastNumber: {
+                id: "6",
+              },
             },
-          },
+          });
         });
-      });
 
-      expect(numbersArray[numbersArray.length - 1]).toEqual({
-        id: '5',
-        value: 72,
-      });
+        expect(numbersArray[numbersArray.length - 1]).toEqual({
+          id: "5",
+          value: 72,
+        });
 
-      expect(result.current.query.loading).toBe(false);
-      expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
-      expect(result.current.mutation[1].loading).toBe(false);
-      expect(result.current.mutation[1].called).toBe(true);
+        expect(result.current.query.loading).toBe(false);
+        expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
+        expect(result.current.mutation[1].loading).toBe(false);
+        expect(result.current.mutation[1].called).toBe(true);
 
-      await act(async () => {
-        expect(await mutate({
-          onQueryUpdated(oq, diff) {
-            expect(oq.queryName).toBe("NumbersQuery");
-            expect(diff.result.numbers.numbersArray.length).toBe(4);
-            expect(diff.result.numbers.sum).toBe(2384);
-            // Returning false from onQueryUpdated prevents the cache broadcast.
-            return false;
-          },
-        })).toEqual({
-          data: {
-            removeLastNumber: {
-              id: '5',
+        await act(async () => {
+          expect(
+            await mutate({
+              onQueryUpdated(oq, diff) {
+                expect(oq.queryName).toBe("NumbersQuery");
+                expect(diff.result.numbers.numbersArray.length).toBe(4);
+                expect(diff.result.numbers.sum).toBe(2384);
+                // Returning false from onQueryUpdated prevents the cache broadcast.
+                return false;
+              },
+            })
+          ).toEqual({
+            data: {
+              removeLastNumber: {
+                id: "5",
+              },
             },
-          },
+          });
         });
-      });
 
-      expect(numbersArray[numbersArray.length - 1]).toEqual({
-        id: '4',
-        value: 344,
-      });
+        expect(numbersArray[numbersArray.length - 1]).toEqual({
+          id: "4",
+          value: 344,
+        });
 
-      expect(result.current.query.loading).toBe(false);
-      expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
-      expect(result.current.mutation[1].loading).toBe(false);
-      expect(result.current.mutation[1].called).toBe(true);
-    });
+        expect(result.current.query.loading).toBe(false);
+        expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
+        expect(result.current.mutation[1].loading).toBe(false);
+        expect(result.current.mutation[1].called).toBe(true);
+      }
+    );
 
     it("refetchQueries should work with BatchHttpLink", async () => {
       const MUTATION_1 = gql`
@@ -2290,16 +2422,20 @@ describe('useMutation Hook', () => {
       fetchMock.restore();
 
       const responseBodies = [
-        { data: { items: [{ id: 1 }, { id: 2 }] }},
-        { data: { doSomething: { message: 'success' }}},
-        { data: { items: [{ id: 1 }, { id: 2 }, { id: 3 }] }},
+        { data: { items: [{ id: 1 }, { id: 2 }] } },
+        { data: { doSomething: { message: "success" } } },
+        { data: { items: [{ id: 1 }, { id: 2 }, { id: 3 }] } },
       ];
 
-      fetchMock.post("/graphql", (url, opts) => new Promise(resolve => {
-        resolve({
-          body: responseBodies.shift(),
-        });
-      }));
+      fetchMock.post(
+        "/graphql",
+        (url, opts) =>
+          new Promise((resolve) => {
+            resolve({
+              body: responseBodies.shift(),
+            });
+          })
+      );
 
       const Test = () => {
         const { data } = useQuery(QUERY_1);
@@ -2310,34 +2446,43 @@ describe('useMutation Hook', () => {
 
         const { items = [] } = data || {};
 
-        return <>
-          <button onClick={() => {
-            return mutate();
-          }} type="button">
-            mutate
-          </button>
-          {items.map((c: any) => (
-            <div key={c.id}>item {c.id}</div>
-          ))}
-        </>;
+        return (
+          <>
+            <button
+              onClick={() => {
+                return mutate();
+              }}
+              type="button"
+            >
+              mutate
+            </button>
+            {items.map((c: any) => (
+              <div key={c.id}>item {c.id}</div>
+            ))}
+          </>
+        );
       };
 
       const client = new ApolloClient({
         link: new BatchHttpLink({
-          uri: '/graphql',
+          uri: "/graphql",
           batchMax: 10,
         }),
         cache: new InMemoryCache(),
       });
 
-      render(<ApolloProvider client={client}><Test /></ApolloProvider>);
+      render(
+        <ApolloProvider client={client}>
+          <Test />
+        </ApolloProvider>
+      );
 
-      await waitFor(() => screen.findByText('item 1'));
-      await userEvent.click(screen.getByRole('button', { name: /mutate/i }));
-      await waitFor(() => screen.findByText('item 3'));
+      await waitFor(() => screen.findByText("item 1"));
+      await userEvent.click(screen.getByRole("button", { name: /mutate/i }));
+      await waitFor(() => screen.findByText("item 3"));
     });
   });
-  describe('defer', () => {
+  describe("defer", () => {
     const CREATE_TODO_MUTATION_DEFER = gql`
       mutation createTodo($description: String!, $priority: String) {
         createTodo(description: $description, priority: $priority) {
@@ -2350,9 +2495,9 @@ describe('useMutation Hook', () => {
       }
     `;
     const variables = {
-      description: 'Get milk!'
+      description: "Get milk!",
     };
-    it('resolves a deferred mutation with the full result', async () => {
+    it("resolves a deferred mutation with the full result", async () => {
       const errorSpy = jest.spyOn(console, "error");
       const link = new MockSubscriptionLink();
 
@@ -2373,16 +2518,11 @@ describe('useMutation Hook', () => {
         return { loading, data };
       };
 
-      const { result } = renderHook(
-        () => useCreateTodo(),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useCreateTodo(), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
@@ -2393,49 +2533,56 @@ describe('useMutation Hook', () => {
             data: {
               createTodo: {
                 id: 1,
-                __typename: 'Todo',
+                __typename: "Todo",
               },
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
       setTimeout(() => {
-        link.simulateResult({
-          result: {
-            incremental: [{
-              data: {
-                description: 'Get milk!',
-                priority: 'High',
-                __typename: 'Todo',
-              },
-              path: ['createTodo'],
-            }],
-            hasNext: false
+        link.simulateResult(
+          {
+            result: {
+              incremental: [
+                {
+                  data: {
+                    description: "Get milk!",
+                    priority: "High",
+                    __typename: "Todo",
+                  },
+                  path: ["createTodo"],
+                },
+              ],
+              hasNext: false,
+            },
           },
-        }, true);
+          true
+        );
       });
 
-
       // When defer is used in a mutation, the final value resolves
       // in a single result
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toEqual({
         createTodo: {
           id: 1,
           description: "Get milk!",
           priority: "High",
-          __typename: 'Todo',
+          __typename: "Todo",
         },
       });
       expect(errorSpy).not.toHaveBeenCalled();
       errorSpy.mockRestore();
     });
-    it('resolves with resulting errors and calls onError callback', async () => {
+    it("resolves with resulting errors and calls onError callback", async () => {
       const errorSpy = jest.spyOn(console, "error");
       const link = new MockSubscriptionLink();
 
@@ -2449,11 +2596,9 @@ describe('useMutation Hook', () => {
         () => useMutation(CREATE_TODO_MUTATION_DEFER, { onError }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       const createTodo = result.current[0];
@@ -2466,27 +2611,30 @@ describe('useMutation Hook', () => {
             data: {
               createTodo: {
                 id: 1,
-                __typename: 'Todo',
+                __typename: "Todo",
               },
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
       setTimeout(() => {
-        link.simulateResult({
-          result: {
-            incremental: [{
-              data: null,
-              errors: [
-                new GraphQLError(CREATE_TODO_ERROR)
+        link.simulateResult(
+          {
+            result: {
+              incremental: [
+                {
+                  data: null,
+                  errors: [new GraphQLError(CREATE_TODO_ERROR)],
+                  path: ["createTodo"],
+                },
               ],
-              path: ['createTodo'],
-            }],
-            hasNext: false
+              hasNext: false,
+            },
           },
-        }, true);
+          true
+        );
       });
       await act(async () => {
         fetchResult = await createTodo({ variables });
@@ -2509,7 +2657,7 @@ describe('useMutation Hook', () => {
       });
       errorSpy.mockRestore();
     });
-    it('calls the update function with the final merged result data', async () => {
+    it("calls the update function with the final merged result data", async () => {
       const errorSpy = jest.spyOn(console, "error");
       const link = new MockSubscriptionLink();
       const update = jest.fn();
@@ -2519,15 +2667,12 @@ describe('useMutation Hook', () => {
       });
 
       const { result } = renderHook(
-        () => useMutation(CREATE_TODO_MUTATION_DEFER,
-          { update }),
+        () => useMutation(CREATE_TODO_MUTATION_DEFER, { update }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
       const [createTodo] = result.current;
 
@@ -2542,26 +2687,31 @@ describe('useMutation Hook', () => {
           data: {
             createTodo: {
               id: 1,
-              __typename: 'Todo',
+              __typename: "Todo",
             },
           },
-          hasNext: true
+          hasNext: true,
         },
       });
 
-      link.simulateResult({
-        result: {
-          incremental: [{
-            data: {
-              description: 'Get milk!',
-              priority: 'High',
-              __typename: 'Todo',
-            },
-            path: ['createTodo'],
-          }],
-          hasNext: false
+      link.simulateResult(
+        {
+          result: {
+            incremental: [
+              {
+                data: {
+                  description: "Get milk!",
+                  priority: "High",
+                  __typename: "Todo",
+                },
+                path: ["createTodo"],
+              },
+            ],
+            hasNext: false,
+          },
         },
-      }, true);
+        true
+      );
 
       await act(async () => {
         await promiseReturnedByMutate;
@@ -2579,9 +2729,9 @@ describe('useMutation Hook', () => {
               id: 1,
               description: "Get milk!",
               priority: "High",
-              __typename: 'Todo',
+              __typename: "Todo",
             },
-          }
+          },
         }),
         // third argument is an object containing context and variables
         // but we only care about variables here
@@ -2595,3 +2745,16 @@ describe('useMutation Hook', () => {
     });
   });
 });
+
+describe.skip("Type Tests", () => {
+  test("NoInfer prevents adding arbitrary additional variables", () => {
+    const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;
+    useMutation(typedNode, {
+      variables: {
+        bar: 4,
+        // @ts-expect-error
+        nonExistingVariable: "string",
+      },
+    });
+  });
+});
diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx
--- a/src/react/hooks/__tests__/useQuery.test.tsx
+++ b/src/react/hooks/__tests__/useQuery.test.tsx
@@ -1,9 +1,9 @@
-import React, { Fragment, ReactNode, useEffect, useState } from 'react';
-import { DocumentNode, GraphQLError } from 'graphql';
-import gql from 'graphql-tag';
-import { act } from 'react-dom/test-utils';
-import userEvent from '@testing-library/user-event';
-import { render, screen, waitFor, renderHook } from '@testing-library/react';
+import React, { Fragment, ReactNode, useEffect, useState } from "react";
+import { DocumentNode, GraphQLError } from "graphql";
+import gql from "graphql-tag";
+import { act } from "react-dom/test-utils";
+import userEvent from "@testing-library/user-event";
+import { render, screen, waitFor, renderHook } from "@testing-library/react";
 import {
   ApolloClient,
   ApolloError,
@@ -11,29 +11,30 @@ import {
   OperationVariables,
   TypedDocumentNode,
   WatchQueryFetchPolicy,
-} from '../../../core';
-import { InMemoryCache } from '../../../cache';
-import { ApolloProvider, resetApolloContext } from '../../context';
-import { Observable, Reference, concatPagination } from '../../../utilities';
-import { ApolloLink } from '../../../link/core';
+} from "../../../core";
+import { InMemoryCache } from "../../../cache";
+import { ApolloProvider } from "../../context";
+import { Observable, Reference, concatPagination } from "../../../utilities";
+import { ApolloLink } from "../../../link/core";
 import {
   MockLink,
   MockedProvider,
   MockSubscriptionLink,
   mockSingleLink,
   tick,
-} from '../../../testing';
+} from "../../../testing";
 import { QueryResult } from "../../types/types";
-import { useQuery } from '../useQuery';
-import { useMutation } from '../useMutation';
+import { useQuery } from "../useQuery";
+import { useMutation } from "../useMutation";
 
-describe('useQuery Hook', () => {
-  afterEach(() => {
-    resetApolloContext();
-  });
-  describe('General use', () => {
-    it('should handle a simple query', async () => {
-      const query = gql`{ hello }`;
+describe("useQuery Hook", () => {
+  describe("General use", () => {
+    it("should handle a simple query", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -45,31 +46,41 @@ describe('useQuery Hook', () => {
         <MockedProvider mocks={mocks}>{children}</MockedProvider>
       );
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(query), { wrapper });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world" });
     });
 
     it("useQuery result is referentially stable", async () => {
-      const query = gql`{ hello }`;
-      const mocks = [ {
+      const query = gql`
+        {
+          hello
+        }
+      `;
+      const mocks = [
+        {
           request: { query },
           result: { data: { hello: "world" } },
-      } ];
-      const wrapper = ({ children }: any) => <MockedProvider mocks={mocks}>{children}</MockedProvider>;
-      const { result, rerender } = renderHook(() => useQuery(query), { wrapper });
+        },
+      ];
+      const wrapper = ({ children }: any) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      );
+      const { result, rerender } = renderHook(() => useQuery(query), {
+        wrapper,
+      });
       let oldResult: QueryResult<any, OperationVariables>;
 
       await waitFor(() => {
-        result.current.loading === false
+        result.current.loading === false;
       });
 
       rerender({ children: null });
@@ -85,13 +96,23 @@ describe('useQuery Hook', () => {
     });
 
     it("useQuery produces the expected renders initially", async () => {
-      const query = gql`{ hello }`;
-      const mocks = [ {
-        request: { query },
-        result: { data: { hello: "world" } },
-      } ];
-      const wrapper = ({ children }: any) => <MockedProvider mocks={mocks}>{children}</MockedProvider>;
-      const { result, rerender } = renderHook(() => useQuery(query), { wrapper });
+      const query = gql`
+        {
+          hello
+        }
+      `;
+      const mocks = [
+        {
+          request: { query },
+          result: { data: { hello: "world" } },
+        },
+      ];
+      const wrapper = ({ children }: any) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      );
+      const { result, rerender } = renderHook(() => useQuery(query), {
+        wrapper,
+      });
 
       await waitFor(() => result.current.loading === false);
 
@@ -100,7 +121,7 @@ describe('useQuery Hook', () => {
       await waitFor(() => {
         expect(result.current.loading).toBe(false);
       });
-      expect(result.current.data).toEqual({ hello: "world"});
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual({ hello: "world" });
 
@@ -114,20 +135,25 @@ describe('useQuery Hook', () => {
     it("useQuery produces the expected frames when variables change", async () => {
       const query = gql`
         query ($id: Int) {
-        hello(id: $id)
-      }
+          hello(id: $id)
+        }
       `;
-      const mocks = [ {
-        request: { query, variables: { id: 1 } },
-        result: { data: { hello: "world 1" } },
-      }, {
-        request: { query, variables: { id: 2 } },
-        result: { data: { hello: "world 2" } },
-      } ];
-      const wrapper = ({ children }: any) => <MockedProvider mocks={mocks}>{children}</MockedProvider>;
+      const mocks = [
+        {
+          request: { query, variables: { id: 1 } },
+          result: { data: { hello: "world 1" } },
+        },
+        {
+          request: { query, variables: { id: 2 } },
+          result: { data: { hello: "world 2" } },
+        },
+      ];
+      const wrapper = ({ children }: any) => (
+        <MockedProvider mocks={mocks}>{children}</MockedProvider>
+      );
       const { result, rerender } = renderHook(
         (options) => useQuery(query, options),
-        { wrapper, initialProps: { variables: { id: 1 } } },
+        { wrapper, initialProps: { variables: { id: 1 } } }
       );
       await waitFor(() => result.current.loading === false);
       await waitFor(() => {
@@ -170,8 +196,12 @@ describe('useQuery Hook', () => {
       });
     });
 
-    it('should read and write results from the cache', async () => {
-      const query = gql`{ hello }`;
+    it("should read and write results from the cache", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -181,19 +211,23 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
-      const { result, rerender } = renderHook(
-        () => useQuery(query),
-        { wrapper }
-      );
+      const { result, rerender } = renderHook(() => useQuery(query), {
+        wrapper,
+      });
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world" });
 
       rerender();
@@ -201,8 +235,12 @@ describe('useQuery Hook', () => {
       expect(result.current.data).toEqual({ hello: "world" });
     });
 
-    it('should preserve functions between renders', async () => {
-      const query = gql`{ hello }`;
+    it("should preserve functions between renders", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -212,24 +250,21 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(query), { wrapper });
       expect(result.current.loading).toBe(true);
-      const {
-        refetch,
-        fetchMore,
-        startPolling,
-        stopPolling,
-        subscribeToMore,
-      } = result.current;
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      const { refetch, fetchMore, startPolling, stopPolling, subscribeToMore } =
+        result.current;
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(refetch).toBe(result.current.refetch);
       expect(fetchMore).toBe(result.current.fetchMore);
       expect(startPolling).toBe(result.current.startPolling);
@@ -237,8 +272,12 @@ describe('useQuery Hook', () => {
       expect(subscribeToMore).toBe(result.current.subscribeToMore);
     });
 
-    it('should set called to true by default', async () => {
-      const query = gql`{ hello }`;
+    it("should set called to true by default", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -248,20 +287,25 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
-      const { result, unmount } = renderHook(
-        () => useQuery(query),
-        { wrapper },
-      );
+      const { result, unmount } = renderHook(() => useQuery(query), {
+        wrapper,
+      });
 
       expect(result.current.called).toBe(true);
       unmount();
     });
 
-    it('should set called to false when skip option is true', async () => {
-      const query = gql`{ hello }`;
+    it("should set called to false when skip option is true", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -271,19 +315,21 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-          <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result, unmount } = renderHook(
-          () => useQuery(query, { skip: true }),
-          { wrapper },
+        () => useQuery(query, { skip: true }),
+        { wrapper }
       );
 
       expect(result.current.called).toBe(false);
       unmount();
     });
 
-    it('should work with variables', async () => {
+    it("should work with variables", async () => {
       const query = gql`
         query ($id: Int) {
           hello(id: $id)
@@ -303,32 +349,40 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result, rerender } = renderHook(
-        ({ id }) => useQuery(query, { variables: { id }}),
-        { wrapper, initialProps: { id: 1 } },
+        ({ id }) => useQuery(query, { variables: { id } }),
+        { wrapper, initialProps: { id: 1 } }
       );
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world 1" });
 
       rerender({ id: 2 });
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world 2" });
     });
 
-    it('should return the same results for the same variables', async () => {
+    it("should return the same results for the same variables", async () => {
       const query = gql`
         query ($id: Int) {
           hello(id: $id)
@@ -348,28 +402,35 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result, rerender } = renderHook(
         ({ id }) => useQuery(query, { variables: { id } }),
-        { wrapper, initialProps: { id: 1 } },
+        { wrapper, initialProps: { id: 1 } }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world 1" });
 
-
       rerender({ id: 2 });
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world 2" });
 
       rerender({ id: 2 });
@@ -377,7 +438,7 @@ describe('useQuery Hook', () => {
       expect(result.current.data).toEqual({ hello: "world 2" });
     });
 
-    it('should work with variables 2', async () => {
+    it("should work with variables 2", async () => {
       const query = gql`
         query ($name: String) {
           names(name: $name)
@@ -401,42 +462,53 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result, rerender } = renderHook(
         ({ name }) => useQuery(query, { variables: { name } }),
-        { wrapper, initialProps: { name: "" } },
+        { wrapper, initialProps: { name: "" } }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ names: ["Alice", "Bob", "Eve"] });
 
-      rerender({ name: 'z' });
+      rerender({ name: "z" });
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ names: [] });
 
-      rerender({ name: 'zz' });
+      rerender({ name: "zz" });
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ names: [] });
     });
 
     // An unsuccessful attempt to reproduce https://github.com/apollographql/apollo-client/issues/9135.
-    it('should not return stale variables when stored in state', async () => {
+    it("should not return stale variables when stored in state", async () => {
       const query = gql`
         query myQuery($name: String) {
           hello(name: $name)
@@ -471,7 +543,7 @@ describe('useQuery Hook', () => {
           const [name, setName1] = React.useState("world 1");
           setName = setName1;
           return [
-            useQuery(query, { variables: { name }}),
+            useQuery(query, { variables: { name } }),
             useMutation(mutation, {
               update(cache, { data }) {
                 cache.writeQuery({
@@ -488,15 +560,18 @@ describe('useQuery Hook', () => {
               {children}
             </MockedProvider>
           ),
-        },
+        }
       );
 
       expect(result.current[0].loading).toBe(true);
       expect(result.current[0].data).toBe(undefined);
       expect(result.current[0].variables).toEqual({ name: "world 1" });
-      await waitFor(() => {
-        expect(result.current[0].loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current[0].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current[0].data).toEqual({ hello: "world 1" });
       expect(result.current[0].variables).toEqual({ name: "world 1" });
@@ -523,7 +598,7 @@ describe('useQuery Hook', () => {
     });
 
     // TODO: Rewrite this test
-    it('should not error when forcing an update with React >= 16.13.0', async () => {
+    it("should not error when forcing an update with React >= 16.13.0", async () => {
       const CAR_QUERY: DocumentNode = gql`
         query {
           cars {
@@ -537,24 +612,24 @@ describe('useQuery Hook', () => {
       const CAR_RESULT_DATA = {
         cars: [
           {
-            make: 'Audi',
-            model: 'RS8',
-            vin: 'DOLLADOLLABILL',
-            __typename: 'Car'
-          }
-        ]
+            make: "Audi",
+            model: "RS8",
+            vin: "DOLLADOLLABILL",
+            __typename: "Car",
+          },
+        ],
       };
       let wasUpdateErrorLogged = false;
       const consoleError = console.error;
       console.error = (msg: string) => {
         console.log(msg);
-        wasUpdateErrorLogged = msg.indexOf('Cannot update a component') > -1;
+        wasUpdateErrorLogged = msg.indexOf("Cannot update a component") > -1;
       };
 
-      const CAR_MOCKS = [1, 2, 3, 4, 5, 6].map(something => ({
+      const CAR_MOCKS = [1, 2, 3, 4, 5, 6].map((something) => ({
         request: {
           query: CAR_QUERY,
-          variables: { something }
+          variables: { something },
         },
         result: { data: CAR_RESULT_DATA },
       }));
@@ -563,8 +638,8 @@ describe('useQuery Hook', () => {
 
       const InnerComponent = ({ something }: any) => {
         const { loading, data } = useQuery(CAR_QUERY, {
-          fetchPolicy: 'network-only',
-          variables: { something }
+          fetchPolicy: "network-only",
+          variables: { something },
         });
         renderCount += 1;
         if (loading) return null;
@@ -575,7 +650,7 @@ describe('useQuery Hook', () => {
 
       function WrapperComponent({ something }: any) {
         const { loading } = useQuery(CAR_QUERY, {
-          variables: { something }
+          variables: { something },
         });
         return loading ? null : <InnerComponent something={something + 1} />;
       }
@@ -592,36 +667,39 @@ describe('useQuery Hook', () => {
 
       await waitFor(() => {
         expect(renderCount).toBe(6);
-      })
+      });
       console.error = consoleError;
     });
 
-    it('should tear down the query on unmount', async () => {
-      const query = gql`{ hello }`;
+    it("should tear down the query on unmount", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const client = new ApolloClient({
-        link: new ApolloLink(() => Observable.of({ data: { hello: 'world' } })),
+        link: new ApolloLink(() => Observable.of({ data: { hello: "world" } })),
         cache: new InMemoryCache(),
       });
 
       const wrapper = ({ children }: any) => (
-        <ApolloProvider client={client}>
-          {children}
-        </ApolloProvider>
+        <ApolloProvider client={client}>{children}</ApolloProvider>
       );
 
-      const { unmount } = renderHook(
-        () => useQuery(query),
-        { wrapper },
-      );
+      const { unmount } = renderHook(() => useQuery(query), { wrapper });
 
       expect(client.getObservableQueries().size).toBe(1);
       unmount();
-      await new Promise(resolve => setTimeout(resolve));
+      await new Promise((resolve) => setTimeout(resolve));
       expect(client.getObservableQueries().size).toBe(0);
     });
 
-    it('should work with ssr: false', async () => {
-      const query = gql`{ hello }`;
+    it("should work with ssr: false", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -629,24 +707,24 @@ describe('useQuery Hook', () => {
         },
       ];
 
-      const { result } = renderHook(
-        () => useQuery(query, { ssr: false }),
-        {
-          wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>{children}</MockedProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query, { ssr: false }), {
+        wrapper: ({ children }) => (
+          <MockedProvider mocks={mocks}>{children}</MockedProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world" });
     });
 
-    it('should keep `no-cache` results when the tree is re-rendered', async () => {
+    it("should keep `no-cache` results when the tree is re-rendered", async () => {
       const query1 = gql`
         query people {
           allPeople(first: 1) {
@@ -668,12 +746,12 @@ describe('useQuery Hook', () => {
       `;
 
       const allPeopleData = {
-        allPeople: { people: [{ name: 'Luke Skywalker' }] },
+        allPeople: { people: [{ name: "Luke Skywalker" }] },
       };
 
       const allThingsData = {
         allThings: {
-          thing: [{ description: 'Thing 1' }, { description: 'Thing 2' }],
+          thing: [{ description: "Thing 1" }, { description: "Thing 2" }],
         },
       };
 
@@ -686,7 +764,7 @@ describe('useQuery Hook', () => {
           request: { query: query2 },
           result: { data: allThingsData },
           delay: 50,
-        },
+        }
       );
 
       const client = new ApolloClient({
@@ -695,17 +773,12 @@ describe('useQuery Hook', () => {
       });
 
       const { result, rerender } = renderHook(
-        () => [
-          useQuery(query1, { fetchPolicy: "no-cache" }),
-          useQuery(query2),
-        ],
+        () => [useQuery(query1, { fetchPolicy: "no-cache" }), useQuery(query2)],
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current[0].loading).toBe(true);
@@ -713,31 +786,55 @@ describe('useQuery Hook', () => {
       expect(result.current[1].loading).toBe(true);
       expect(result.current[1].data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current[0].loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[0].data).toEqual(allPeopleData);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].loading).toBe(true);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].data).toBe(undefined);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current[0].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[0].data).toEqual(allPeopleData);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].loading).toBe(true);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].data).toBe(undefined);
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        expect(result.current[0].loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[0].data).toEqual(allPeopleData);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current[1].data).toEqual(allThingsData);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current[0].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[0].data).toEqual(allPeopleData);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current[1].data).toEqual(allThingsData);
+        },
+        { interval: 1 }
+      );
 
       rerender();
       expect(result.current[0].loading).toBe(false);
@@ -746,15 +843,24 @@ describe('useQuery Hook', () => {
       expect(result.current[1].data).toEqual(allThingsData);
     });
 
-    it('changing queries', async () => {
-      const query1 = gql`query { hello }`;
-      const query2 = gql`query { hello, name }`;
-      const mocks = [
-        {
-          request: { query: query1 },
-          result: { data: { hello: "world" } },
-        },
-        {
+    it("changing queries", async () => {
+      const query1 = gql`
+        query {
+          hello
+        }
+      `;
+      const query2 = gql`
+        query {
+          hello
+          name
+        }
+      `;
+      const mocks = [
+        {
+          request: { query: query1 },
+          result: { data: { hello: "world" } },
+        },
+        {
           request: { query: query2 },
           result: { data: { hello: "world", name: "world" } },
         },
@@ -770,137 +876,157 @@ describe('useQuery Hook', () => {
             </MockedProvider>
           ),
           initialProps: { query: query1 },
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
       rerender({ query: query2 });
       expect(result.current.loading).toBe(true);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(mocks[1].result.data);
     });
 
-    it('`cache-and-network` fetch policy', async () => {
-      const query = gql`{ hello }`;
+    it("`cache-and-network` fetch policy", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
 
       const cache = new InMemoryCache();
-      const link = mockSingleLink(
-        {
-          request: { query },
-          result: { data: { hello: 'from link' } },
-          delay: 20,
-        },
-      );
+      const link = mockSingleLink({
+        request: { query },
+        result: { data: { hello: "from link" } },
+        delay: 20,
+      });
 
       const client = new ApolloClient({
         link,
         cache,
       });
 
-      cache.writeQuery({ query, data: { hello: 'from cache' }});
+      cache.writeQuery({ query, data: { hello: "from cache" } });
 
       const { result } = renderHook(
-        () => useQuery(query, { fetchPolicy: 'cache-and-network' }),
+        () => useQuery(query, { fetchPolicy: "cache-and-network" }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       // TODO: FIXME
       expect(result.current.loading).toBe(true);
-      expect(result.current.data).toEqual({ hello: 'from cache' });
+      expect(result.current.data).toEqual({ hello: "from cache" });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'from link' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "from link" });
     });
 
-    it('should not use the cache when using `network-only`', async () => {
-      const query = gql`{ hello }`;
+    it("should not use the cache when using `network-only`", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'from link' } },
+          result: { data: { hello: "from link" } },
         },
       ];
 
       const cache = new InMemoryCache();
       cache.writeQuery({
         query,
-        data: { hello: 'from cache' },
+        data: { hello: "from cache" },
       });
 
       const { result } = renderHook(
-        () => useQuery(query, { fetchPolicy: 'network-only' }),
+        () => useQuery(query, { fetchPolicy: "network-only" }),
         {
           wrapper: ({ children }) => (
             <MockedProvider mocks={mocks} cache={cache}>
               {children}
             </MockedProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBeUndefined();
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'from link' });
-    });
-
-    it('should use the cache when in ssrMode and fetchPolicy is `network-only`', async () => {
-      const query = gql`query { hello }`;
-      const link = mockSingleLink(
-        {
-          request: { query },
-          result: { data: { hello: 'from link' } },
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
         },
+        { interval: 1 }
       );
+      expect(result.current.data).toEqual({ hello: "from link" });
+    });
+
+    it("should use the cache when in ssrMode and fetchPolicy is `network-only`", async () => {
+      const query = gql`
+        query {
+          hello
+        }
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: { data: { hello: "from link" } },
+      });
 
       const cache = new InMemoryCache();
       cache.writeQuery({
         query,
-        data: { hello: 'from cache' },
+        data: { hello: "from cache" },
       });
 
-      const client = new ApolloClient({ link, cache, ssrMode: true, });
+      const client = new ApolloClient({ link, cache, ssrMode: true });
       const { result } = renderHook(
-        () => useQuery(query, { fetchPolicy: 'network-only' }),
+        () => useQuery(query, { fetchPolicy: "network-only" }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(false);
-      expect(result.current.data).toEqual({ hello: 'from cache' });
+      expect(result.current.data).toEqual({ hello: "from cache" });
 
-      await expect(waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'from link' });
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await expect(
+        waitFor(
+          () => {
+            expect(result.current.data).toEqual({ hello: "from link" });
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
     });
 
-    it('should not hang when ssrMode is true but the cache is not populated for some reason', async () => {
-      const query = gql`query { hello }`;
-      const link = mockSingleLink(
-        {
-          request: { query },
-          result: { data: { hello: 'from link' } },
-        },
-      );
+    it("should not hang when ssrMode is true but the cache is not populated for some reason", async () => {
+      const query = gql`
+        query {
+          hello
+        }
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: { data: { hello: "from link" } },
+      });
 
       const client = new ApolloClient({
         link,
@@ -908,36 +1034,36 @@ describe('useQuery Hook', () => {
         ssrMode: true,
       });
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBeUndefined();
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'from link' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "from link" });
     });
   });
 
   describe("options.defaultOptions", () => {
     it("can provide a default fetchPolicy", async () => {
-      const query = gql`query { hello }`;
-      const link = mockSingleLink(
-        {
-          request: { query },
-          result: { data: { hello: 'from link' } },
-        },
-      );
+      const query = gql`
+        query {
+          hello
+        }
+      `;
+      const link = mockSingleLink({
+        request: { query },
+        result: { data: { hello: "from link" } },
+      });
 
       const client = new ApolloClient({
         link,
@@ -960,28 +1086,27 @@ describe('useQuery Hook', () => {
         },
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
         }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBeUndefined();
-      expect(fetchPolicyLog).toEqual([
-        "cache-and-network",
-      ]);
+      expect(fetchPolicyLog).toEqual(["cache-and-network"]);
 
       // Change the default fetchPolicy to verify that it is not used the second
       // time useQuery is called.
       defaultFetchPolicy = "network-only";
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
-      expect(result.current.data).toEqual({ hello: 'from link' });
+      expect(result.current.data).toEqual({ hello: "from link" });
       expect(fetchPolicyLog).toEqual([
         "cache-and-network",
         "cache-and-network",
@@ -989,23 +1114,29 @@ describe('useQuery Hook', () => {
     });
 
     it("can provide individual default variables", async () => {
-      const query: TypedDocumentNode<{
-        vars: OperationVariables,
-      }, OperationVariables> = gql`
+      const query: TypedDocumentNode<
+        {
+          vars: OperationVariables;
+        },
+        OperationVariables
+      > = gql`
         query VarsQuery {
           vars
         }
       `;
 
       const client = new ApolloClient({
-        link: new ApolloLink(request => new Observable(observer => {
-          observer.next({
-            data: {
-              vars: request.variables,
-            },
-          });
-          observer.complete();
-        })),
+        link: new ApolloLink(
+          (request) =>
+            new Observable((observer) => {
+              observer.next({
+                data: {
+                  vars: request.variables,
+                },
+              });
+              observer.complete();
+            })
+        ),
 
         cache: new InMemoryCache(),
 
@@ -1040,9 +1171,7 @@ describe('useQuery Hook', () => {
         },
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
         }
       );
@@ -1055,9 +1184,9 @@ describe('useQuery Hook', () => {
         mandatory: true,
       });
 
-      expect(
-        result.current.observable.options.fetchPolicy
-      ).toBe("cache-and-network");
+      expect(result.current.observable.options.fetchPolicy).toBe(
+        "cache-and-network"
+      );
 
       expect(
         // The defaultOptions field is for useQuery options (QueryHookOptions),
@@ -1065,13 +1194,14 @@ describe('useQuery Hook', () => {
         "defaultOptions" in result.current.observable.options
       ).toBe(false);
 
-      expect(fetchPolicyLog).toEqual([
-        "cache-and-network",
-      ]);
+      expect(fetchPolicyLog).toEqual(["cache-and-network"]);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toEqual({
         vars: {
@@ -1086,30 +1216,32 @@ describe('useQuery Hook', () => {
         "cache-and-network",
       ]);
 
-      const reobservePromise = act(() => result.current.observable.reobserve({
-        fetchPolicy: "network-only",
-        nextFetchPolicy: "cache-first",
-        variables: {
-          // Since reobserve replaces the variables object rather than merging
-          // the individual variables together, we need to include the current
-          // variables manually if we want them to show up in the output below.
-          ...result.current.observable.variables,
-          sourceOfVar: "reobserve",
-        },
-      }).then(finalResult => {
-        expect(finalResult.loading).toBe(false);
-        expect(finalResult.data).toEqual({
-          vars: {
-            sourceOfVar: "reobserve",
-            isGlobal: false,
-            mandatory: true,
-          },
-        });
-      }));
+      const reobservePromise = act(() =>
+        result.current.observable
+          .reobserve({
+            fetchPolicy: "network-only",
+            nextFetchPolicy: "cache-first",
+            variables: {
+              // Since reobserve replaces the variables object rather than merging
+              // the individual variables together, we need to include the current
+              // variables manually if we want them to show up in the output below.
+              ...result.current.observable.variables,
+              sourceOfVar: "reobserve",
+            },
+          })
+          .then((finalResult) => {
+            expect(finalResult.loading).toBe(false);
+            expect(finalResult.data).toEqual({
+              vars: {
+                sourceOfVar: "reobserve",
+                isGlobal: false,
+                mandatory: true,
+              },
+            });
+          })
+      );
 
-      expect(
-        result.current.observable.options.fetchPolicy
-      ).toBe("cache-first");
+      expect(result.current.observable.options.fetchPolicy).toBe("cache-first");
 
       expect(result.current.observable.variables).toEqual({
         sourceOfVar: "reobserve",
@@ -1119,9 +1251,7 @@ describe('useQuery Hook', () => {
 
       await reobservePromise;
 
-      expect(
-        result.current.observable.options.fetchPolicy
-      ).toBe("cache-first");
+      expect(result.current.observable.options.fetchPolicy).toBe("cache-first");
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual({
@@ -1131,9 +1261,7 @@ describe('useQuery Hook', () => {
           mandatory: true,
         },
       });
-      expect(
-        result.current.observable.variables
-      ).toEqual(
+      expect(result.current.observable.variables).toEqual(
         result.current.data!.vars
       );
 
@@ -1143,30 +1271,32 @@ describe('useQuery Hook', () => {
         "cache-first",
       ]);
 
-      const reobserveNoVarMergePromise = act(() => result.current.observable.reobserve({
-        fetchPolicy: "network-only",
-        nextFetchPolicy: "cache-first",
-        variables: {
-          // This reobservation is like the one above, with no variable merging.
-          // ...result.current.observable.variables,
-          sourceOfVar: "reobserve without variable merge",
-        },
-      }).then(finalResult => {
-        expect(finalResult.loading).toBe(false);
-        expect(finalResult.data).toEqual({
-          vars: {
-            sourceOfVar: "reobserve without variable merge",
-            // Since we didn't merge in result.current.observable.variables, we
-            // don't see these variables anymore:
-            // isGlobal: false,
-            // mandatory: true,
-          },
-        });
-      }));
+      const reobserveNoVarMergePromise = act(() =>
+        result.current.observable
+          .reobserve({
+            fetchPolicy: "network-only",
+            nextFetchPolicy: "cache-first",
+            variables: {
+              // This reobservation is like the one above, with no variable merging.
+              // ...result.current.observable.variables,
+              sourceOfVar: "reobserve without variable merge",
+            },
+          })
+          .then((finalResult) => {
+            expect(finalResult.loading).toBe(false);
+            expect(finalResult.data).toEqual({
+              vars: {
+                sourceOfVar: "reobserve without variable merge",
+                // Since we didn't merge in result.current.observable.variables, we
+                // don't see these variables anymore:
+                // isGlobal: false,
+                // mandatory: true,
+              },
+            });
+          })
+      );
 
-      expect(
-        result.current.observable.options.fetchPolicy
-      ).toBe("cache-first");
+      expect(result.current.observable.options.fetchPolicy).toBe("cache-first");
 
       expect(result.current.observable.variables).toEqual({
         sourceOfVar: "reobserve without variable merge",
@@ -1174,9 +1304,7 @@ describe('useQuery Hook', () => {
 
       await reobserveNoVarMergePromise;
 
-      expect(
-        result.current.observable.options.fetchPolicy
-      ).toBe("cache-first");
+      expect(result.current.observable.options.fetchPolicy).toBe("cache-first");
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual({
@@ -1184,9 +1312,7 @@ describe('useQuery Hook', () => {
           sourceOfVar: "reobserve without variable merge",
         },
       });
-      expect(
-        result.current.observable.variables
-      ).toEqual(
+      expect(result.current.observable.variables).toEqual(
         result.current.data!.vars
       );
 
@@ -1210,22 +1336,27 @@ describe('useQuery Hook', () => {
       let count = 0;
       const client = new ApolloClient({
         cache: new InMemoryCache(),
-        link: new ApolloLink(request => new Observable(observer => {
-          if (request.operationName === "GetCounter") {
-            observer.next({
-              data: {
-                counter: ++count,
-              },
-            });
-            setTimeout(() => {
-              observer.complete();
-            }, 10);
-          } else {
-            observer.error(new Error(`Unknown query: ${
-              request.operationName || request.query
-            }`));
-          }
-        })),
+        link: new ApolloLink(
+          (request) =>
+            new Observable((observer) => {
+              if (request.operationName === "GetCounter") {
+                observer.next({
+                  data: {
+                    counter: ++count,
+                  },
+                });
+                setTimeout(() => {
+                  observer.complete();
+                }, 10);
+              } else {
+                observer.error(
+                  new Error(
+                    `Unknown query: ${request.operationName || request.query}`
+                  )
+                );
+              }
+            })
+        ),
       });
 
       const defaultFetchPolicy = "network-only";
@@ -1245,20 +1376,23 @@ describe('useQuery Hook', () => {
         },
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.query.loading).toBe(false);
       expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.query.data).toBeUndefined();
 
-      await expect(waitFor(() => {
-        expect(result.current.query.data).toEqual({ counter: 1 });
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await expect(
+        waitFor(
+          () => {
+            expect(result.current.query.data).toEqual({ counter: 1 });
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
 
       act(() => {
         result.current.setSkip(false);
@@ -1266,9 +1400,12 @@ describe('useQuery Hook', () => {
       expect(result.current.query.loading).toBe(true);
       expect(result.current.query.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.query.data).toBeUndefined();
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.query.data).toEqual({ counter: 1 });
 
@@ -1282,9 +1419,14 @@ describe('useQuery Hook', () => {
       expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.query.data).toBeUndefined();
 
-      await expect(waitFor(() => {
-        expect(result.current.query.data).toEqual({ counter: 1 });
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await expect(
+        waitFor(
+          () => {
+            expect(result.current.query.data).toEqual({ counter: 1 });
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
 
       act(() => {
         result.current.setSkip(false);
@@ -1292,9 +1434,12 @@ describe('useQuery Hook', () => {
       expect(result.current.query.loading).toBe(true);
       expect(result.current.query.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.query.data).toEqual({ counter: 1 });
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.query.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.query.data).toEqual({ counter: 2 });
 
@@ -1303,13 +1448,15 @@ describe('useQuery Hook', () => {
   });
 
   it("can provide options.client without ApolloProvider", async () => {
-    const query = gql`query { hello }`;
-    const link = mockSingleLink(
-      {
-        request: { query },
-        result: { data: { hello: 'from link' } },
-      },
-    );
+    const query = gql`
+      query {
+        hello
+      }
+    `;
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: { hello: "from link" } },
+    });
 
     const client = new ApolloClient({
       link,
@@ -1318,7 +1465,7 @@ describe('useQuery Hook', () => {
     });
 
     const { result } = renderHook(
-      () => useQuery(query, { client }),
+      () => useQuery(query, { client })
       // We deliberately do not provide the usual ApolloProvider wrapper for
       // this test, since we are providing the client directly to useQuery.
       // {
@@ -1333,54 +1480,68 @@ describe('useQuery Hook', () => {
     expect(result.current.loading).toBe(true);
     expect(result.current.data).toBeUndefined();
 
-    await waitFor(() => {
-      expect(result.current.loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
 
-    expect(result.current.data).toEqual({ hello: 'from link' });
+    expect(result.current.data).toEqual({ hello: "from link" });
   });
 
-  describe('<React.StrictMode>', () => {
+  describe("<React.StrictMode>", () => {
     it("double-rendering should not trigger duplicate network requests", async () => {
       const query: TypedDocumentNode<{
         linkCount: number;
-      }> = gql`query Counter { linkCount }`;
+      }> = gql`
+        query Counter {
+          linkCount
+        }
+      `;
 
       let linkCount = 0;
       const client = new ApolloClient({
         cache: new InMemoryCache(),
-        link: new ApolloLink(request => new Observable(observer => {
-          if (request.operationName === "Counter") {
-            observer.next({
-              data: {
-                linkCount: ++linkCount,
-              },
-            });
-            observer.complete();
-          }
-        })),
+        link: new ApolloLink(
+          (request) =>
+            new Observable((observer) => {
+              if (request.operationName === "Counter") {
+                observer.next({
+                  data: {
+                    linkCount: ++linkCount,
+                  },
+                });
+                observer.complete();
+              }
+            })
+        ),
       });
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          fetchPolicy: "cache-and-network",
-        }),
+        () =>
+          useQuery(query, {
+            fetchPolicy: "cache-and-network",
+          }),
         {
           wrapper: ({ children }) => (
             <React.StrictMode>
               <ApolloProvider client={client}>{children}</ApolloProvider>
             </React.StrictMode>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({
         linkCount: 1,
@@ -1392,7 +1553,7 @@ describe('useQuery Hook', () => {
 
         const activeSet = new Set<typeof result.current.observable>();
         const inactiveSet = new Set<typeof result.current.observable>();
-        obsQueries.forEach(obsQuery => {
+        obsQueries.forEach((obsQuery) => {
           if (obsQuery.hasObservers()) {
             expect(inactiveSet.has(obsQuery)).toBe(false);
             activeSet.add(obsQuery);
@@ -1414,7 +1575,7 @@ describe('useQuery Hook', () => {
 
       checkObservableQueries(1);
 
-      await result.current.reobserve().then(result => {
+      await result.current.reobserve().then((result) => {
         expect(result.loading).toBe(false);
         expect(result.loading).toBe(false);
         expect(result.networkStatus).toBe(NetworkStatus.ready);
@@ -1426,22 +1587,32 @@ describe('useQuery Hook', () => {
       await waitFor(() => {
         expect(result.current.loading).toBe(false);
       });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          linkCount: 2,
-        });
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            linkCount: 2,
+          });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
 
       checkObservableQueries(2);
     });
   });
 
-  describe('polling', () => {
-    it('should support polling', async () => {
-      const query = gql`{ hello }`;
+  describe("polling", () => {
+    it("should support polling", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -1459,44 +1630,64 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result } = renderHook(
         () => useQuery(query, { pollInterval: 10 }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 1" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 1" });
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 2" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 2" });
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 3" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 3" });
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
 
       const { data: previousData } = result.current;
       result.current.stopPolling();
-      await expect(waitFor(() => {
-        expect(result.current.data).not.toEqual(previousData);
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await expect(
+        waitFor(
+          () => {
+            expect(result.current.data).not.toEqual(previousData);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
     });
 
-    it('should start polling when skip goes from true to false', async () => {
-      const query = gql`{ hello }`;
+    it("should start polling when skip goes from true to false", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -1524,16 +1715,19 @@ describe('useQuery Hook', () => {
               {children}
             </MockedProvider>
           ),
-          initialProps: { skip: undefined } as any
-        },
+          initialProps: { skip: undefined } as any,
+        }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual({ hello: "world 1" });
@@ -1542,27 +1736,42 @@ describe('useQuery Hook', () => {
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
 
-      await expect(waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 1" });
-      }, { interval: 1, timeout: 20 })).rejects.toThrow()
+      await expect(
+        waitFor(
+          () => {
+            expect(result.current.data).toEqual({ hello: "world 1" });
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
 
       rerender({ skip: false });
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual({ hello: "world 1" });
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 2" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 2" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 3" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 3" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
     });
 
     it("should return data from network when clients default fetch policy set to network-only", async () => {
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const data = { hello: "world" };
       const mocks = [
         {
@@ -1587,22 +1796,26 @@ describe('useQuery Hook', () => {
         </MockedProvider>
       );
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(query), { wrapper });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data);
     });
 
-    it('should stop polling when component unmounts', async () => {
-      const query = gql`{ hello }`;
+    it("should stop polling when component unmounts", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -1621,27 +1834,35 @@ describe('useQuery Hook', () => {
       const cache = new InMemoryCache();
 
       const link = new MockLink(mocks);
-      const requestSpy = jest.spyOn(link, 'request');
+      const requestSpy = jest.spyOn(link, "request");
       const onErrorFn = jest.fn();
       link.setOnError(onErrorFn);
       const wrapper = ({ children }: any) => (
-        <MockedProvider link={link} cache={cache}>{children}</MockedProvider>
+        <MockedProvider link={link} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result, unmount } = renderHook(
         () => useQuery(query, { pollInterval: 10 }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 1" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 1" });
+        },
+        { interval: 1 }
+      );
 
       await waitFor(() => {
         expect(requestSpy).toHaveBeenCalled();
@@ -1654,10 +1875,15 @@ describe('useQuery Hook', () => {
 
       expect(requestSpy).toHaveBeenCalledTimes(requestCount);
 
-      await expect(waitFor(() => {
-        const newRequestCount = requestSpy.mock.calls.length;
-        expect(newRequestCount).toBeGreaterThan(requestCount);
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await expect(
+        waitFor(
+          () => {
+            const newRequestCount = requestSpy.mock.calls.length;
+            expect(newRequestCount).toBeGreaterThan(requestCount);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
 
       await waitFor(() => {
         expect(onErrorFn).toHaveBeenCalledTimes(0);
@@ -1666,8 +1892,12 @@ describe('useQuery Hook', () => {
       requestSpy.mockRestore();
     });
 
-    it('should stop polling when component is unmounted in Strict Mode', async () => {
-      const query = gql`{ hello }`;
+    it("should stop polling when component is unmounted in Strict Mode", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -1688,42 +1918,59 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const link = new MockLink(mocks);
-      const requestSpy = jest.spyOn(link, 'request');
+      const requestSpy = jest.spyOn(link, "request");
       const onErrorFn = jest.fn();
       link.setOnError(onErrorFn);
       const wrapper = ({ children }: any) => (
         <React.StrictMode>
-<MockedProvider link={link} cache={cache}>{children}</MockedProvider>
+          <MockedProvider link={link} cache={cache}>
+            {children}
+          </MockedProvider>
         </React.StrictMode>
       );
 
       const { result, unmount } = renderHook(
         () => useQuery(query, { pollInterval: 10 }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ hello: "world 1" });
 
-      const requestCount = requestSpy.mock.calls.length;
-      expect(requestSpy).toHaveBeenCalledTimes(requestCount);
+      const requestSpyCallCount = requestSpy.mock.calls.length;
+      expect(requestSpy).toHaveBeenCalledTimes(requestSpyCallCount);
 
       unmount();
 
-      await expect(waitFor(() => {
-        expect(requestSpy).toHaveBeenCalledTimes(requestCount + 1)
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      expect(requestSpy).toHaveBeenCalledTimes(requestSpyCallCount);
+      await expect(
+        waitFor(
+          () => {
+            expect(requestSpy).toHaveBeenCalledTimes(requestSpyCallCount + 1);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
+      expect(requestSpy).toHaveBeenCalledTimes(requestSpyCallCount);
       expect(onErrorFn).toHaveBeenCalledTimes(0);
+
       requestSpy.mockRestore();
     });
 
-    it('should start and stop polling in Strict Mode', async () => {
-      const query = gql`{ hello }`;
+    it("should start and stop polling in Strict Mode", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -1745,97 +1992,121 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const link = new MockLink(mocks);
-      const requestSpy = jest.spyOn(link, 'request');
+      const requestSpy = jest.spyOn(link, "request");
       const onErrorFn = jest.fn();
       link.setOnError(onErrorFn);
       const wrapper = ({ children }: any) => (
         <React.StrictMode>
-          <MockedProvider link={link} cache={cache}>{children}</MockedProvider>
+          <MockedProvider link={link} cache={cache}>
+            {children}
+          </MockedProvider>
         </React.StrictMode>
       );
 
       const { result } = renderHook(
         () => useQuery(query, { pollInterval: 20 }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 1" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 1" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
 
-
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 2" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 2" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
 
-
       result.current.stopPolling();
 
-      await expect(waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 3" });
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await expect(
+        waitFor(
+          () => {
+            expect(result.current.data).toEqual({ hello: "world 3" });
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
       result.current.startPolling(20);
 
       expect(requestSpy).toHaveBeenCalledTimes(2);
       expect(onErrorFn).toHaveBeenCalledTimes(0);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 3" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 3" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: "world 4" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 4" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
       expect(requestSpy).toHaveBeenCalledTimes(4);
       expect(onErrorFn).toHaveBeenCalledTimes(0);
       requestSpy.mockRestore();
     });
 
-    it('should not throw an error if stopPolling is called manually', async () => {
-      const query = gql`{ hello }`;
+    it("should not throw an error if stopPolling is called manually", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            data: { hello: 'world' },
-          }
-        }
+            data: { hello: "world" },
+          },
+        },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
-      const { result, unmount } = renderHook(
-        () => useQuery(query),
-        { wrapper },
-      );
+      const { result, unmount } = renderHook(() => useQuery(query), {
+        wrapper,
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
-      expect(result.current.data).toEqual({ hello: 'world' });
+      expect(result.current.data).toEqual({ hello: "world" });
 
       unmount();
       result.current.stopPolling();
     });
   });
 
-  describe('Error handling', () => {
-
-    it('should pass along GraphQL errors', async () => {
+  describe("Error handling", () => {
+    it("should pass along GraphQL errors", async () => {
       const query = gql`
         query TestQuery {
           rates(currency: "USD") {
@@ -1848,106 +2119,124 @@ describe('useQuery Hook', () => {
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error')]
-          }
-        }
+            errors: [new GraphQLError("error")],
+          },
+        },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(query), { wrapper });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
     });
 
-    it('calls `onError` when a GraphQL error is returned', async () => {
-      const query = gql`{ hello }`;
+    it("calls `onError` when a GraphQL error is returned", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error')],
+            errors: [new GraphQLError("error")],
           },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
-      const { result } = renderHook(
-        () => useQuery(query, { onError }),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(query, { onError }), {
+        wrapper,
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBeUndefined();
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
 
       await waitFor(() => {
         expect(onError).toHaveBeenCalledTimes(1);
       });
       await waitFor(() => {
         expect(onError).toHaveBeenCalledWith(
-          new ApolloError({ graphQLErrors: [new GraphQLError('error')] })
+          new ApolloError({ graphQLErrors: [new GraphQLError("error")] })
         );
       });
     });
 
-    it('calls `onError` when a network error has occured', async () => {
-      const query = gql`{ hello }`;
+    it("calls `onError` when a network error has occured", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          error: new Error('Could not fetch')
+          error: new Error("Could not fetch"),
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
-      const { result } = renderHook(
-        () => useQuery(query, { onError }),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(query, { onError }), {
+        wrapper,
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBeUndefined();
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('Could not fetch');
+      expect(result.current.error!.message).toBe("Could not fetch");
       expect(result.current.error!.networkError).toEqual(
-        new Error('Could not fetch')
+        new Error("Could not fetch")
       );
 
       await waitFor(() => {
@@ -1955,79 +2244,96 @@ describe('useQuery Hook', () => {
       });
       await waitFor(() => {
         expect(onError).toHaveBeenCalledWith(
-          new ApolloError({ networkError: new Error('Could not fetch') })
+          new ApolloError({ networkError: new Error("Could not fetch") })
         );
       });
     });
 
-    it('removes partial data from result when response has errors', async () => {
-      const query = gql`{ hello }`;
+    it("removes partial data from result when response has errors", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
             data: { hello: null },
-            errors: [new GraphQLError('Could not fetch "hello"')]
-          }
+            errors: [new GraphQLError('Could not fetch "hello"')],
+          },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
-      const { result } = renderHook(
-        () => useQuery(query, { onError }),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(query, { onError }), {
+        wrapper,
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBeUndefined();
       expect(result.current.error).toBeInstanceOf(ApolloError);
       expect(result.current.error!.message).toBe('Could not fetch "hello"');
       expect(result.current.error!.graphQLErrors).toEqual([
-        new GraphQLError('Could not fetch "hello"')
+        new GraphQLError('Could not fetch "hello"'),
       ]);
     });
 
     it('does not call `onError` when returning GraphQL errors while using an `errorPolicy` set to "ignore"', async () => {
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error')]
-          }
+            errors: [new GraphQLError("error")],
+          },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, { onError, errorPolicy: 'ignore' }),
-        { wrapper },
+        () => useQuery(query, { onError, errorPolicy: "ignore" }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBeUndefined();
       expect(result.current.error).toBeUndefined();
@@ -2038,38 +2344,47 @@ describe('useQuery Hook', () => {
     });
 
     it('calls `onError` when a network error has occurred while using an `errorPolicy` set to "ignore"', async () => {
-      const query = gql`{ hello }`;
-      const mocks = [
+      const query = gql`
         {
-          request: { query },
-          error: new Error('Could not fetch')
-        },
+          hello
+        }
+      `;
+      const mocks = [
+        {
+          request: { query },
+          error: new Error("Could not fetch"),
+        },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, { onError, errorPolicy: 'ignore' }),
-        { wrapper },
+        () => useQuery(query, { onError, errorPolicy: "ignore" }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBeUndefined();
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('Could not fetch');
+      expect(result.current.error!.message).toBe("Could not fetch");
       expect(result.current.error!.networkError).toEqual(
-        new Error('Could not fetch')
+        new Error("Could not fetch")
       );
 
       await waitFor(() => {
@@ -2077,76 +2392,94 @@ describe('useQuery Hook', () => {
       });
       await waitFor(() => {
         expect(onError).toHaveBeenCalledWith(
-          new ApolloError({ networkError: new Error('Could not fetch') })
+          new ApolloError({ networkError: new Error("Could not fetch") })
         );
       });
     });
 
     it('returns partial data and discards GraphQL errors when using an `errorPolicy` set to "ignore"', async () => {
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
             data: { hello: null },
-            errors: [new GraphQLError('Could not fetch "hello"')]
-          }
+            errors: [new GraphQLError('Could not fetch "hello"')],
+          },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result } = renderHook(
-        () => useQuery(query, { errorPolicy: 'ignore' }),
-        { wrapper },
+        () => useQuery(query, { errorPolicy: "ignore" }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
-      expect(result.current.data).toEqual({ hello: null })
+      expect(result.current.data).toEqual({ hello: null });
       expect(result.current.error).toBeUndefined();
     });
 
     it('calls `onCompleted` with partial data but avoids calling `onError` when using an `errorPolicy` set to "ignore"', async () => {
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
             data: { hello: null },
-            errors: [new GraphQLError('Could not fetch "hello"')]
-          }
+            errors: [new GraphQLError('Could not fetch "hello"')],
+          },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
       const onCompleted = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, { onError, onCompleted, errorPolicy: 'ignore' }),
-        { wrapper },
+        () => useQuery(query, { onError, onCompleted, errorPolicy: "ignore" }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
-      expect(result.current.data).toEqual({ hello: null })
+      expect(result.current.data).toEqual({ hello: null });
       expect(result.current.error).toBeUndefined();
 
       await waitFor(() => {
@@ -2161,39 +2494,48 @@ describe('useQuery Hook', () => {
     });
 
     it('calls `onError` when returning GraphQL errors while using an `errorPolicy` set to "all"', async () => {
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error')]
-          }
+            errors: [new GraphQLError("error")],
+          },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, { onError, errorPolicy: 'all' }),
-        { wrapper },
+        () => useQuery(query, { onError, errorPolicy: "all" }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBeUndefined();
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
       expect(result.current.error!.graphQLErrors).toEqual([
-        new GraphQLError('error')
+        new GraphQLError("error"),
       ]);
 
       await waitFor(() => {
@@ -2201,85 +2543,103 @@ describe('useQuery Hook', () => {
       });
       await waitFor(() => {
         expect(onError).toHaveBeenCalledWith(
-          new ApolloError({ graphQLErrors: [new GraphQLError('error')] })
+          new ApolloError({ graphQLErrors: [new GraphQLError("error")] })
         );
       });
     });
 
     it('returns partial data when returning GraphQL errors while using an `errorPolicy` set to "all"', async () => {
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
             data: { hello: null },
-            errors: [new GraphQLError('Could not fetch "hello"')]
-          }
+            errors: [new GraphQLError('Could not fetch "hello"')],
+          },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, { onError, errorPolicy: 'all' }),
-        { wrapper },
+        () => useQuery(query, { onError, errorPolicy: "all" }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toEqual({ hello: null });
       expect(result.current.error).toBeInstanceOf(ApolloError);
       expect(result.current.error!.message).toBe('Could not fetch "hello"');
       expect(result.current.error!.graphQLErrors).toEqual([
-        new GraphQLError('Could not fetch "hello"')
+        new GraphQLError('Could not fetch "hello"'),
       ]);
     });
 
     it('calls `onError` but not `onCompleted` when returning partial data with GraphQL errors while using an `errorPolicy` set to "all"', async () => {
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
             data: { hello: null },
-            errors: [new GraphQLError('Could not fetch "hello"')]
-          }
+            errors: [new GraphQLError('Could not fetch "hello"')],
+          },
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
       const onCompleted = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, { onError, onCompleted, errorPolicy: 'all' }),
-        { wrapper },
+        () => useQuery(query, { onError, onCompleted, errorPolicy: "all" }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toEqual({ hello: null });
       expect(result.current.error).toBeInstanceOf(ApolloError);
       expect(result.current.error!.message).toBe('Could not fetch "hello"');
       expect(result.current.error!.graphQLErrors).toEqual([
-        new GraphQLError('Could not fetch "hello"')
+        new GraphQLError('Could not fetch "hello"'),
       ]);
 
       await waitFor(() => {
@@ -2288,7 +2648,7 @@ describe('useQuery Hook', () => {
       await waitFor(() => {
         expect(onError).toHaveBeenCalledWith(
           new ApolloError({
-            graphQLErrors: [new GraphQLError('Could not fetch "hello"')]
+            graphQLErrors: [new GraphQLError('Could not fetch "hello"')],
           })
         );
       });
@@ -2297,19 +2657,23 @@ describe('useQuery Hook', () => {
       });
     });
 
-    it('calls `onError` a single time when refetching returns a successful result', async () => {
-      const query = gql`{ hello }`;
+    it("calls `onError` a single time when refetching returns a successful result", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error')],
+            errors: [new GraphQLError("error")],
           },
         },
         {
           request: { query },
           result: {
-            data: { hello: 'world' },
+            data: { hello: "world" },
           },
           delay: 10,
         },
@@ -2317,94 +2681,120 @@ describe('useQuery Hook', () => {
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const onError = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, {
-          onError,
-          notifyOnNetworkStatusChange: true,
-        }),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            onError,
+            notifyOnNetworkStatusChange: true,
+          }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
 
       await new Promise((resolve) => setTimeout(resolve));
       expect(onError).toHaveBeenCalledTimes(1);
 
       result.current.refetch();
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(onError).toHaveBeenCalledTimes(1);
     });
 
-    it('should persist errors on re-render if they are still valid', async () => {
-      const query = gql`{ hello }`;
+    it("should persist errors on re-render if they are still valid", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
 
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error')]
-          }
-        }
+            errors: [new GraphQLError("error")],
+          },
+        },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       let updates = 0;
       const { result, rerender } = renderHook(
         () => (updates++, useQuery(query)),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
 
       rerender();
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
 
       let previousUpdates = updates;
-      await expect(waitFor(() => {
-        expect(updates).not.toEqual(previousUpdates)
-      }, { interval: 1, timeout: 20 })).rejects.toThrow()
+      await expect(
+        waitFor(
+          () => {
+            expect(updates).not.toEqual(previousUpdates);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
     });
 
-    it('should not return partial data from cache on refetch with errorPolicy: none (default) and notifyOnNetworkStatusChange: true', async () => {
+    it("should not return partial data from cache on refetch with errorPolicy: none (default) and notifyOnNetworkStatusChange: true", async () => {
       const query = gql`
         {
           dogs {
@@ -2429,21 +2819,23 @@ describe('useQuery Hook', () => {
 
       const dogData = [
         {
-          "id": "Z1fdFgU",
-          "breed": "affenpinscher",
-          "__typename": "Dog"
+          id: "Z1fdFgU",
+          breed: "affenpinscher",
+          __typename: "Dog",
         },
         {
-          "id": "ZNDtCU",
-          "breed": "airedale",
-          "__typename": "Dog"
+          id: "ZNDtCU",
+          breed: "airedale",
+          __typename: "Dog",
         },
       ];
 
       const detailsMock = (breed: string) => ({
         request: { query: GET_DOG_DETAILS, variables: { breed } },
         result: {
-          errors: [new GraphQLError(`Cannot query field "unexisting" on type "Dog".`)],
+          errors: [
+            new GraphQLError(`Cannot query field "unexisting" on type "Dog".`),
+          ],
         },
       });
 
@@ -2454,15 +2846,15 @@ describe('useQuery Hook', () => {
         },
         // use the same mock for the initial query on select change
         // and subsequent refetch() call
-        detailsMock('airedale'),
-        detailsMock('airedale'),
+        detailsMock("airedale"),
+        detailsMock("airedale"),
       ];
       const Dogs: React.FC<{
         onDogSelected: (event: React.ChangeEvent<HTMLSelectElement>) => void;
       }> = ({ onDogSelected }) => {
-        const { loading, error, data } = useQuery<
-          { dogs: { id: string; breed: string; }[] }
-        >(query);
+        const { loading, error, data } = useQuery<{
+          dogs: { id: string; breed: string }[];
+        }>(query);
 
         if (loading) return <>Loading...</>;
         if (error) return <>{`Error! ${error.message}`}</>;
@@ -2485,24 +2877,16 @@ describe('useQuery Hook', () => {
           GET_DOG_DETAILS,
           {
             variables: { breed },
-            notifyOnNetworkStatusChange: true
+            notifyOnNetworkStatusChange: true,
           }
         );
         if (networkStatus === 4) return <p>Refetching!</p>;
         if (loading) return <p>Loading!</p>;
         return (
           <div>
-            <div>
-              {data ? 'Partial data rendered' : null}
-            </div>
+            <div>{data ? "Partial data rendered" : null}</div>
 
-            <div>
-              {error ? (
-                `Error!: ${error}`
-                ) : (
-                'Rendering!'
-              )}
-            </div>
+            <div>{error ? `Error!: ${error}` : "Rendering!"}</div>
             <button onClick={() => refetch()}>Refetch!</button>
           </div>
         );
@@ -2526,30 +2910,34 @@ describe('useQuery Hook', () => {
       render(<ParentComponent />);
 
       // on initial load, the list of dogs populates the dropdown
-      await screen.findByText('affenpinscher');
+      await screen.findByText("affenpinscher");
 
       // the user selects a different dog from the dropdown which
       // fires the GET_DOG_DETAILS query, retuning an error
       const user = userEvent.setup();
       await user.selectOptions(
-        screen.getByRole('combobox'),
-        screen.getByRole('option', { name: 'airedale' })
+        screen.getByRole("combobox"),
+        screen.getByRole("option", { name: "airedale" })
       );
 
       // With the default errorPolicy of 'none', the error is rendered
       // and partial data is not
-      await screen.findByText('Error!: ApolloError: Cannot query field "unexisting" on type "Dog".')
+      await screen.findByText(
+        'Error!: ApolloError: Cannot query field "unexisting" on type "Dog".'
+      );
       expect(screen.queryByText(/partial data rendered/i)).toBeNull();
 
       // When we call refetch...
-      await user.click(screen.getByRole('button', { name: /Refetch!/i }))
+      await user.click(screen.getByRole("button", { name: /Refetch!/i }));
 
       // The error is still present, and partial data still not rendered
-      await screen.findByText('Error!: ApolloError: Cannot query field "unexisting" on type "Dog".')
+      await screen.findByText(
+        'Error!: ApolloError: Cannot query field "unexisting" on type "Dog".'
+      );
       expect(screen.queryByText(/partial data rendered/i)).toBeNull();
     });
 
-    it('should return partial data from cache on refetch', async () => {
+    it("should return partial data from cache on refetch", async () => {
       const GET_DOG_DETAILS = gql`
         query dog($breed: String!) {
           dog(breed: $breed) {
@@ -2562,38 +2950,33 @@ describe('useQuery Hook', () => {
         result: {
           data: {
             dog: {
-              "id": "ZNDtCU",
-              "__typename": "Dog"
-            }
-          }
+              id: "ZNDtCU",
+              __typename: "Dog",
+            },
+          },
         },
       });
 
       const mocks = [
         // use the same mock for the initial query on select change
         // and subsequent refetch() call
-        detailsMock('airedale'),
-        detailsMock('airedale'),
+        detailsMock("airedale"),
+        detailsMock("airedale"),
       ];
 
       const DogDetails: React.FC<{
         breed?: string;
       }> = ({ breed = "airedale" }) => {
-        const { data, refetch, networkStatus } = useQuery(
-          GET_DOG_DETAILS,
-          {
-            variables: { breed },
-            notifyOnNetworkStatusChange: true
-          }
-        );
+        const { data, refetch, networkStatus } = useQuery(GET_DOG_DETAILS, {
+          variables: { breed },
+          notifyOnNetworkStatusChange: true,
+        });
         if (networkStatus === 1) return <p>Loading!</p>;
         return (
           // Render existing results, but dim the UI until the results
           // have finished loading...
           <div style={{ opacity: networkStatus === 4 ? 0.5 : 1 }}>
-            <div>
-              {data ? 'Data rendered' : null}
-            </div>
+            <div>{data ? "Data rendered" : null}</div>
             <button onClick={() => refetch()}>Refetch!</button>
           </div>
         );
@@ -2611,31 +2994,41 @@ describe('useQuery Hook', () => {
 
       const user = userEvent.setup();
 
-      await waitFor(() => {
-        expect(screen.getByText('Loading!')).toBeTruthy();
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(screen.getByText("Loading!")).toBeTruthy();
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        expect(screen.getByText('Data rendered')).toBeTruthy();
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(screen.getByText("Data rendered")).toBeTruthy();
+        },
+        { interval: 1 }
+      );
 
       // When we call refetch...
-      await user.click(screen.getByRole('button', { name: /Refetch!/i }))
+      await user.click(screen.getByRole("button", { name: /Refetch!/i }));
 
       // Data from the cache remains onscreen while network request
       // is made
-      expect(screen.getByText('Data rendered')).toBeTruthy();
+      expect(screen.getByText("Data rendered")).toBeTruthy();
     });
 
-    it('should persist errors on re-render with inline onError/onCompleted callbacks',  async () => {
-      const query = gql`{ hello }`;
+    it("should persist errors on re-render with inline onError/onCompleted callbacks", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error')]
-          }
-        }
+            errors: [new GraphQLError("error")],
+          },
+        },
       ];
 
       const cache = new InMemoryCache();
@@ -2650,38 +3043,49 @@ describe('useQuery Hook', () => {
 
       let updates = 0;
       const { result, rerender } = renderHook(
-        () => (updates++, useQuery(query, { onError: () => {}, onCompleted: () => {} })),
-        { wrapper },
+        () => (
+          updates++,
+          useQuery(query, { onError: () => {}, onCompleted: () => {} })
+        ),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
 
       rerender();
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
 
       expect(onErrorFn).toHaveBeenCalledTimes(0);
       let previousUpdates = updates;
-      await expect(waitFor(() => {
-        expect(updates).not.toEqual(previousUpdates)
-      }, { interval: 1, timeout: 20 })).rejects.toThrow()
+      await expect(
+        waitFor(
+          () => {
+            expect(updates).not.toEqual(previousUpdates);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
     });
 
-    it('should not persist errors when variables change', async () => {
+    it("should not persist errors when variables change", async () => {
       const query = gql`
         query hello($id: ID) {
           hello(id: $id)
@@ -2695,7 +3099,7 @@ describe('useQuery Hook', () => {
             variables: { id: 1 },
           },
           result: {
-            errors: [new GraphQLError('error')]
+            errors: [new GraphQLError("error")],
           },
         },
         {
@@ -2704,7 +3108,7 @@ describe('useQuery Hook', () => {
             variables: { id: 2 },
           },
           result: {
-            data: { hello: 'world 2' },
+            data: { hello: "world 2" },
           },
         },
         {
@@ -2713,7 +3117,7 @@ describe('useQuery Hook', () => {
             variables: { id: 1 },
           },
           result: {
-            data: { hello: 'world 1' },
+            data: { hello: "world 1" },
           },
         },
       ];
@@ -2722,35 +3126,39 @@ describe('useQuery Hook', () => {
         ({ id }) => useQuery(query, { variables: { id } }),
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
           ),
           initialProps: { id: 1 },
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error');
+      expect(result.current.error!.message).toBe("error");
 
       rerender({ id: 2 });
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world 2' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world 2" });
       expect(result.current.error).toBe(undefined);
 
       rerender({ id: 1 });
@@ -2758,29 +3166,36 @@ describe('useQuery Hook', () => {
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world 1' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world 1" });
       expect(result.current.error).toBe(undefined);
     });
 
-    it('should render multiple errors when refetching', async () => {
-      const query = gql`{ hello }`;
+    it("should render multiple errors when refetching", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error 1')]
-          }
+            errors: [new GraphQLError("error 1")],
+          },
         },
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('error 2')]
+            errors: [new GraphQLError("error 2")],
           },
           delay: 10,
-        }
+        },
       ];
 
       const cache = new InMemoryCache();
@@ -2792,58 +3207,71 @@ describe('useQuery Hook', () => {
 
       const { result } = renderHook(
         () => useQuery(query, { notifyOnNetworkStatusChange: true }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error 1');
+      expect(result.current.error!.message).toBe("error 1");
 
       const catchFn = jest.fn();
 
       result.current.refetch().catch(catchFn);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toBe(undefined);
-      expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('error 2');
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toBe(undefined);
+      expect(result.current.error).toBeInstanceOf(ApolloError);
+      expect(result.current.error!.message).toBe("error 2");
 
       expect(catchFn.mock.calls.length).toBe(1);
       expect(catchFn.mock.calls[0].length).toBe(1);
       expect(catchFn.mock.calls[0][0]).toBeInstanceOf(ApolloError);
-      expect(catchFn.mock.calls[0][0].message).toBe('error 2');
+      expect(catchFn.mock.calls[0][0].message).toBe("error 2");
     });
 
-    it('should render the same error on refetch', async () => {
-      const query = gql`{ hello }`;
+    it("should render the same error on refetch", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
 
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('same error')]
-          }
+            errors: [new GraphQLError("same error")],
+          },
         },
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('same error')]
-          }
-        }
+            errors: [new GraphQLError("same error")],
+          },
+        },
       ];
 
       const cache = new InMemoryCache();
@@ -2855,21 +3283,23 @@ describe('useQuery Hook', () => {
 
       const { result } = renderHook(
         () => useQuery(query, { notifyOnNetworkStatusChange: true }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('same error');
-
+      expect(result.current.error!.message).toBe("same error");
 
       const catchFn = jest.fn();
       await act(async () => {
@@ -2879,37 +3309,41 @@ describe('useQuery Hook', () => {
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('same error');
+      expect(result.current.error!.message).toBe("same error");
 
       expect(catchFn.mock.calls.length).toBe(1);
       expect(catchFn.mock.calls[0].length).toBe(1);
       expect(catchFn.mock.calls[0][0]).toBeInstanceOf(ApolloError);
-      expect(catchFn.mock.calls[0][0].message).toBe('same error');
+      expect(catchFn.mock.calls[0][0].message).toBe("same error");
     });
 
-    it('should render data and errors with refetch', async () => {
-      const query = gql`{ hello }`;
+    it("should render data and errors with refetch", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('same error')],
+            errors: [new GraphQLError("same error")],
           },
         },
         {
           request: { query },
           result: {
-            data: { hello: 'world' },
+            data: { hello: "world" },
           },
           delay: 10,
         },
         {
           request: { query },
           result: {
-            errors: [new GraphQLError('same error')],
+            errors: [new GraphQLError("same error")],
           },
           delay: 10,
-        }
+        },
       ];
 
       const cache = new InMemoryCache();
@@ -2921,59 +3355,74 @@ describe('useQuery Hook', () => {
 
       const { result } = renderHook(
         () => useQuery(query, { notifyOnNetworkStatusChange: true }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('same error');
+      expect(result.current.error!.message).toBe("same error");
 
       result.current.refetch();
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(result.current.error).toBe(undefined);
 
       const catchFn = jest.fn();
       result.current.refetch().catch(catchFn);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(result.current.error).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       // TODO: Is this correct behavior here?
-      expect(result.current.data).toEqual({ hello: 'world' });
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.error!.message).toBe('same error');
+      expect(result.current.error!.message).toBe("same error");
 
       expect(catchFn.mock.calls.length).toBe(1);
       expect(catchFn.mock.calls[0].length).toBe(1);
       expect(catchFn.mock.calls[0][0]).toBeInstanceOf(ApolloError);
-      expect(catchFn.mock.calls[0][0].message).toBe('same error');
+      expect(catchFn.mock.calls[0][0].message).toBe("same error");
     });
 
-    it('should call onCompleted when variables change', async () => {
+    it("should call onCompleted when variables change", async () => {
       const query = gql`
         query people($first: Int) {
           allPeople(first: $first) {
@@ -2984,8 +3433,8 @@ describe('useQuery Hook', () => {
         }
       `;
 
-      const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } };
-      const data2 = { allPeople: { people: [{ name: 'Han Solo' }] } };
+      const data1 = { allPeople: { people: [{ name: "Luke Skywalker" }] } };
+      const data2 = { allPeople: { people: [{ name: "Han Solo" }] } };
       const mocks = [
         {
           request: { query, variables: { first: 1 } },
@@ -3003,45 +3452,52 @@ describe('useQuery Hook', () => {
         ({ variables }) => useQuery(query, { variables, onCompleted }),
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
           ),
           initialProps: {
             variables: { first: 1 },
           },
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data1);
       expect(onCompleted).toHaveBeenLastCalledWith(data1);
 
       rerender({ variables: { first: 2 } });
       expect(result.current.loading).toBe(true);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data2);
       expect(onCompleted).toHaveBeenLastCalledWith(data2);
 
       rerender({ variables: { first: 1 } });
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual(data1);
-      await waitFor(() => {
-        expect(onCompleted).toHaveBeenLastCalledWith(data1);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(onCompleted).toHaveBeenLastCalledWith(data1);
+        },
+        { interval: 1 }
+      );
 
       expect(onCompleted).toHaveBeenCalledTimes(3);
     });
   });
 
-  describe('Pagination', () => {
+  describe("Pagination", () => {
     const query = gql`
       query letters($limit: Int) {
         letters(limit: $limit) {
@@ -3052,13 +3508,13 @@ describe('useQuery Hook', () => {
     `;
 
     const ab = [
-      { name: 'A', position: 1 },
-      { name: 'B', position: 2 },
+      { name: "A", position: 1 },
+      { name: "B", position: 2 },
     ];
 
     const cd = [
-      { name: 'C', position: 3 },
-      { name: 'D', position: 4 },
+      { name: "C", position: 3 },
+      { name: "D", position: 4 },
     ];
 
     const mocks = [
@@ -3081,7 +3537,7 @@ describe('useQuery Hook', () => {
       },
     ];
 
-    it('should fetchMore with updateQuery', async () => {
+    it("should fetchMore with updateQuery", async () => {
       // TODO: Calling fetchMore with an updateQuery callback is deprecated
       const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
 
@@ -3091,7 +3547,7 @@ describe('useQuery Hook', () => {
 
       const { result } = renderHook(
         () => useQuery(query, { variables: { limit: 2 } }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
@@ -3104,23 +3560,29 @@ describe('useQuery Hook', () => {
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({ letters: ab });
 
-      await waitFor(() => void result.current.fetchMore({
-        variables: { limit: 2 },
-        updateQuery: (prev, { fetchMoreResult }) => ({
-          letters: prev.letters.concat(fetchMoreResult.letters),
-        }),
-      }));
+      await waitFor(
+        () =>
+          void result.current.fetchMore({
+            variables: { limit: 2 },
+            updateQuery: (prev, { fetchMoreResult }) => ({
+              letters: prev.letters.concat(fetchMoreResult.letters),
+            }),
+          })
+      );
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ letters: ab.concat(cd) });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ letters: ab.concat(cd) });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
 
       warnSpy.mockRestore();
     });
 
-    it('should fetchMore with updateQuery and notifyOnNetworkStatusChange', async () => {
+    it("should fetchMore with updateQuery and notifyOnNetworkStatusChange", async () => {
       // TODO: Calling fetchMore with an updateQuery callback is deprecated
       const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
 
@@ -3129,45 +3591,55 @@ describe('useQuery Hook', () => {
       );
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          variables: { limit: 2 },
-          notifyOnNetworkStatusChange: true,
-        }),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            variables: { limit: 2 },
+            notifyOnNetworkStatusChange: true,
+          }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({ letters: ab });
 
-      act(() => void result.current.fetchMore({
-        variables: { limit: 2 },
-        updateQuery: (prev, { fetchMoreResult }) => ({
-          letters: prev.letters.concat(fetchMoreResult.letters),
-        }),
-      }));
+      act(
+        () =>
+          void result.current.fetchMore({
+            variables: { limit: 2 },
+            updateQuery: (prev, { fetchMoreResult }) => ({
+              letters: prev.letters.concat(fetchMoreResult.letters),
+            }),
+          })
+      );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.fetchMore);
       expect(result.current.data).toEqual({ letters: ab });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({ letters: ab.concat(cd) });
 
       warnSpy.mockRestore();
     });
 
-    it('fetchMore with concatPagination', async () => {
+    it("fetchMore with concatPagination", async () => {
       const cache = new InMemoryCache({
         typePolicies: {
           Query: {
@@ -3179,33 +3651,41 @@ describe('useQuery Hook', () => {
       });
 
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result } = renderHook(
         () => useQuery(query, { variables: { limit: 2 } }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({ letters: ab });
       result.current.fetchMore({ variables: { limit: 2 } });
 
       expect(result.current.loading).toBe(false);
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ letters: ab.concat(cd) });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ letters: ab.concat(cd) });
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
     });
 
-    it('fetchMore with concatPagination and notifyOnNetworkStatusChange', async () => {
+    it("fetchMore with concatPagination and notifyOnNetworkStatusChange", async () => {
       const cache = new InMemoryCache({
         typePolicies: {
           Query: {
@@ -3217,24 +3697,30 @@ describe('useQuery Hook', () => {
       });
 
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          variables: { limit: 2 },
-          notifyOnNetworkStatusChange: true,
-        }),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            variables: { limit: 2 },
+            notifyOnNetworkStatusChange: true,
+          }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({ letters: ab });
 
@@ -3243,9 +3729,12 @@ describe('useQuery Hook', () => {
       expect(result.current.networkStatus).toBe(NetworkStatus.fetchMore);
       expect(result.current.data).toEqual({ letters: ab });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({ letters: ab.concat(cd) });
     });
@@ -3256,18 +3745,18 @@ describe('useQuery Hook', () => {
           Country: {
             fields: {
               cities: {
-                keyArgs: ['size'],
+                keyArgs: ["size"],
                 merge(existing, incoming, { args }) {
-                  if (!args) return incoming
+                  if (!args) return incoming;
 
-                  const items = existing ? existing.slice(0) : []
+                  const items = existing ? existing.slice(0) : [];
 
-                  const offset = args.offset ?? 0
+                  const offset = args.offset ?? 0;
                   for (let i = 0; i < incoming.length; ++i) {
-                    items[offset + i] = incoming[i]
+                    items[offset + i] = incoming[i];
                   }
 
-                  return items
+                  return items;
                 },
               },
             },
@@ -3306,49 +3795,51 @@ describe('useQuery Hook', () => {
 
       const countries = [
         {
-          __typename: 'Country',
+          __typename: "Country",
           id: 123,
           biggestCity: {
-            __typename: 'City',
+            __typename: "City",
             id: 234,
             info: {
-              __typename: 'CityInfo',
+              __typename: "CityInfo",
               airQuality: 0,
             },
           },
-          smallCities: [
-            { __typename: 'City', id: 345 },
-          ],
+          smallCities: [{ __typename: "City", id: 345 }],
         },
       ];
 
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={[
-          {
-            request: { query: GET_COUNTRIES },
-            result: { data: { countries } },
-          },
-        ]} cache={cache}>{children}</MockedProvider>
+        <MockedProvider
+          mocks={[
+            {
+              request: { query: GET_COUNTRIES },
+              result: { data: { countries } },
+            },
+          ]}
+          cache={cache}
+        >
+          {children}
+        </MockedProvider>
       );
 
-      const { result } = renderHook(
-        () => useQuery(GET_COUNTRIES),
-        { wrapper },
-      );
+      const { result } = renderHook(() => useQuery(GET_COUNTRIES), { wrapper });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBeUndefined();
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({ countries });
     });
   });
 
-
-  describe('Refetching', () => {
-    it('refetching with different variables', async () => {
+  describe("Refetching", () => {
+    it("refetching with different variables", async () => {
       const query = gql`
         query ($id: Int) {
           hello(id: $id)
@@ -3358,63 +3849,79 @@ describe('useQuery Hook', () => {
       const mocks = [
         {
           request: { query, variables: { id: 1 } },
-          result: { data: { hello: 'world 1' } },
+          result: { data: { hello: "world 1" } },
         },
         {
           request: { query, variables: { id: 2 } },
-          result: { data: { hello: 'world 2' } },
+          result: { data: { hello: "world 2" } },
           delay: 10,
         },
       ];
 
       const cache = new InMemoryCache();
       const wrapper = ({ children }: any) => (
-        <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+        <MockedProvider mocks={mocks} cache={cache}>
+          {children}
+        </MockedProvider>
       );
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          variables: { id: 1 },
-          notifyOnNetworkStatusChange: true,
-        }),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            variables: { id: 1 },
+            notifyOnNetworkStatusChange: true,
+          }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world 1' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world 1" });
 
       result.current.refetch({ id: 2 });
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world 2' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world 2" });
     });
 
-    it('refetching after an error', async () => {
-      const query = gql`{ hello }`;
+    it("refetching after an error", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'world 1' } },
+          result: { data: { hello: "world 1" } },
         },
         {
           request: { query },
-          error: new Error('This is an error!'),
+          error: new Error("This is an error!"),
           delay: 10,
         },
         {
           request: { query },
-          result: { data: { hello: 'world 2' } },
+          result: { data: { hello: "world 2" } },
           delay: 10,
         },
       ];
@@ -3422,57 +3929,73 @@ describe('useQuery Hook', () => {
       const cache = new InMemoryCache();
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          notifyOnNetworkStatusChange: true,
-        }),
+        () =>
+          useQuery(query, {
+            notifyOnNetworkStatusChange: true,
+          }),
         {
           wrapper: ({ children }) => (
             <MockedProvider mocks={mocks} cache={cache}>
               {children}
             </MockedProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error).toBe(undefined);
-      expect(result.current.data).toEqual({ hello: 'world 1' });
+      expect(result.current.data).toEqual({ hello: "world 1" });
 
       result.current.refetch();
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error).toBe(undefined);
-      expect(result.current.data).toEqual({ hello: 'world 1' });
+      expect(result.current.data).toEqual({ hello: "world 1" });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error).toBeInstanceOf(ApolloError);
-      expect(result.current.data).toEqual({ hello: 'world 1' });
+      expect(result.current.data).toEqual({ hello: "world 1" });
 
       result.current.refetch();
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error).toBe(undefined);
-      expect(result.current.data).toEqual({ hello: 'world 1' });
+      expect(result.current.data).toEqual({ hello: "world 1" });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error).toBe(undefined);
-      expect(result.current.data).toEqual({ hello: 'world 2' });
+      expect(result.current.data).toEqual({ hello: "world 2" });
     });
 
-    describe('refetchWritePolicy', () => {
+    describe("refetchWritePolicy", () => {
       const query = gql`
-        query GetPrimes ($min: number, $max: number) {
+        query GetPrimes($min: number, $max: number) {
           primes(min: $min, max: $max)
         }
       `;
@@ -3486,8 +4009,8 @@ describe('useQuery Hook', () => {
           result: {
             data: {
               primes: [2, 3, 5, 7, 11],
-            }
-          }
+            },
+          },
         },
         {
           request: {
@@ -3497,7 +4020,7 @@ describe('useQuery Hook', () => {
           result: {
             data: {
               primes: [13, 17, 19, 23, 29],
-            }
+            },
           },
           delay: 10,
         },
@@ -3522,40 +4045,46 @@ describe('useQuery Hook', () => {
         });
 
         const wrapper = ({ children }: any) => (
-          <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+          <MockedProvider mocks={mocks} cache={cache}>
+            {children}
+          </MockedProvider>
         );
 
         const { result } = renderHook(
-          () => useQuery(query, {
-            variables: { min: 0, max: 12 },
-            notifyOnNetworkStatusChange: true,
-            // This is the key line in this test.
-            refetchWritePolicy: 'overwrite',
-          }),
-          { wrapper },
+          () =>
+            useQuery(query, {
+              variables: { min: 0, max: 12 },
+              notifyOnNetworkStatusChange: true,
+              // This is the key line in this test.
+              refetchWritePolicy: "overwrite",
+            }),
+          { wrapper }
         );
 
         expect(result.current.loading).toBe(true);
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toBe(undefined);
-        expect(typeof result.current.refetch).toBe('function');
+        expect(typeof result.current.refetch).toBe("function");
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(false);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(false);
+          },
+          { interval: 1 }
+        );
         expect(result.current.error).toBeUndefined();
         expect(result.current.data).toEqual({ primes: [2, 3, 5, 7, 11] });
-        expect(mergeParams).toEqual([
-          [void 0, [2, 3, 5, 7, 11]],
-        ]);
-
+        expect(mergeParams).toEqual([[void 0, [2, 3, 5, 7, 11]]]);
 
         const thenFn = jest.fn();
         result.current.refetch({ min: 12, max: 30 }).then(thenFn);
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(true);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(true);
+          },
+          { interval: 1 }
+        );
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toEqual({
           // We get the stale data because we configured keyArgs: false.
@@ -3566,9 +4095,12 @@ describe('useQuery Hook', () => {
         // called refetch with new variables.
         expect(result.current.networkStatus).toBe(NetworkStatus.setVariables);
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(false);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(false);
+          },
+          { interval: 1 }
+        );
 
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toEqual({ primes: [13, 17, 19, 23, 29] });
@@ -3606,40 +4138,46 @@ describe('useQuery Hook', () => {
         });
 
         const wrapper = ({ children }: any) => (
-          <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+          <MockedProvider mocks={mocks} cache={cache}>
+            {children}
+          </MockedProvider>
         );
 
         const { result } = renderHook(
-          () => useQuery(query, {
-            variables: { min: 0, max: 12 },
-            notifyOnNetworkStatusChange: true,
-            // This is the key line in this test.
-            refetchWritePolicy: 'merge',
-          }),
-          { wrapper },
+          () =>
+            useQuery(query, {
+              variables: { min: 0, max: 12 },
+              notifyOnNetworkStatusChange: true,
+              // This is the key line in this test.
+              refetchWritePolicy: "merge",
+            }),
+          { wrapper }
         );
 
         expect(result.current.loading).toBe(true);
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toBe(undefined);
-        expect(typeof result.current.refetch).toBe('function');
+        expect(typeof result.current.refetch).toBe("function");
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(false);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(false);
+          },
+          { interval: 1 }
+        );
         expect(result.current.error).toBeUndefined();
         expect(result.current.data).toEqual({ primes: [2, 3, 5, 7, 11] });
-        expect(mergeParams).toEqual([
-          [undefined, [2, 3, 5, 7, 11]],
-        ]);
-
+        expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
 
         const thenFn = jest.fn();
         result.current.refetch({ min: 12, max: 30 }).then(thenFn);
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(true);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(true);
+          },
+          { interval: 1 }
+        );
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toEqual({
           // We get the stale data because we configured keyArgs: false.
@@ -3648,9 +4186,12 @@ describe('useQuery Hook', () => {
         // This networkStatus is setVariables instead of refetch because we
         // called refetch with new variables.
         expect(result.current.networkStatus).toBe(NetworkStatus.setVariables);
-        await waitFor(() => {
-          expect(result.current.loading).toBe(false);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(false);
+          },
+          { interval: 1 }
+        );
 
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toEqual({
@@ -3659,13 +4200,19 @@ describe('useQuery Hook', () => {
         expect(mergeParams).toEqual([
           [void 0, [2, 3, 5, 7, 11]],
           // This indicates concatenation happened.
-          [[2, 3, 5, 7, 11], [13, 17, 19, 23, 29]],
+          [
+            [2, 3, 5, 7, 11],
+            [13, 17, 19, 23, 29],
+          ],
         ]);
         expect(mergeParams).toEqual([
           [undefined, [2, 3, 5, 7, 11]],
           // Without refetchWritePolicy: "overwrite", this array will be
           // all 10 primes (2 through 29) together.
-          [[2, 3, 5, 7, 11], [13, 17, 19, 23, 29]],
+          [
+            [2, 3, 5, 7, 11],
+            [13, 17, 19, 23, 29],
+          ],
         ]);
 
         expect(thenFn).toHaveBeenCalledTimes(1);
@@ -3695,39 +4242,45 @@ describe('useQuery Hook', () => {
         });
 
         const wrapper = ({ children }: any) => (
-          <MockedProvider mocks={mocks} cache={cache}>{children}</MockedProvider>
+          <MockedProvider mocks={mocks} cache={cache}>
+            {children}
+          </MockedProvider>
         );
 
         const { result } = renderHook(
-          () => useQuery(query, {
-            variables: { min: 0, max: 12 },
-            notifyOnNetworkStatusChange: true,
-            // Intentionally not passing refetchWritePolicy.
-          }),
-          { wrapper },
+          () =>
+            useQuery(query, {
+              variables: { min: 0, max: 12 },
+              notifyOnNetworkStatusChange: true,
+              // Intentionally not passing refetchWritePolicy.
+            }),
+          { wrapper }
         );
 
         expect(result.current.loading).toBe(true);
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toBe(undefined);
-        expect(typeof result.current.refetch).toBe('function');
+        expect(typeof result.current.refetch).toBe("function");
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(false);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(false);
+          },
+          { interval: 1 }
+        );
         expect(result.current.error).toBeUndefined();
         expect(result.current.data).toEqual({ primes: [2, 3, 5, 7, 11] });
-        expect(mergeParams).toEqual([
-          [void 0, [2, 3, 5, 7, 11]],
-        ]);
-
+        expect(mergeParams).toEqual([[void 0, [2, 3, 5, 7, 11]]]);
 
         const thenFn = jest.fn();
         result.current.refetch({ min: 12, max: 30 }).then(thenFn);
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(true);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(true);
+          },
+          { interval: 1 }
+        );
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toEqual({
           // We get the stale data because we configured keyArgs: false.
@@ -3738,9 +4291,12 @@ describe('useQuery Hook', () => {
         // called refetch with new variables.
         expect(result.current.networkStatus).toBe(NetworkStatus.setVariables);
 
-        await waitFor(() => {
-          expect(result.current.loading).toBe(false);
-        }, { interval: 1 });
+        await waitFor(
+          () => {
+            expect(result.current.loading).toBe(false);
+          },
+          { interval: 1 }
+        );
 
         expect(result.current.error).toBe(undefined);
         expect(result.current.data).toEqual({ primes: [13, 17, 19, 23, 29] });
@@ -3761,14 +4317,18 @@ describe('useQuery Hook', () => {
     });
   });
 
-  describe('Callbacks', () => {
-    it('onCompleted is called once with cached data', async () => {
-      const query = gql`{ hello }`;
+  describe("Callbacks", () => {
+    it("onCompleted is called once with cached data", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
 
       const cache = new InMemoryCache();
       cache.writeQuery({
         query,
-        data: { hello: 'world' },
+        data: { hello: "world" },
       });
 
       const wrapper = ({ children }: any) => (
@@ -3779,34 +4339,51 @@ describe('useQuery Hook', () => {
 
       const onCompleted = jest.fn();
       const { result } = renderHook(
-        () => (useQuery(query, {
-          fetchPolicy: 'cache-only',
-          onCompleted,
-        })),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            fetchPolicy: "cache-only",
+            onCompleted,
+          }),
+        { wrapper }
       );
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'world' });
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(onCompleted).toHaveBeenCalledTimes(1);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(onCompleted).toHaveBeenCalledWith({ hello: 'world' });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world" });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(onCompleted).toHaveBeenCalledTimes(1);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(onCompleted).toHaveBeenCalledWith({ hello: "world" });
+        },
+        { interval: 1 }
+      );
       expect(onCompleted).toHaveBeenCalledTimes(1);
     });
 
-    it('onCompleted is called once despite state changes', async () => {
-      const query = gql`{ hello }`;
+    it("onCompleted is called once despite state changes", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'world' } },
+          result: { data: { hello: "world" } },
         },
       ];
 
@@ -3819,37 +4396,45 @@ describe('useQuery Hook', () => {
 
       const onCompleted = jest.fn();
       const { result, rerender } = renderHook(
-        () => useQuery(query, {
-          onCompleted,
-        }),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            onCompleted,
+          }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
-      expect(result.current.data).toEqual({ hello: 'world' });
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(onCompleted).toHaveBeenCalledTimes(1);
-      expect(onCompleted).toHaveBeenCalledWith({ hello: 'world' });
+      expect(onCompleted).toHaveBeenCalledWith({ hello: "world" });
 
       rerender();
       expect(result.current.loading).toBe(false);
-      expect(result.current.data).toEqual({ hello: 'world' });
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(onCompleted).toHaveBeenCalledTimes(1);
-      expect(onCompleted).toHaveBeenCalledWith({ hello: 'world' });
+      expect(onCompleted).toHaveBeenCalledWith({ hello: "world" });
       expect(onCompleted).toHaveBeenCalledTimes(1);
     });
 
-    it('should not call onCompleted if skip is true', async () => {
-      const query = gql`{ hello }`;
+    it("should not call onCompleted if skip is true", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'world' } },
+          result: { data: { hello: "world" } },
         },
       ];
 
@@ -3862,28 +4447,41 @@ describe('useQuery Hook', () => {
 
       const onCompleted = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, {
-          skip: true,
-          onCompleted,
-        }),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            skip: true,
+            onCompleted,
+          }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
 
-      await expect(waitFor(() => {
-        expect(onCompleted).not.toHaveBeenCalledTimes(0);
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      expect(onCompleted).toHaveBeenCalledTimes(0);
+
+      await expect(
+        waitFor(
+          () => {
+            expect(onCompleted).toHaveBeenCalledTimes(1);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
+
       expect(onCompleted).toHaveBeenCalledTimes(0);
     });
 
-    it('should not make extra network requests when `onCompleted` is defined with a `network-only` fetch policy', async () => {
-      const query = gql`{ hello }`;
+    it("should not make extra network requests when `onCompleted` is defined with a `network-only` fetch policy", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'world' } },
+          result: { data: { hello: "world" } },
         },
       ];
 
@@ -3899,79 +4497,170 @@ describe('useQuery Hook', () => {
       const { result } = renderHook(
         () => {
           const pendingResult = useQuery(query, {
-            fetchPolicy: 'network-only',
+            fetchPolicy: "network-only",
             onCompleted,
           });
           updates++;
 
           return pendingResult;
         },
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world' });
-      const previousUpdates = updates
-      await expect(waitFor(() => {
-        expect(updates).not.toEqual(previousUpdates)
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world" });
+      const previousUpdates = updates;
+      await expect(
+        waitFor(
+          () => {
+            expect(updates).not.toEqual(previousUpdates);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
       expect(onCompleted).toHaveBeenCalledTimes(1);
     });
 
-    it('onCompleted should work with polling', async () => {
-      const query = gql`{ hello }`;
+    it("onCompleted should not fire for polling queries without notifyOnNetworkStatusChange: true", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'world 1' } },
+          result: { data: { hello: "world 1" } },
         },
         {
           request: { query },
-          result: { data: { hello: 'world 2' } },
+          result: { data: { hello: "world 2" } },
         },
         {
           request: { query },
-          result: { data: { hello: 'world 3' } },
+          result: { data: { hello: "world 3" } },
         },
       ];
 
       const cache = new InMemoryCache();
       const onCompleted = jest.fn();
       const { result } = renderHook(
-        () => useQuery(query, {
-          onCompleted,
-          pollInterval: 10,
-        }),
+        () =>
+          useQuery(query, {
+            onCompleted,
+            pollInterval: 10,
+          }),
         {
           wrapper: ({ children }) => (
             <MockedProvider mocks={mocks} cache={cache}>
               {children}
             </MockedProvider>
           ),
+        }
+      );
+
+      expect(result.current.loading).toBe(true);
+
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 1" });
+        },
+        { interval: 1 }
+      );
+      expect(result.current.loading).toBe(false);
+      expect(onCompleted).toHaveBeenCalledTimes(1);
+
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 2" });
+        },
+        { interval: 1 }
+      );
+      expect(result.current.loading).toBe(false);
+      expect(onCompleted).toHaveBeenCalledTimes(1);
+
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 3" });
+        },
+        { interval: 1 }
+      );
+      expect(result.current.loading).toBe(false);
+      expect(onCompleted).toHaveBeenCalledTimes(1);
+    });
+
+    it("onCompleted should fire when polling with notifyOnNetworkStatusChange: true", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
+      const mocks = [
+        {
+          request: { query },
+          result: { data: { hello: "world 1" } },
+        },
+        {
+          request: { query },
+          result: { data: { hello: "world 2" } },
+        },
+        {
+          request: { query },
+          result: { data: { hello: "world 3" } },
         },
+      ];
+
+      const cache = new InMemoryCache();
+      const onCompleted = jest.fn();
+      const { result } = renderHook(
+        () =>
+          useQuery(query, {
+            onCompleted,
+            notifyOnNetworkStatusChange: true,
+            pollInterval: 10,
+          }),
+        {
+          wrapper: ({ children }) => (
+            <MockedProvider mocks={mocks} cache={cache}>
+              {children}
+            </MockedProvider>
+          ),
+        }
       );
 
       expect(result.current.loading).toBe(true);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'world 1' });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 1" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
       expect(onCompleted).toHaveBeenCalledTimes(1);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'world 2' });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 2" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
       expect(onCompleted).toHaveBeenCalledTimes(2);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'world 3' });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 3" });
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
       expect(onCompleted).toHaveBeenCalledTimes(3);
     });
@@ -4021,29 +4710,161 @@ describe('useQuery Hook', () => {
       expect(errorSpy).not.toHaveBeenCalled();
       errorSpy.mockRestore();
     });
-  });
 
-  describe('Optimistic data', () => {
-    it('should display rolled back optimistic data when an error occurs', async () => {
+    it("onCompleted should not execute on cache writes after initial query execution", async () => {
       const query = gql`
-        query AllCars {
-          cars {
-            id
-            make
-            model
-          }
+        {
+          hello
         }
       `;
-
-      const carsData = {
-        cars: [
-          {
-            id: 1,
-            make: 'Audi',
-            model: 'RS8',
-            __typename: 'Car'
-          }
-        ]
+      const mocks = [
+        {
+          request: { query },
+          result: { data: { hello: "foo" } },
+        },
+        {
+          request: { query },
+          result: { data: { hello: "bar" } },
+        },
+      ];
+      const link = new MockLink(mocks);
+      const cache = new InMemoryCache();
+      const onCompleted = jest.fn();
+
+      const ChildComponent: React.FC = () => {
+        const { data, client } = useQuery(query, { onCompleted });
+        function refetchQueries() {
+          client.refetchQueries({ include: "active" });
+        }
+        function writeQuery() {
+          client.writeQuery({ query, data: { hello: "baz" } });
+        }
+        return (
+          <div>
+            <span>Data: {data?.hello}</span>
+            <button onClick={() => refetchQueries()}>Refetch queries</button>
+            <button onClick={() => writeQuery()}>Update word</button>
+          </div>
+        );
+      };
+
+      const ParentComponent: React.FC = () => {
+        return (
+          <MockedProvider link={link} cache={cache}>
+            <div>
+              <ChildComponent />
+            </div>
+          </MockedProvider>
+        );
+      };
+
+      render(<ParentComponent />);
+
+      await screen.findByText("Data: foo");
+      await userEvent.click(
+        screen.getByRole("button", { name: /refetch queries/i })
+      );
+      expect(onCompleted).toBeCalledTimes(1);
+      await screen.findByText("Data: bar");
+      await userEvent.click(
+        screen.getByRole("button", { name: /update word/i })
+      );
+      expect(onCompleted).toBeCalledTimes(1);
+      await screen.findByText("Data: baz");
+      expect(onCompleted).toBeCalledTimes(1);
+    });
+
+    it("onCompleted should execute on cache writes after initial query execution with notifyOnNetworkStatusChange: true", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
+      const mocks = [
+        {
+          request: { query },
+          result: { data: { hello: "foo" } },
+        },
+        {
+          request: { query },
+          result: { data: { hello: "bar" } },
+        },
+      ];
+      const link = new MockLink(mocks);
+      const cache = new InMemoryCache();
+      const onCompleted = jest.fn();
+
+      const ChildComponent: React.FC = () => {
+        const { data, client } = useQuery(query, {
+          onCompleted,
+          notifyOnNetworkStatusChange: true,
+        });
+        function refetchQueries() {
+          client.refetchQueries({ include: "active" });
+        }
+        function writeQuery() {
+          client.writeQuery({ query, data: { hello: "baz" } });
+        }
+        return (
+          <div>
+            <span>Data: {data?.hello}</span>
+            <button onClick={() => refetchQueries()}>Refetch queries</button>
+            <button onClick={() => writeQuery()}>Update word</button>
+          </div>
+        );
+      };
+
+      const ParentComponent: React.FC = () => {
+        return (
+          <MockedProvider link={link} cache={cache}>
+            <div>
+              <ChildComponent />
+            </div>
+          </MockedProvider>
+        );
+      };
+
+      render(<ParentComponent />);
+
+      await screen.findByText("Data: foo");
+      expect(onCompleted).toBeCalledTimes(1);
+      await userEvent.click(
+        screen.getByRole("button", { name: /refetch queries/i })
+      );
+      // onCompleted increments when refetch occurs since we're hitting the network...
+      expect(onCompleted).toBeCalledTimes(2);
+      await screen.findByText("Data: bar");
+      await userEvent.click(
+        screen.getByRole("button", { name: /update word/i })
+      );
+      // but not on direct cache write, since there's no network request to complete
+      expect(onCompleted).toBeCalledTimes(2);
+      await screen.findByText("Data: baz");
+      expect(onCompleted).toBeCalledTimes(2);
+    });
+  });
+
+  describe("Optimistic data", () => {
+    it("should display rolled back optimistic data when an error occurs", async () => {
+      const query = gql`
+        query AllCars {
+          cars {
+            id
+            make
+            model
+          }
+        }
+      `;
+
+      const carsData = {
+        cars: [
+          {
+            id: 1,
+            make: "Audi",
+            model: "RS8",
+            __typename: "Car",
+          },
+        ],
       };
 
       const mutation = gql`
@@ -4058,16 +4879,13 @@ describe('useQuery Hook', () => {
 
       const carData = {
         id: 2,
-        make: 'Ford',
-        model: 'Pinto',
-        __typename: 'Car'
+        make: "Ford",
+        model: "Pinto",
+        __typename: "Car",
       };
 
       const allCarsData = {
-        cars: [
-          carsData.cars[0],
-          carData
-        ]
+        cars: [carsData.cars[0], carData],
       };
 
       const mocks = [
@@ -4077,9 +4895,9 @@ describe('useQuery Hook', () => {
         },
         {
           request: { query: mutation },
-          error: new Error('Oh no!'),
+          error: new Error("Oh no!"),
           delay: 500,
-        }
+        },
       ];
 
       const cache = new InMemoryCache();
@@ -4100,16 +4918,21 @@ describe('useQuery Hook', () => {
                   cars(existing, { readField }) {
                     const newCarRef = cache.writeFragment({
                       data: data!.addCar,
-                      fragment: gql`fragment NewCar on Car {
-                        id
-                        make
-                        model
-                      }`,
+                      fragment: gql`
+                        fragment NewCar on Car {
+                          id
+                          make
+                          model
+                        }
+                      `,
                     });
 
-                    if (existing.some(
-                      (ref: Reference) => readField('id', ref) === data!.addCar.id
-                    )) {
+                    if (
+                      existing.some(
+                        (ref: Reference) =>
+                          readField("id", ref) === data!.addCar.id
+                      )
+                    ) {
                       return existing;
                     }
 
@@ -4122,15 +4945,18 @@ describe('useQuery Hook', () => {
           }),
           query: useQuery(query),
         }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.query.loading).toBe(true);
       const mutate = result.current.mutation[0];
 
-      await waitFor(() => {
-        expect(result.current.query.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.query.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.query.loading).toBe(false);
       expect(result.current.query.data).toEqual(carsData);
 
@@ -4152,7 +4978,7 @@ describe('useQuery Hook', () => {
 
       await waitFor(() => {
         expect(result.current.mutation[1].loading).toBe(false);
-      })
+      });
 
       // The mutation has completely finished, leaving the query with access to
       // the original cache data.
@@ -4161,15 +4987,20 @@ describe('useQuery Hook', () => {
       expect(result.current.query.data).toEqual(carsData);
 
       expect(onError).toHaveBeenCalledTimes(1);
-      expect(onError.mock.calls[0][0].message).toBe('Oh no!');
+      expect(onError.mock.calls[0][0].message).toBe("Oh no!");
     });
   });
 
-  describe('Partial refetch', () => {
-    it('should attempt a refetch when data is missing and partialRefetch is true', async () => {
-      const errorSpy = jest.spyOn(console, 'error')
+  describe("Partial refetch", () => {
+    it("should attempt a refetch when data is missing and partialRefetch is true", async () => {
+      const errorSpy = jest
+        .spyOn(console, "error")
         .mockImplementation(() => {});
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
 
       const link = mockSingleLink(
         {
@@ -4190,17 +5021,16 @@ describe('useQuery Hook', () => {
       });
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          partialRefetch: true,
-          notifyOnNetworkStatusChange: true,
-        }),
+        () =>
+          useQuery(query, {
+            partialRefetch: true,
+            notifyOnNetworkStatusChange: true,
+          }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
@@ -4208,27 +5038,33 @@ describe('useQuery Hook', () => {
       expect(result.current.error).toBe(undefined);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
 
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.refetch);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.refetch);
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
       expect(errorSpy).toHaveBeenCalledTimes(1);
-      expect(errorSpy.mock.calls[0][0]).toMatch('Missing field');
+      expect(errorSpy.mock.calls[0][0]).toMatch("Missing field");
       errorSpy.mockRestore();
 
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
-      expect(result.current.data).toEqual({ hello: 'world' });
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(result.current.error).toBe(undefined);
     });
 
-    it('should attempt a refetch when data is missing and partialRefetch is true 2', async () => {
+    it("should attempt a refetch when data is missing and partialRefetch is true 2", async () => {
       const query = gql`
         query people {
           allPeople(first: 1) {
@@ -4240,10 +5076,11 @@ describe('useQuery Hook', () => {
       `;
 
       const data = {
-        allPeople: { people: [{ name: 'Luke Skywalker' }] },
+        allPeople: { people: [{ name: "Luke Skywalker" }] },
       };
 
-      const errorSpy = jest.spyOn(console, 'error')
+      const errorSpy = jest
+        .spyOn(console, "error")
         .mockImplementation(() => {});
       const link = mockSingleLink(
         { request: { query }, result: { data: {} }, delay: 20 },
@@ -4256,17 +5093,16 @@ describe('useQuery Hook', () => {
       });
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          partialRefetch: true,
-          notifyOnNetworkStatusChange: true,
-        }),
+        () =>
+          useQuery(query, {
+            partialRefetch: true,
+            notifyOnNetworkStatusChange: true,
+          }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
@@ -4274,29 +5110,40 @@ describe('useQuery Hook', () => {
       expect(result.current.error).toBe(undefined);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
 
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.refetch);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.refetch);
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
 
       expect(errorSpy).toHaveBeenCalledTimes(1);
-      expect(errorSpy.mock.calls[0][0]).toMatch('Missing field');
+      expect(errorSpy.mock.calls[0][0]).toMatch("Missing field");
       errorSpy.mockRestore();
 
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual(data);
       expect(result.current.error).toBe(undefined);
     });
 
-    it('should attempt a refetch when data is missing, partialRefetch is true and addTypename is false for the cache', async () => {
-      const errorSpy = jest.spyOn(console, 'error')
+    it("should attempt a refetch when data is missing, partialRefetch is true and addTypename is false for the cache", async () => {
+      const errorSpy = jest
+        .spyOn(console, "error")
         .mockImplementation(() => {});
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
 
       const link = mockSingleLink(
         {
@@ -4318,17 +5165,16 @@ describe('useQuery Hook', () => {
       });
 
       const wrapper = ({ children }: any) => (
-        <ApolloProvider client={client}>
-          {children}
-        </ApolloProvider>
+        <ApolloProvider client={client}>{children}</ApolloProvider>
       );
 
       const { result } = renderHook(
-        () => useQuery(query, {
-          partialRefetch: true,
-          notifyOnNetworkStatusChange: true,
-        }),
-        { wrapper },
+        () =>
+          useQuery(query, {
+            partialRefetch: true,
+            notifyOnNetworkStatusChange: true,
+          }),
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
@@ -4336,29 +5182,35 @@ describe('useQuery Hook', () => {
       expect(result.current.error).toBe(undefined);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
 
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.refetch);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.refetch);
+        },
+        { interval: 1 }
+      );
       expect(result.current.loading).toBe(true);
       expect(result.current.error).toBe(undefined);
       expect(result.current.data).toBe(undefined);
 
       expect(errorSpy).toHaveBeenCalledTimes(1);
-      expect(errorSpy.mock.calls[0][0]).toMatch('Missing field');
+      expect(errorSpy.mock.calls[0][0]).toMatch("Missing field");
       errorSpy.mockRestore();
 
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
-      expect(result.current.data).toEqual({ hello: 'world' });
+      expect(result.current.data).toEqual({ hello: "world" });
       expect(result.current.error).toBe(undefined);
     });
   });
 
-  describe('Client Resolvers', () => {
-    it('should receive up to date @client(always: true) fields on entity update', async () => {
+  describe("Client Resolvers", () => {
+    it("should receive up to date @client(always: true) fields on entity update", async () => {
       const query = gql`
         query GetClientData($id: ID) {
           clientEntity(id: $id) @client(always: true) {
@@ -4388,23 +5240,23 @@ describe('useQuery Hook', () => {
         resolvers: {
           ClientData: {
             titleLength(data) {
-              return data.title.length
-            }
+              return data.title.length;
+            },
           },
           Query: {
-            clientEntity(_root, {id}, {cache}) {
+            clientEntity(_root, { id }, { cache }) {
               return cache.readFragment({
-                id: cache.identify({id, __typename: "ClientData"}),
+                id: cache.identify({ id, __typename: "ClientData" }),
                 fragment,
               });
             },
           },
           Mutation: {
-            addOrUpdate(_root, {id, title}, {cache}) {
+            addOrUpdate(_root, { id, title }, { cache }) {
               return cache.writeFragment({
-                id: cache.identify({id, __typename: "ClientData"}),
+                id: cache.identify({ id, __typename: "ClientData" }),
                 fragment,
-                data: {id, title, __typename: "ClientData"},
+                data: { id, title, __typename: "ClientData" },
               });
             },
           },
@@ -4412,8 +5264,8 @@ describe('useQuery Hook', () => {
       });
 
       const entityId = 1;
-      const shortTitle = 'Short';
-      const longerTitle = 'A little longer';
+      const shortTitle = "Short";
+      const longerTitle = "A little longer";
       client.mutate({
         mutation,
         variables: {
@@ -4423,28 +5275,29 @@ describe('useQuery Hook', () => {
       });
 
       const wrapper = ({ children }: any) => (
-        <ApolloProvider client={client}>
-          {children}
-        </ApolloProvider>
+        <ApolloProvider client={client}>{children}</ApolloProvider>
       );
 
       const { result } = renderHook(
         () => useQuery(query, { variables: { id: entityId } }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toEqual({
         clientEntity: {
           id: entityId,
           title: shortTitle,
           titleLength: shortTitle.length,
-          __typename: 'ClientData',
+          __typename: "ClientData",
         },
       });
 
@@ -4454,45 +5307,56 @@ describe('useQuery Hook', () => {
           variables: {
             id: entityId,
             title: longerTitle,
-          }
+          },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          clientEntity: {
-            id: entityId,
-            title: longerTitle,
-            titleLength: longerTitle.length,
-            __typename: "ClientData",
-          },
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            clientEntity: {
+              id: entityId,
+              title: longerTitle,
+              titleLength: longerTitle.length,
+              __typename: "ClientData",
+            },
+          });
+        },
+        { interval: 1 }
+      );
     });
   });
 
-  describe('Skipping', () => {
-    const query = gql`query greeting($someVar: Boolean) { hello }`;
+  describe("Skipping", () => {
+    const query = gql`
+      query greeting($someVar: Boolean) {
+        hello
+      }
+    `;
     const mocks = [
       {
         request: { query },
-        result: { data: { hello: 'world' } },
+        result: { data: { hello: "world" } },
       },
       {
         request: {
           query,
           variables: { someVar: true },
         },
-        result: { data: { hello: 'world' } },
+        result: { data: { hello: "world" } },
       },
     ];
 
-    it('should skip running a query when `skip` is `true`', async () => {
-      const query = gql`{ hello }`;
+    it("should skip running a query when `skip` is `true`", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'world' } },
+          result: { data: { hello: "world" } },
         },
       ];
 
@@ -4505,7 +5369,7 @@ describe('useQuery Hook', () => {
 
       const { result, rerender } = renderHook(
         ({ skip }) => useQuery(query, { skip }),
-        { wrapper, initialProps: { skip: true }  },
+        { wrapper, initialProps: { skip: true } }
       );
 
       expect(result.current.loading).toBe(false);
@@ -4515,13 +5379,16 @@ describe('useQuery Hook', () => {
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBeFalsy();
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBeFalsy();
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world" });
     });
 
-    it('should not make network requests when `skip` is `true`', async () => {
+    it("should not make network requests when `skip` is `true`", async () => {
       const linkFn = jest.fn();
       const link = new ApolloLink((o, f) => {
         linkFn();
@@ -4533,23 +5400,24 @@ describe('useQuery Hook', () => {
       });
 
       const wrapper = ({ children }: any) => (
-        <ApolloProvider client={client}>
-          {children}
-        </ApolloProvider>
+        <ApolloProvider client={client}>{children}</ApolloProvider>
       );
 
       const { result, rerender } = renderHook(
         ({ skip, variables }) => useQuery(query, { skip, variables }),
-        { wrapper, initialProps: { skip: false, variables: undefined as any } },
+        { wrapper, initialProps: { skip: false, variables: undefined as any } }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world" });
 
       rerender({ skip: true, variables: { someVar: true } });
       expect(result.current.loading).toBe(false);
@@ -4557,88 +5425,96 @@ describe('useQuery Hook', () => {
       expect(linkFn).toHaveBeenCalledTimes(1);
     });
 
-    it('should tear down the query if `skip` is `true`', async () => {
+    it("should tear down the query if `skip` is `true`", async () => {
       const client = new ApolloClient({
-        link: new ApolloLink(() => Observable.of({ data: { hello: 'world' } })),
+        link: new ApolloLink(() => Observable.of({ data: { hello: "world" } })),
         cache: new InMemoryCache(),
       });
 
       const wrapper = ({ children }: any) => (
-        <ApolloProvider client={client}>
-          {children}
-        </ApolloProvider>
+        <ApolloProvider client={client}>{children}</ApolloProvider>
       );
 
-      const { unmount } = renderHook(
-        () => useQuery(query, { skip: true }),
-        { wrapper },
-      );
+      const { unmount } = renderHook(() => useQuery(query, { skip: true }), {
+        wrapper,
+      });
 
-      expect(client.getObservableQueries('all').size).toBe(1);
+      expect(client.getObservableQueries("all").size).toBe(1);
       unmount();
-      await new Promise(resolve => setTimeout(resolve));
-      expect(client.getObservableQueries('all').size).toBe(0);
+      await new Promise((resolve) => setTimeout(resolve));
+      expect(client.getObservableQueries("all").size).toBe(0);
     });
 
-    it('should treat fetchPolicy standby like skip', async () => {
-      const query = gql`{ hello }`;
+    it("should treat fetchPolicy standby like skip", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
-          result: { data: { hello: 'world' } },
+          result: { data: { hello: "world" } },
         },
       ];
       const { result, rerender } = renderHook(
         ({ fetchPolicy }) => useQuery(query, { fetchPolicy }),
         {
           wrapper: ({ children }) => (
-            <MockedProvider mocks={mocks}>
-              {children}
-            </MockedProvider>
+            <MockedProvider mocks={mocks}>{children}</MockedProvider>
           ),
-          initialProps: { fetchPolicy: 'standby' as any },
-        },
+          initialProps: { fetchPolicy: "standby" as any },
+        }
       );
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toBe(undefined);
 
-      await expect(waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1, timeout: 20 })).rejects.toThrow();
+      await expect(
+        waitFor(
+          () => {
+            expect(result.current.loading).toBe(true);
+          },
+          { interval: 1, timeout: 20 }
+        )
+      ).rejects.toThrow();
 
-      rerender({ fetchPolicy: 'cache-first' });
+      rerender({ fetchPolicy: "cache-first" });
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBeFalsy();
-      }, { interval: 1 });
-      expect(result.current.data).toEqual({ hello: 'world' });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBeFalsy();
+        },
+        { interval: 1 }
+      );
+      expect(result.current.data).toEqual({ hello: "world" });
     });
 
     // Amusingly, #8270 thinks this is a bug, but #9101 thinks this is not.
-    it('should refetch when skip is true', async () => {
-      const query = gql`{ hello }`;
-      const link = new ApolloLink(() => Observable.of({
-        data: { hello: 'world' },
-      }));
+    it("should refetch when skip is true", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
+      const link = new ApolloLink(() =>
+        Observable.of({
+          data: { hello: "world" },
+        })
+      );
 
-      const requestSpy = jest.spyOn(link, 'request');
+      const requestSpy = jest.spyOn(link, "request");
       const client = new ApolloClient({
         cache: new InMemoryCache(),
         link,
       });
 
-      const { result } = renderHook(
-        () => useQuery(query, { skip: true }),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query, { skip: true }), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       let promise;
       await waitFor(async () => {
@@ -4646,29 +5522,41 @@ describe('useQuery Hook', () => {
       });
 
       expect(result.current.loading).toBe(false);
-      await waitFor(() => {
-        expect(result.current.data).toBe(undefined);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toBe(undefined);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.loading).toBe(false);
-      await waitFor(() => {
-        expect(result.current.data).toBe(undefined);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toBe(undefined);
+        },
+        { interval: 1 }
+      );
       expect(requestSpy).toHaveBeenCalledTimes(1);
       requestSpy.mockRestore();
       expect(promise).resolves.toEqual({
-        data: {hello: "world"},
+        data: { hello: "world" },
         loading: false,
         networkStatus: 7,
       });
     });
 
-    it('should set correct initialFetchPolicy even if skip:true', async () => {
-      const query = gql`{ hello }`;
+    it("should set correct initialFetchPolicy even if skip:true", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       let linkCount = 0;
-      const link = new ApolloLink(() => Observable.of({
-        data: { hello: ++linkCount },
-      }));
+      const link = new ApolloLink(() =>
+        Observable.of({
+          data: { hello: ++linkCount },
+        })
+      );
 
       const client = new ApolloClient({
         cache: new InMemoryCache(),
@@ -4678,26 +5566,28 @@ describe('useQuery Hook', () => {
       const correctInitialFetchPolicy: WatchQueryFetchPolicy =
         "cache-and-network";
 
-      const { result, rerender } = renderHook<QueryResult, {
-        skip: boolean;
-      }>(
-        ({ skip = true }) => useQuery(query, {
-          // Skipping equates to using a fetchPolicy of "standby", but that
-          // should not mean we revert to standby whenever we want to go back to
-          // the initial fetchPolicy (e.g. when variables change).
-          skip,
-          fetchPolicy: correctInitialFetchPolicy,
-        }),
+      const { result, rerender } = renderHook<
+        QueryResult,
+        {
+          skip: boolean;
+        }
+      >(
+        ({ skip = true }) =>
+          useQuery(query, {
+            // Skipping equates to using a fetchPolicy of "standby", but that
+            // should not mean we revert to standby whenever we want to go back to
+            // the initial fetchPolicy (e.g. when variables change).
+            skip,
+            fetchPolicy: correctInitialFetchPolicy,
+          }),
         {
           initialProps: {
             skip: true,
           },
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(false);
@@ -4705,77 +5595,75 @@ describe('useQuery Hook', () => {
 
       function check(
         expectedFetchPolicy: WatchQueryFetchPolicy,
-        expectedInitialFetchPolicy: WatchQueryFetchPolicy,
+        expectedInitialFetchPolicy: WatchQueryFetchPolicy
       ) {
         const { observable } = result.current;
-        const {
-          fetchPolicy,
-          initialFetchPolicy,
-        } = observable.options;
+        const { fetchPolicy, initialFetchPolicy } = observable.options;
 
         expect(fetchPolicy).toBe(expectedFetchPolicy);
         expect(initialFetchPolicy).toBe(expectedInitialFetchPolicy);
       }
 
-      check(
-        "standby",
-        correctInitialFetchPolicy,
-      );
+      check("standby", correctInitialFetchPolicy);
 
       rerender({
         skip: false,
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toEqual({
         hello: 1,
       });
 
-      check(
-        correctInitialFetchPolicy,
-        correctInitialFetchPolicy,
-      );
+      check(correctInitialFetchPolicy, correctInitialFetchPolicy);
 
       const reasons: string[] = [];
 
-      const reobservePromise = result.current.observable.reobserve({
-        variables: {
-          newVar: true,
-        },
-        nextFetchPolicy(currentFetchPolicy, context) {
-          expect(currentFetchPolicy).toBe("cache-and-network");
-          expect(context.initialFetchPolicy).toBe("cache-and-network");
-          reasons.push(context.reason);
-          return currentFetchPolicy;
-        },
-      }).then(result => {
-        expect(result.loading).toBe(false);
-        expect(result.data).toEqual({ hello: 2 });
-      });
+      const reobservePromise = result.current.observable
+        .reobserve({
+          variables: {
+            newVar: true,
+          },
+          nextFetchPolicy(currentFetchPolicy, context) {
+            expect(currentFetchPolicy).toBe("cache-and-network");
+            expect(context.initialFetchPolicy).toBe("cache-and-network");
+            reasons.push(context.reason);
+            return currentFetchPolicy;
+          },
+        })
+        .then((result) => {
+          expect(result.loading).toBe(false);
+          expect(result.data).toEqual({ hello: 2 });
+        });
 
       expect(result.current.loading).toBe(false);
 
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          hello: 2,
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            hello: 2,
+          });
+        },
+        { interval: 1 }
+      );
 
       await reobservePromise;
 
-      expect(reasons).toEqual([
-        "variables-changed",
-        "after-fetch",
-      ]);
+      expect(reasons).toEqual(["variables-changed", "after-fetch"]);
     });
   });
 
-  describe('Missing Fields', () => {
-    it('should log debug messages about MissingFieldErrors from the cache', async () => {
-      const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+  describe("Missing Fields", () => {
+    it("should log debug messages about MissingFieldErrors from the cache", async () => {
+      const errorSpy = jest
+        .spyOn(console, "error")
+        .mockImplementation(() => {});
 
       const carQuery: DocumentNode = gql`
         query cars($id: Int) {
@@ -4793,18 +5681,18 @@ describe('useQuery Hook', () => {
         cars: [
           {
             id: 1,
-            make: 'Audi',
-            model: 'RS8',
-            vine: 'DOLLADOLLABILL',
-            __typename: 'Car'
-          }
-        ]
+            make: "Audi",
+            model: "RS8",
+            vine: "DOLLADOLLABILL",
+            __typename: "Car",
+          },
+        ],
       };
 
       const mocks = [
         {
           request: { query: carQuery, variables: { id: 1 } },
-          result: { data: carData }
+          result: { data: carData },
         },
       ];
 
@@ -4817,32 +5705,37 @@ describe('useQuery Hook', () => {
 
       const { result } = renderHook(
         () => useQuery(carQuery, { variables: { id: 1 } }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.error).toBe(undefined);
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(carData);
       expect(result.current.error).toBeUndefined();
 
-      expect(errorSpy).toHaveBeenCalledTimes(1);
+      expect(errorSpy).toHaveBeenCalled();
       expect(errorSpy).toHaveBeenLastCalledWith(
-        `Missing field 'vin' while writing result ${JSON.stringify({
+        `Missing field '%s' while writing result %o`,
+        "vin",
+        {
           id: 1,
           make: "Audi",
           model: "RS8",
           vine: "DOLLADOLLABILL",
-          __typename: "Car"
-        }, null, 2)}`
+          __typename: "Car",
+        }
       );
       errorSpy.mockRestore();
     });
 
-    it('should return partial cache data when `returnPartialData` is true', async () => {
+    it("should return partial cache data when `returnPartialData` is true", async () => {
       const cache = new InMemoryCache();
       const client = new ApolloClient({
         cache,
@@ -4867,15 +5760,15 @@ describe('useQuery Hook', () => {
         data: {
           cars: [
             {
-              __typename: 'Car',
-              make: 'Ford',
-              model: 'Mustang',
-              vin: 'PONY123',
+              __typename: "Car",
+              make: "Ford",
+              model: "Mustang",
+              vin: "PONY123",
               repairs: [
                 {
-                  __typename: 'Repair',
-                  date: '2019-05-08',
-                  description: 'Could not get after it.',
+                  __typename: "Repair",
+                  date: "2019-05-08",
+                  description: "Could not get after it.",
                 },
               ],
             },
@@ -4898,22 +5791,20 @@ describe('useQuery Hook', () => {
         () => useQuery(partialQuery, { returnPartialData: true }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toEqual({
         cars: [
           {
-            __typename: 'Car',
+            __typename: "Car",
             repairs: [
               {
-                __typename: 'Repair',
-                date: '2019-05-08',
+                __typename: "Repair",
+                date: "2019-05-08",
               },
             ],
           },
@@ -4921,7 +5812,7 @@ describe('useQuery Hook', () => {
       });
     });
 
-    it('should not return partial cache data when `returnPartialData` is false', () => {
+    it("should not return partial cache data when `returnPartialData` is false", () => {
       const cache = new InMemoryCache();
       const client = new ApolloClient({
         cache,
@@ -4946,15 +5837,15 @@ describe('useQuery Hook', () => {
         data: {
           cars: [
             {
-              __typename: 'Car',
-              make: 'Ford',
-              model: 'Mustang',
-              vin: 'PONY123',
+              __typename: "Car",
+              make: "Ford",
+              model: "Mustang",
+              vin: "PONY123",
               repairs: [
                 {
-                  __typename: 'Repair',
-                  date: '2019-05-08',
-                  description: 'Could not get after it.',
+                  __typename: "Repair",
+                  date: "2019-05-08",
+                  description: "Could not get after it.",
                 },
               ],
             },
@@ -4977,18 +5868,16 @@ describe('useQuery Hook', () => {
         () => useQuery(partialQuery, { returnPartialData: false }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
     });
 
-    it('should not return partial cache data when `returnPartialData` is false and new variables are passed in', async () => {
+    it("should not return partial cache data when `returnPartialData` is false and new variables are passed in", async () => {
       const cache = new InMemoryCache();
       const client = new ApolloClient({
         cache,
@@ -4997,7 +5886,7 @@ describe('useQuery Hook', () => {
 
       const query = gql`
         query MyCar($id: ID) {
-          car (id: $id) {
+          car(id: $id) {
             id
             make
           }
@@ -5006,7 +5895,7 @@ describe('useQuery Hook', () => {
 
       const partialQuery = gql`
         query MyCar($id: ID) {
-          car (id: $id) {
+          car(id: $id) {
             id
             make
             model
@@ -5019,10 +5908,10 @@ describe('useQuery Hook', () => {
         variables: { id: 1 },
         data: {
           car: {
-            __typename: 'Car',
+            __typename: "Car",
             id: 1,
-            make: 'Ford',
-            model: 'Pinto',
+            make: "Ford",
+            model: "Pinto",
           },
         },
       });
@@ -5032,15 +5921,14 @@ describe('useQuery Hook', () => {
         variables: { id: 2 },
         data: {
           car: {
-            __typename: 'Car',
+            __typename: "Car",
             id: 2,
-            make: 'Ford',
-            model: 'Pinto',
+            make: "Ford",
+            model: "Pinto",
           },
         },
       });
 
-
       let setId: any;
       const { result } = renderHook(
         () => {
@@ -5054,20 +5942,18 @@ describe('useQuery Hook', () => {
         },
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(false);
       expect(result.current.data).toEqual({
         car: {
-          __typename: 'Car',
+          __typename: "Car",
           id: 2,
-          make: 'Ford',
-          model: 'Pinto',
+          make: "Ford",
+          model: "Pinto",
         },
       });
 
@@ -5075,16 +5961,19 @@ describe('useQuery Hook', () => {
         setId(1);
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.data).toBe(undefined);
     });
   });
 
-  describe('Previous data', () => {
-    it('should persist previous data when a query is re-run', async () => {
+  describe("Previous data", () => {
+    it("should persist previous data when a query is re-run", async () => {
       const query = gql`
         query car {
           car {
@@ -5097,17 +5986,17 @@ describe('useQuery Hook', () => {
       const data1 = {
         car: {
           id: 1,
-          make: 'Venturi',
-          __typename: 'Car',
-        }
+          make: "Venturi",
+          __typename: "Car",
+        },
       };
 
       const data2 = {
         car: {
           id: 2,
-          make: 'Wiesmann',
-          __typename: 'Car',
-        }
+          make: "Wiesmann",
+          __typename: "Car",
+        },
       };
 
       const mocks = [
@@ -5124,42 +6013,54 @@ describe('useQuery Hook', () => {
 
       const { result } = renderHook(
         () => useQuery(query, { notifyOnNetworkStatusChange: true }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.previousData).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data1);
       expect(result.current.previousData).toBe(undefined);
 
       setTimeout(() => result.current.refetch());
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data1);
       expect(result.current.previousData).toEqual(data1);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data2);
       expect(result.current.previousData).toEqual(data1);
     });
 
-    it('should persist result.previousData across multiple results', async () => {
-      const query: TypedDocumentNode<{
-        car: {
-          id: string;
-          make: string;
-        };
-      }, {
-        vin?: string;
-      }> = gql`
+    it("should persist result.previousData across multiple results", async () => {
+      const query: TypedDocumentNode<
+        {
+          car: {
+            id: string;
+            make: string;
+          };
+        },
+        {
+          vin?: string;
+        }
+      > = gql`
         query car($vin: String) {
           car(vin: $vin) {
             id
@@ -5171,24 +6072,24 @@ describe('useQuery Hook', () => {
       const data1 = {
         car: {
           id: 1,
-          make: 'Venturi',
-          __typename: 'Car',
+          make: "Venturi",
+          __typename: "Car",
         },
       };
 
       const data2 = {
         car: {
           id: 2,
-          make: 'Wiesmann',
-          __typename: 'Car',
+          make: "Wiesmann",
+          __typename: "Car",
         },
       };
 
       const data3 = {
         car: {
           id: 3,
-          make: 'Beetle',
-          __typename: 'Car',
+          make: "Beetle",
+          __typename: "Car",
         },
       };
 
@@ -5213,23 +6114,29 @@ describe('useQuery Hook', () => {
 
       const { result } = renderHook(
         () => useQuery(query, { notifyOnNetworkStatusChange: true }),
-        { wrapper },
+        { wrapper }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
       expect(result.current.previousData).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data1);
       expect(result.current.previousData).toBe(undefined);
 
       setTimeout(() => result.current.refetch());
-      await waitFor(() => {
-        expect(result.current.loading).toBe(true);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(true);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data1);
       expect(result.current.previousData).toEqual(data1);
 
@@ -5238,63 +6145,82 @@ describe('useQuery Hook', () => {
       expect(result.current.data).toEqual(data1);
       expect(result.current.previousData).toEqual(data1);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual(data3);
       expect(result.current.previousData).toEqual(data1);
     });
 
-    it('should persist result.previousData even if query changes', async () => {
+    it("should persist result.previousData even if query changes", async () => {
       const aQuery: TypedDocumentNode<{
         a: string;
-      }> = gql`query A { a }`;
+      }> = gql`
+        query A {
+          a
+        }
+      `;
 
       const abQuery: TypedDocumentNode<{
         a: string;
         b: number;
-      }> = gql`query AB { a b }`;
+      }> = gql`
+        query AB {
+          a
+          b
+        }
+      `;
 
       const bQuery: TypedDocumentNode<{
         b: number;
-      }> = gql`query B { b }`;
+      }> = gql`
+        query B {
+          b
+        }
+      `;
 
       let stringOfAs = "";
       let countOfBs = 0;
       const client = new ApolloClient({
         cache: new InMemoryCache(),
-        link: new ApolloLink(request => new Observable(observer => {
-          switch (request.operationName) {
-            case "A": {
-              observer.next({
-                data: {
-                  a: stringOfAs += 'a',
-                },
-              });
-              break;
-            }
-            case "AB": {
-              observer.next({
-                data: {
-                  a: stringOfAs += 'a',
-                  b: countOfBs += 1,
-                },
-              });
-              break;
-            }
-            case "B": {
-              observer.next({
-                data: {
-                  b: countOfBs += 1,
-                },
-              });
-              break;
-            }
-          }
-          setTimeout(() => {
-            observer.complete();
-          }, 10);
-        })),
+        link: new ApolloLink(
+          (request) =>
+            new Observable((observer) => {
+              switch (request.operationName) {
+                case "A": {
+                  observer.next({
+                    data: {
+                      a: (stringOfAs += "a"),
+                    },
+                  });
+                  break;
+                }
+                case "AB": {
+                  observer.next({
+                    data: {
+                      a: (stringOfAs += "a"),
+                      b: (countOfBs += 1),
+                    },
+                  });
+                  break;
+                }
+                case "B": {
+                  observer.next({
+                    data: {
+                      b: (countOfBs += 1),
+                    },
+                  });
+                  break;
+                }
+              }
+              setTimeout(() => {
+                observer.complete();
+              }, 10);
+            })
+        ),
       });
 
       const { result } = renderHook(
@@ -5311,141 +6237,213 @@ describe('useQuery Hook', () => {
         },
         {
           wrapper: ({ children }: any) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
+      await waitFor(
+        () => {
+          const { loading } = result.current.useQueryResult;
+          expect(loading).toBe(true);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { data } = result.current.useQueryResult;
+          expect(data).toEqual({ a: "a" });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { previousData } = result.current.useQueryResult;
+          expect(previousData).toBeUndefined();
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        const { loading } = result.current.useQueryResult;
-        expect(loading).toBe(true);
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { data } = result.current.useQueryResult;
-        expect(data).toEqual({ a: "a" });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { previousData } = result.current.useQueryResult;
-        expect(previousData).toBeUndefined();
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          const { loading } = result.current.useQueryResult;
+          expect(loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { data } = result.current.useQueryResult;
+          expect(data).toEqual({ a: "a" });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { previousData } = result.current.useQueryResult;
+          expect(previousData).toBe(undefined);
+        },
+        { interval: 1 }
+      );
 
+      await expect(
+        await waitFor(
+          () => {
+            result.current.setQuery(abQuery);
+          },
+          { interval: 1 }
+        )
+      );
 
-      await waitFor(() => {
-        const { loading } = result.current.useQueryResult;
-        expect(loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { data } = result.current.useQueryResult;
-        expect(data).toEqual({ a: "a" });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { previousData } = result.current.useQueryResult;
-        expect(previousData).toBe(undefined);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          const { loading } = result.current.useQueryResult;
+          expect(loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { data } = result.current.useQueryResult;
+          expect(data).toEqual({ a: "aa", b: 1 });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { previousData } = result.current.useQueryResult;
+          expect(previousData).toEqual({ a: "a" });
+        },
+        { interval: 1 }
+      );
 
-      await expect(await waitFor(() => {
-        result.current.setQuery(abQuery);
-      }, { interval: 1 }));
+      await waitFor(
+        () => {
+          const { loading } = result.current.useQueryResult;
+          expect(loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { data } = result.current.useQueryResult;
+          expect(data).toEqual({ a: "aa", b: 1 });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { previousData } = result.current.useQueryResult;
+          expect(previousData).toEqual({ a: "a" });
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        const { loading } = result.current.useQueryResult;
-        expect(loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { data } = result.current.useQueryResult;
-        expect(data).toEqual({ a: "aa", b: 1 });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { previousData } = result.current.useQueryResult;
-        expect(previousData).toEqual({ a: "a" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          result.current.useQueryResult.reobserve().then((result) => {
+            expect(result.loading).toBe(false);
+            expect(result.data).toEqual({ a: "aaa", b: 2 });
+          });
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        const { loading } = result.current.useQueryResult;
-        expect(loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { data } = result.current.useQueryResult;
-        expect(data).toEqual({ a: "aa", b: 1 });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { previousData } = result.current.useQueryResult;
-        expect(previousData).toEqual({ a: "a" });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          const { loading } = result.current.useQueryResult;
+          expect(loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { data } = result.current.useQueryResult;
+          expect(data).toEqual({ a: "aaa", b: 2 });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { previousData } = result.current.useQueryResult;
+          expect(previousData).toEqual({ a: "aa", b: 1 });
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        result.current.useQueryResult.reobserve().then(result => {
-          expect(result.loading).toBe(false);
-          expect(result.data).toEqual({ a: "aaa", b: 2 });
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          result.current.setQuery(bQuery);
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        const { loading } = result.current.useQueryResult;
-        expect(loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { data } = result.current.useQueryResult;
-        expect(data).toEqual({ a: "aaa", b: 2 });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { previousData } = result.current.useQueryResult;
-        expect(previousData).toEqual({ a: "aa", b: 1 });
-      }, { interval: 1 });
-
-      await waitFor(() => {
-        result.current.setQuery(bQuery);
-      }, { interval: 1 });
-
-      await waitFor(() => {
-        const { loading } = result.current.useQueryResult;
-        expect(loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { data } = result.current.useQueryResult;
-        expect(data).toEqual({ b: 3 });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { previousData } = result.current.useQueryResult;
-        expect(previousData).toEqual({ b: 2 });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { loading } = result.current.useQueryResult;
-        expect(loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { data } = result.current.useQueryResult;
-        expect(data).toEqual({ b: 3 });
-      }, { interval: 1 });
-      await waitFor(() => {
-        const { previousData } = result.current.useQueryResult;
-        expect(previousData).toEqual({ b: 2 });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          const { loading } = result.current.useQueryResult;
+          expect(loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { data } = result.current.useQueryResult;
+          expect(data).toEqual({ b: 3 });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { previousData } = result.current.useQueryResult;
+          expect(previousData).toEqual({ b: 2 });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { loading } = result.current.useQueryResult;
+          expect(loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { data } = result.current.useQueryResult;
+          expect(data).toEqual({ b: 3 });
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          const { previousData } = result.current.useQueryResult;
+          expect(previousData).toEqual({ b: 2 });
+        },
+        { interval: 1 }
+      );
     });
 
     it("should be cleared when variables change causes cache miss", async () => {
       const peopleData = [
-        { id: 1, name: 'John Smith', gender: 'male' },
-        { id: 2, name: 'Sara Smith', gender: 'female' },
-        { id: 3, name: 'Budd Deey', gender: 'nonbinary' },
-        { id: 4, name: 'Johnny Appleseed', gender: 'male' },
-        { id: 5, name: 'Ada Lovelace', gender: 'female' },
+        { id: 1, name: "John Smith", gender: "male" },
+        { id: 2, name: "Sara Smith", gender: "female" },
+        { id: 3, name: "Budd Deey", gender: "nonbinary" },
+        { id: 4, name: "Johnny Appleseed", gender: "male" },
+        { id: 5, name: "Ada Lovelace", gender: "female" },
       ];
 
-      const link = new ApolloLink(operation => {
-        return new Observable(observer => {
+      const link = new ApolloLink((operation) => {
+        return new Observable((observer) => {
           const { gender } = operation.variables;
-          new Promise(resolve => setTimeout(resolve, 300)).then(() => {
+          new Promise((resolve) => setTimeout(resolve, 300)).then(() => {
             observer.next({
               data: {
-                people: gender === "all" ? peopleData :
-                  gender ? peopleData.filter(
-                    person => person.gender === gender
-                  ) : peopleData,
-              }
+                people:
+                  gender === "all"
+                    ? peopleData
+                    : gender
+                    ? peopleData.filter((person) => person.gender === gender)
+                    : peopleData,
+              },
             });
             observer.complete();
           });
@@ -5477,61 +6475,75 @@ describe('useQuery Hook', () => {
       );
 
       const { result, rerender } = renderHook(
-        ({ gender }) => useQuery(query, {
-          variables: { gender },
-          fetchPolicy: 'network-only',
-        }),
-        { wrapper, initialProps: { gender: 'all' } },
+        ({ gender }) =>
+          useQuery(query, {
+            variables: { gender },
+            fetchPolicy: "network-only",
+          }),
+        { wrapper, initialProps: { gender: "all" } }
       );
 
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
 
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({
         people: peopleData.map(({ gender, ...person }) => person),
       });
 
-      rerender({ gender: 'female' });
+      rerender({ gender: "female" });
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.setVariables);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({
         people: peopleData
-          .filter((person) => person.gender === 'female')
+          .filter((person) => person.gender === "female")
           .map(({ gender, ...person }) => person),
       });
 
-      rerender({ gender: 'nonbinary' });
+      rerender({ gender: "nonbinary" });
       expect(result.current.loading).toBe(true);
       expect(result.current.networkStatus).toBe(NetworkStatus.setVariables);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.networkStatus).toBe(NetworkStatus.ready);
       expect(result.current.data).toEqual({
         people: peopleData
-          .filter((person) => person.gender === 'nonbinary')
+          .filter((person) => person.gender === "nonbinary")
           .map(({ gender, ...person }) => person),
       });
     });
   });
 
-  describe('defaultOptions', () => {
-    it('should allow polling options to be passed to the client', async () => {
-      const query = gql`{ hello }`;
+  describe("defaultOptions", () => {
+    it("should allow polling options to be passed to the client", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const cache = new InMemoryCache();
       const link = mockSingleLink(
         {
@@ -5545,7 +6557,7 @@ describe('useQuery Hook', () => {
         {
           request: { query },
           result: { data: { hello: "world 3" } },
-        },
+        }
       );
 
       const client = new ApolloClient({
@@ -5558,45 +6570,58 @@ describe('useQuery Hook', () => {
         link,
       });
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'world 1' });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 1" });
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'world 2' });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 2" });
+        },
+        { interval: 1 }
+      );
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ hello: 'world 3' });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ hello: "world 3" });
+        },
+        { interval: 1 }
+      );
     });
   });
 
-  describe('canonical cache results', () => {
-    it('can be disabled via useQuery options', async () => {
+  describe("canonical cache results", () => {
+    it("can be disabled via useQuery options", async () => {
       const cache = new InMemoryCache({
         canonizeResults: true,
         typePolicies: {
@@ -5626,20 +6651,19 @@ describe('useQuery Hook', () => {
       cache.writeQuery({
         query,
         data: { results },
-      })
+      });
 
       const wrapper = ({ children }: any) => (
-        <MockedProvider cache={cache}>
-          {children}
-        </MockedProvider>
+        <MockedProvider cache={cache}>{children}</MockedProvider>
       );
 
       const { result, rerender } = renderHook(
-        ({ canonizeResults }) => useQuery(query, {
-          fetchPolicy: 'cache-only',
-          canonizeResults,
-        }),
-        { wrapper, initialProps: { canonizeResults: false } },
+        ({ canonizeResults }) =>
+          useQuery(query, {
+            fetchPolicy: "cache-only",
+            canonizeResults,
+          }),
+        { wrapper, initialProps: { canonizeResults: false } }
       );
 
       expect(result.current.loading).toBe(false);
@@ -5667,12 +6691,18 @@ describe('useQuery Hook', () => {
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ results });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ results });
+        },
+        { interval: 1 }
+      );
       expect(result.current.data.results.length).toBe(7);
       resultSet = new Set(result.current.data.results);
       // Since canonization is happening now, the duplicate 1 results are
@@ -5684,8 +6714,8 @@ describe('useQuery Hook', () => {
     });
   });
 
-  describe('canonical cache results', () => {
-    it('can be disabled via useQuery options', async () => {
+  describe("canonical cache results", () => {
+    it("can be disabled via useQuery options", async () => {
       const cache = new InMemoryCache({
         canonizeResults: true,
         typePolicies: {
@@ -5715,20 +6745,19 @@ describe('useQuery Hook', () => {
       cache.writeQuery({
         query,
         data: { results },
-      })
+      });
 
       const wrapper = ({ children }: any) => (
-        <MockedProvider cache={cache}>
-          {children}
-        </MockedProvider>
+        <MockedProvider cache={cache}>{children}</MockedProvider>
       );
 
       const { result, rerender } = renderHook(
-        ({ canonizeResults }) => useQuery(query, {
-          fetchPolicy: 'cache-only',
-          canonizeResults,
-        }),
-        { wrapper, initialProps: { canonizeResults: false } },
+        ({ canonizeResults }) =>
+          useQuery(query, {
+            fetchPolicy: "cache-only",
+            canonizeResults,
+          }),
+        { wrapper, initialProps: { canonizeResults: false } }
       );
 
       expect(result.current.loading).toBe(false);
@@ -5756,12 +6785,18 @@ describe('useQuery Hook', () => {
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({ results });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({ results });
+        },
+        { interval: 1 }
+      );
       expect(result.current.data.results.length).toBe(7);
       resultSet = new Set(result.current.data.results);
       // Since canonization is happening now, the duplicate 1 results are
@@ -5781,11 +6816,25 @@ describe('useQuery Hook', () => {
 
     const aQuery: TypedDocumentNode<{
       a: ABFields;
-    }> = gql`query A { a { id name }}`;
+    }> = gql`
+      query A {
+        a {
+          id
+          name
+        }
+      }
+    `;
 
     const bQuery: TypedDocumentNode<{
       b: ABFields;
-    }> = gql`query B { b { id name }}`;
+    }> = gql`
+      query B {
+        b {
+          id
+          name
+        }
+      }
+    `;
 
     const aData = {
       a: {
@@ -5805,29 +6854,32 @@ describe('useQuery Hook', () => {
 
     function makeClient() {
       return new ApolloClient({
-        cache: new InMemoryCache,
-        link: new ApolloLink(operation => new Observable(observer => {
-            switch (operation.operationName) {
-              case "A":
-                setTimeout(() => {
-                  observer.next({ data: aData });
-                  observer.complete();
-                });
-                break;
-              case "B":
-                setTimeout(() => {
-                  observer.next({ data: bData });
-                  observer.complete();
-                }, 10);
-                break;
-            }
-        })),
+        cache: new InMemoryCache(),
+        link: new ApolloLink(
+          (operation) =>
+            new Observable((observer) => {
+              switch (operation.operationName) {
+                case "A":
+                  setTimeout(() => {
+                    observer.next({ data: aData });
+                    observer.complete();
+                  });
+                  break;
+                case "B":
+                  setTimeout(() => {
+                    observer.next({ data: bData });
+                    observer.complete();
+                  }, 10);
+                  break;
+              }
+            })
+        ),
       });
     }
 
     async function check(
       aFetchPolicy: WatchQueryFetchPolicy,
-      bFetchPolicy: WatchQueryFetchPolicy,
+      bFetchPolicy: WatchQueryFetchPolicy
     ) {
       const client = makeClient();
       const { result } = renderHook(
@@ -5839,7 +6891,7 @@ describe('useQuery Hook', () => {
           wrapper: ({ children }) => (
             <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.a.loading).toBe(true);
@@ -5857,55 +6909,39 @@ describe('useQuery Hook', () => {
       expect(result.current.b.data).toEqual(bData);
     }
 
-    it("cache-first for both", () => check(
-      "cache-first",
-      "cache-first",
-    ));
-
-    it("cache-first first, cache-and-network second", () => check(
-      "cache-first",
-      "cache-and-network",
-    ));
-
-    it("cache-first first, network-only second", () => check(
-      "cache-first",
-      "network-only",
-    ));
-
-    it("cache-and-network for both", () => check(
-      "cache-and-network",
-      "cache-and-network",
-    ));
-
-    it("cache-and-network first, cache-first second", () => check(
-      "cache-and-network",
-      "cache-first",
-    ));
-
-    it("cache-and-network first, network-only second", () => check(
-      "cache-and-network",
-      "network-only",
-    ));
-
-    it("network-only for both", () => check(
-      "network-only",
-      "network-only",
-    ));
-
-    it("network-only first, cache-first second", () => check(
-      "network-only",
-      "cache-first",
-    ));
-
-    it("network-only first, cache-and-network second", () => check(
-      "network-only",
-      "cache-and-network",
-    ));
+    it("cache-first for both", () => check("cache-first", "cache-first"));
+
+    it("cache-first first, cache-and-network second", () =>
+      check("cache-first", "cache-and-network"));
+
+    it("cache-first first, network-only second", () =>
+      check("cache-first", "network-only"));
+
+    it("cache-and-network for both", () =>
+      check("cache-and-network", "cache-and-network"));
+
+    it("cache-and-network first, cache-first second", () =>
+      check("cache-and-network", "cache-first"));
+
+    it("cache-and-network first, network-only second", () =>
+      check("cache-and-network", "network-only"));
+
+    it("network-only for both", () => check("network-only", "network-only"));
+
+    it("network-only first, cache-first second", () =>
+      check("network-only", "cache-first"));
+
+    it("network-only first, cache-and-network second", () =>
+      check("network-only", "cache-and-network"));
   });
 
-  describe('regression test issue #9204', () => {
-    it('should handle a simple query', async () => {
-      const query = gql`{ hello }`;
+  describe("regression test issue #9204", () => {
+    it("should handle a simple query", async () => {
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const mocks = [
         {
           request: { query },
@@ -5914,8 +6950,8 @@ describe('useQuery Hook', () => {
       ];
 
       const Component = ({ query }: any) => {
-        const [counter, setCounter] = useState(0)
-        const result = useQuery(query)
+        const [counter, setCounter] = useState(0);
+        const result = useQuery(query);
 
         useEffect(() => {
           /**
@@ -5925,17 +6961,19 @@ describe('useQuery Hook', () => {
           if (counter > 10) {
             console.error(`Too many results (${counter})`);
           } else {
-            setCounter(c => c + 1);
+            setCounter((c) => c + 1);
           }
-        }, [
-          result,
-          result.data,
-        ]);
+        }, [result, result.data]);
 
         if (result.loading) return null;
 
-        return <div>{result.data.hello}{counter}</div>;
-      }
+        return (
+          <div>
+            {result.data.hello}
+            {counter}
+          </div>
+        );
+      };
 
       render(
         <MockedProvider mocks={mocks}>
@@ -5944,18 +6982,22 @@ describe('useQuery Hook', () => {
       );
 
       await waitFor(() => {
-        expect(screen.getByText('world2')).toBeTruthy();
+        expect(screen.getByText("world2")).toBeTruthy();
       });
     });
   });
 
   // https://github.com/apollographql/apollo-client/issues/10222
-  describe('regression test issue #10222', () => {
-    it('maintains initial fetch policy when component unmounts and remounts', async () => {
+  describe("regression test issue #10222", () => {
+    it("maintains initial fetch policy when component unmounts and remounts", async () => {
       let helloCount = 1;
-      const query = gql`{ hello }`;
+      const query = gql`
+        {
+          hello
+        }
+      `;
       const link = new ApolloLink(() => {
-        return new Observable(observer => {
+        return new Observable((observer) => {
           const timer = setTimeout(() => {
             observer.next({ data: { hello: `hello ${helloCount++}` } });
             observer.complete();
@@ -5963,29 +7005,30 @@ describe('useQuery Hook', () => {
 
           return () => {
             clearTimeout(timer);
-          }
-        })
-      })
+          };
+        });
+      });
 
       const cache = new InMemoryCache();
 
       const client = new ApolloClient({
         link,
-        cache
+        cache,
       });
 
-      let setShow: Function
+      let setShow: Function;
       const Toggler = ({ children }: { children: ReactNode }) => {
         const [show, _setShow] = useState(true);
         setShow = _setShow;
 
         return show ? <>{children}</> : null;
-      }
+      };
 
       const { result } = renderHook(
-        () => useQuery(query, {
-            fetchPolicy: 'network-only',
-            nextFetchPolicy: 'cache-first'
+        () =>
+          useQuery(query, {
+            fetchPolicy: "network-only",
+            nextFetchPolicy: "cache-first",
           }),
         {
           wrapper: ({ children }) => (
@@ -5993,7 +7036,7 @@ describe('useQuery Hook', () => {
               <Toggler>{children}</Toggler>
             </ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
@@ -6003,8 +7046,8 @@ describe('useQuery Hook', () => {
         expect(result.current.loading).toBe(false);
       });
 
-      expect(result.current.data).toEqual({ hello: 'hello 1' });
-      expect(cache.readQuery({ query })).toEqual({ hello: 'hello 1' })
+      expect(result.current.data).toEqual({ hello: "hello 1" });
+      expect(cache.readQuery({ query })).toEqual({ hello: "hello 1" });
 
       act(() => {
         setShow(false);
@@ -6019,16 +7062,15 @@ describe('useQuery Hook', () => {
 
       await waitFor(() => {
         expect(result.current.loading).toBe(false);
-      })
+      });
 
-      expect(result.current.data).toEqual({ hello: 'hello 2' });
-      expect(cache.readQuery({ query })).toEqual({ hello: 'hello 2' })
+      expect(result.current.data).toEqual({ hello: "hello 2" });
+      expect(cache.readQuery({ query })).toEqual({ hello: "hello 2" });
     });
   });
 
-
-  describe('defer', () => {
-    it('should handle deferred queries', async () => {
+  describe("defer", () => {
+    it("should handle deferred queries", async () => {
       const query = gql`
         {
           greeting {
@@ -6049,16 +7091,11 @@ describe('useQuery Hook', () => {
         cache: new InMemoryCache(),
       });
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
@@ -6067,22 +7104,25 @@ describe('useQuery Hook', () => {
           result: {
             data: {
               greeting: {
-                message: 'Hello world',
-                __typename: 'Greeting',
+                message: "Hello world",
+                __typename: "Greeting",
               },
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({
         greeting: {
-          message: 'Hello world',
-          __typename: 'Greeting',
+          message: "Hello world",
+          __typename: "Greeting",
         },
       });
       expect(result.current).not.toContain({ hasNext: true });
@@ -6090,39 +7130,47 @@ describe('useQuery Hook', () => {
       setTimeout(() => {
         link.simulateResult({
           result: {
-            incremental: [{
-              data: {
-                recipient: {
-                  name: 'Alice',
-                  __typename: 'Person',
+            incremental: [
+              {
+                data: {
+                  recipient: {
+                    name: "Alice",
+                    __typename: "Person",
+                  },
+                  __typename: "Greeting",
                 },
-                __typename: 'Greeting',
+                path: ["greeting"],
               },
-              path: ['greeting'],
-            }],
-            hasNext: false
+            ],
+            hasNext: false,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greeting: {
-            message: 'Hello world',
-            __typename: 'Greeting',
-            recipient: {
-              name: 'Alice',
-              __typename: 'Person',
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greeting: {
+              message: "Hello world",
+              __typename: "Greeting",
+              recipient: {
+                name: "Alice",
+                __typename: "Person",
+              },
             },
-          },
-        });
-      }, { interval: 1 });
+          });
+        },
+        { interval: 1 }
+      );
     });
 
-    it('should handle deferred queries in lists', async () => {
+    it("should handle deferred queries in lists", async () => {
       const query = gql`
         {
           greetings {
@@ -6143,16 +7191,11 @@ describe('useQuery Hook', () => {
         cache: new InMemoryCache(),
       });
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
@@ -6161,106 +7204,125 @@ describe('useQuery Hook', () => {
           result: {
             data: {
               greetings: [
-                { message: 'Hello world', __typename: 'Greeting' },
-                { message: 'Hello again', __typename: 'Greeting' },
+                { message: "Hello world", __typename: "Greeting" },
+                { message: "Hello again", __typename: "Greeting" },
               ],
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
       expect(result.current.data).toEqual({
         greetings: [
-          { message: 'Hello world', __typename: 'Greeting' },
-          { message: 'Hello again', __typename: 'Greeting' },
+          { message: "Hello world", __typename: "Greeting" },
+          { message: "Hello again", __typename: "Greeting" },
         ],
       });
 
       setTimeout(() => {
         link.simulateResult({
           result: {
-            incremental: [{
-              data: {
-                recipient: {
-                  name: 'Alice',
-                  __typename: 'Person',
+            incremental: [
+              {
+                data: {
+                  recipient: {
+                    name: "Alice",
+                    __typename: "Person",
+                  },
+                  __typename: "Greeting",
                 },
-                __typename: 'Greeting',
+                path: ["greetings", 0],
               },
-              path: ['greetings', 0],
-            }],
+            ],
             hasNext: true,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greetings: [
-            {
-              message: 'Hello world',
-              __typename: 'Greeting',
-              recipient: { name: 'Alice', __typename: 'Person' },
-            },
-            { message: 'Hello again', __typename: 'Greeting' },
-          ],
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greetings: [
+              {
+                message: "Hello world",
+                __typename: "Greeting",
+                recipient: { name: "Alice", __typename: "Person" },
+              },
+              { message: "Hello again", __typename: "Greeting" },
+            ],
+          });
+        },
+        { interval: 1 }
+      );
 
       setTimeout(() => {
         link.simulateResult({
           result: {
-            incremental: [{
-              data: {
-                recipient: {
-                  name: 'Bob',
-                  __typename: 'Person',
+            incremental: [
+              {
+                data: {
+                  recipient: {
+                    name: "Bob",
+                    __typename: "Person",
+                  },
+                  __typename: "Greeting",
                 },
-                __typename: 'Greeting',
+                path: ["greetings", 1],
               },
-              path: ['greetings', 1],
-            }],
-            hasNext: false
+            ],
+            hasNext: false,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greetings: [
-            {
-              message: 'Hello world',
-              __typename: 'Greeting',
-              recipient: { name: 'Alice', __typename: 'Person' },
-            },
-            {
-              message: 'Hello again',
-              __typename: 'Greeting',
-              recipient: { name: 'Bob', __typename: 'Person' },
-            },
-          ],
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greetings: [
+              {
+                message: "Hello world",
+                __typename: "Greeting",
+                recipient: { name: "Alice", __typename: "Person" },
+              },
+              {
+                message: "Hello again",
+                __typename: "Greeting",
+                recipient: { name: "Bob", __typename: "Person" },
+              },
+            ],
+          });
+        },
+        { interval: 1 }
+      );
     });
 
-    it('should handle deferred queries in lists, merging arrays', async () => {
+    it("should handle deferred queries in lists, merging arrays", async () => {
       const query = gql`
         query DeferVariation {
           allProducts {
             delivery {
               ...MyFragment @defer
             }
-            sku,
+            sku
             id
           }
         }
@@ -6277,16 +7339,11 @@ describe('useQuery Hook', () => {
         cache: new InMemoryCache(),
       });
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
@@ -6298,51 +7355,57 @@ describe('useQuery Hook', () => {
                 {
                   __typename: "Product",
                   delivery: {
-                    __typename: "DeliveryEstimates"
+                    __typename: "DeliveryEstimates",
                   },
                   id: "apollo-federation",
-                  sku: "federation"
+                  sku: "federation",
                 },
                 {
                   __typename: "Product",
                   delivery: {
-                    __typename: "DeliveryEstimates"
+                    __typename: "DeliveryEstimates",
                   },
                   id: "apollo-studio",
-                  sku: "studio"
-                }
-              ]
+                  sku: "studio",
+                },
+              ],
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          allProducts: [
-            {
-              __typename: "Product",
-              delivery: {
-                __typename: "DeliveryEstimates"
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            allProducts: [
+              {
+                __typename: "Product",
+                delivery: {
+                  __typename: "DeliveryEstimates",
+                },
+                id: "apollo-federation",
+                sku: "federation",
               },
-              id: "apollo-federation",
-              sku: "federation"
-            },
-            {
-              __typename: "Product",
-              delivery: {
-                __typename: "DeliveryEstimates"
+              {
+                __typename: "Product",
+                delivery: {
+                  __typename: "DeliveryEstimates",
+                },
+                id: "apollo-studio",
+                sku: "studio",
               },
-              id: "apollo-studio",
-              sku: "studio"
-            }
-          ]
-        });
-      }, { interval: 1 });
+            ],
+          });
+        },
+        { interval: 1 }
+      );
 
       setTimeout(() => {
         link.simulateResult({
@@ -6355,11 +7418,7 @@ describe('useQuery Hook', () => {
                   estimatedDelivery: "6/25/2021",
                   fastestDelivery: "6/24/2021",
                 },
-                path: [
-                  "allProducts",
-                  0,
-                  "delivery"
-                ]
+                path: ["allProducts", 0, "delivery"],
               },
               {
                 data: {
@@ -6367,49 +7426,51 @@ describe('useQuery Hook', () => {
                   estimatedDelivery: "6/25/2021",
                   fastestDelivery: "6/24/2021",
                 },
-                path: [
-                  "allProducts",
-                  1,
-                  "delivery"
-                ]
+                path: ["allProducts", 1, "delivery"],
               },
-            ]
+            ],
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          allProducts: [
-            {
-              __typename: "Product",
-              delivery: {
-                __typename: "DeliveryEstimates",
-                estimatedDelivery: "6/25/2021",
-                fastestDelivery: "6/24/2021"
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            allProducts: [
+              {
+                __typename: "Product",
+                delivery: {
+                  __typename: "DeliveryEstimates",
+                  estimatedDelivery: "6/25/2021",
+                  fastestDelivery: "6/24/2021",
+                },
+                id: "apollo-federation",
+                sku: "federation",
               },
-              id: "apollo-federation",
-              sku: "federation"
-            },
-            {
-              __typename: "Product",
-              delivery: {
-                __typename: "DeliveryEstimates",
-                estimatedDelivery: "6/25/2021",
-                fastestDelivery: "6/24/2021"
+              {
+                __typename: "Product",
+                delivery: {
+                  __typename: "DeliveryEstimates",
+                  estimatedDelivery: "6/25/2021",
+                  fastestDelivery: "6/24/2021",
+                },
+                id: "apollo-studio",
+                sku: "studio",
               },
-              id: "apollo-studio",
-              sku: "studio"
-            }
-          ]
-        });
-      }, { interval: 1 });
+            ],
+          });
+        },
+        { interval: 1 }
+      );
     });
 
-    it('should handle deferred queries with fetch policy no-cache', async () => {
+    it("should handle deferred queries with fetch policy no-cache", async () => {
       const query = gql`
         {
           greeting {
@@ -6431,14 +7492,12 @@ describe('useQuery Hook', () => {
       });
 
       const { result } = renderHook(
-        () => useQuery(query, {fetchPolicy: 'no-cache'}),
+        () => useQuery(query, { fetchPolicy: "no-cache" }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
@@ -6448,63 +7507,77 @@ describe('useQuery Hook', () => {
           result: {
             data: {
               greeting: {
-                message: 'Hello world',
-                __typename: 'Greeting',
+                message: "Hello world",
+                __typename: "Greeting",
               },
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greeting: {
-            message: 'Hello world',
-            __typename: 'Greeting',
-          },
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greeting: {
+              message: "Hello world",
+              __typename: "Greeting",
+            },
+          });
+        },
+        { interval: 1 }
+      );
 
       setTimeout(() => {
         link.simulateResult({
           result: {
-            incremental: [{
-              data: {
-                recipient: {
-                  name: 'Alice',
-                  __typename: 'Person',
+            incremental: [
+              {
+                data: {
+                  recipient: {
+                    name: "Alice",
+                    __typename: "Person",
+                  },
+                  __typename: "Greeting",
                 },
-                __typename: 'Greeting',
+                path: ["greeting"],
               },
-              path: ['greeting'],
-            }],
-            hasNext: false
+            ],
+            hasNext: false,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greeting: {
-            message: 'Hello world',
-            __typename: 'Greeting',
-            recipient: {
-              name: 'Alice',
-              __typename: 'Person',
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greeting: {
+              message: "Hello world",
+              __typename: "Greeting",
+              recipient: {
+                name: "Alice",
+                __typename: "Person",
+              },
             },
-          },
-        });
-      }, { interval: 1 });
+          });
+        },
+        { interval: 1 }
+      );
     });
 
-    it('should handle deferred queries with errors returned on the incremental batched result', async () => {
+    it("should handle deferred queries with errors returned on the incremental batched result", async () => {
       const query = gql`
         query {
           hero {
@@ -6527,16 +7600,11 @@ describe('useQuery Hook', () => {
         cache: new InMemoryCache(),
       });
 
-      const { result } = renderHook(
-        () => useQuery(query),
-        {
-          wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
-          ),
-        },
-      );
+      const { result } = renderHook(() => useQuery(query), {
+        wrapper: ({ children }) => (
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      });
 
       expect(result.current.loading).toBe(true);
       expect(result.current.data).toBe(undefined);
@@ -6549,40 +7617,46 @@ describe('useQuery Hook', () => {
                 heroFriends: [
                   {
                     id: "1000",
-                    name: "Luke Skywalker"
+                    name: "Luke Skywalker",
                   },
                   {
                     id: "1003",
-                    name: "Leia Organa"
-                  }
-                ]
-              }
+                    name: "Leia Organa",
+                  },
+                ],
+              },
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          hero: {
-            heroFriends: [
-              {
-                id: '1000',
-                name: 'Luke Skywalker'
-              },
-              {
-                id: '1003',
-                name: 'Leia Organa'
-              },
-            ],
-            name: "R2-D2"
-          }
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            hero: {
+              heroFriends: [
+                {
+                  id: "1000",
+                  name: "Luke Skywalker",
+                },
+                {
+                  id: "1003",
+                  name: "Leia Organa",
+                },
+              ],
+              name: "R2-D2",
+            },
+          });
+        },
+        { interval: 1 }
+      );
 
       setTimeout(() => {
         link.simulateResult({
@@ -6594,51 +7668,65 @@ describe('useQuery Hook', () => {
                   new GraphQLError(
                     "homeWorld for character with ID 1000 could not be fetched.",
                     { path: ["hero", "heroFriends", 0, "homeWorld"] }
-                  )
+                  ),
                 ],
                 data: {
-                  "homeWorld": null,
-                }
+                  homeWorld: null,
+                },
               },
               {
                 path: ["hero", "heroFriends", 1],
                 data: {
-                  "homeWorld": "Alderaan",
-                }
+                  homeWorld: "Alderaan",
+                },
               },
             ],
-            "hasNext": false
-          }
+            hasNext: false,
+          },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.error).toBeInstanceOf(ApolloError);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.error!.message).toBe('homeWorld for character with ID 1000 could not be fetched.');
-      }, { interval: 1 });
-      await waitFor(() => {
-        // since default error policy is "none", we do *not* return partial results
-        expect(result.current.data).toEqual({
-          hero: {
-            heroFriends: [
-              {
-                id: '1000',
-                name: 'Luke Skywalker'
-              },
-              {
-                id: '1003',
-                name: 'Leia Organa'
-              },
-            ],
-            name: "R2-D2"
-          }
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.error).toBeInstanceOf(ApolloError);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.error!.message).toBe(
+            "homeWorld for character with ID 1000 could not be fetched."
+          );
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          // since default error policy is "none", we do *not* return partial results
+          expect(result.current.data).toEqual({
+            hero: {
+              heroFriends: [
+                {
+                  id: "1000",
+                  name: "Luke Skywalker",
+                },
+                {
+                  id: "1003",
+                  name: "Leia Organa",
+                },
+              ],
+              name: "R2-D2",
+            },
+          });
+        },
+        { interval: 1 }
+      );
     });
 
     it('should handle deferred queries with errors returned on the incremental batched result and errorPolicy "all"', async () => {
@@ -6668,11 +7756,9 @@ describe('useQuery Hook', () => {
         () => useQuery(query, { errorPolicy: "all" }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
 
       expect(result.current.loading).toBe(true);
@@ -6686,40 +7772,46 @@ describe('useQuery Hook', () => {
                 heroFriends: [
                   {
                     id: "1000",
-                    name: "Luke Skywalker"
+                    name: "Luke Skywalker",
                   },
                   {
                     id: "1003",
-                    name: "Leia Organa"
-                  }
-                ]
-              }
+                    name: "Leia Organa",
+                  },
+                ],
+              },
             },
-            hasNext: true
+            hasNext: true,
           },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          hero: {
-            heroFriends: [
-              {
-                id: '1000',
-                name: 'Luke Skywalker'
-              },
-              {
-                id: '1003',
-                name: 'Leia Organa'
-              },
-            ],
-            name: "R2-D2"
-          }
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            hero: {
+              heroFriends: [
+                {
+                  id: "1000",
+                  name: "Luke Skywalker",
+                },
+                {
+                  id: "1003",
+                  name: "Leia Organa",
+                },
+              ],
+              name: "R2-D2",
+            },
+          });
+        },
+        { interval: 1 }
+      );
 
       setTimeout(() => {
         link.simulateResult({
@@ -6731,73 +7823,93 @@ describe('useQuery Hook', () => {
                   new GraphQLError(
                     "homeWorld for character with ID 1000 could not be fetched.",
                     { path: ["hero", "heroFriends", 0, "homeWorld"] }
-                  )
+                  ),
                 ],
                 data: {
-                  "homeWorld": null,
+                  homeWorld: null,
                 },
                 extensions: {
-                  thing1: 'foo',
-                  thing2: 'bar',
+                  thing1: "foo",
+                  thing2: "bar",
                 },
               },
               {
                 path: ["hero", "heroFriends", 1],
                 data: {
-                  "homeWorld": "Alderaan",
+                  homeWorld: "Alderaan",
                 },
                 extensions: {
-                  thing1: 'foo',
-                  thing2: 'bar',
+                  thing1: "foo",
+                  thing2: "bar",
                 },
               },
             ],
-            "hasNext": false
-          }
+            hasNext: false,
+          },
         });
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        // @ts-ignore
-        expect(result.current.label).toBe(undefined);
-      }, { interval: 1 });
-      await waitFor(() => {
-        // @ts-ignore
-        expect(result.current.extensions).toBe(undefined);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.error).toBeInstanceOf(ApolloError);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.error!.message).toBe('homeWorld for character with ID 1000 could not be fetched.');
-      }, { interval: 1 });
-      await waitFor(() => {
-        // since default error policy is "all", we *do* return partial results
-        expect(result.current.data).toEqual({
-          hero: {
-            heroFriends: [
-              {
-                // the only difference with the previous test
-                // is that homeWorld is populated since errorPolicy: all
-                // populates both partial data and error.graphQLErrors
-                homeWorld: null,
-                id: '1000',
-                name: 'Luke Skywalker'
-              },
-              {
-                // homeWorld is populated due to errorPolicy: all
-                homeWorld: "Alderaan",
-                id: '1003',
-                name: 'Leia Organa'
-              },
-            ],
-            name: "R2-D2"
-          }
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          // @ts-ignore
+          expect(result.current.label).toBe(undefined);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          // @ts-ignore
+          expect(result.current.extensions).toBe(undefined);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.error).toBeInstanceOf(ApolloError);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.error!.message).toBe(
+            "homeWorld for character with ID 1000 could not be fetched."
+          );
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          // since default error policy is "all", we *do* return partial results
+          expect(result.current.data).toEqual({
+            hero: {
+              heroFriends: [
+                {
+                  // the only difference with the previous test
+                  // is that homeWorld is populated since errorPolicy: all
+                  // populates both partial data and error.graphQLErrors
+                  homeWorld: null,
+                  id: "1000",
+                  name: "Luke Skywalker",
+                },
+                {
+                  // homeWorld is populated due to errorPolicy: all
+                  homeWorld: "Alderaan",
+                  id: "1003",
+                  name: "Leia Organa",
+                },
+              ],
+              name: "R2-D2",
+            },
+          });
+        },
+        { interval: 1 }
+      );
     });
 
     it('returns eventually consistent data from deferred queries with data in the cache while using a "cache-and-network" fetch policy', async () => {
@@ -6822,20 +7934,18 @@ describe('useQuery Hook', () => {
         query,
         data: {
           greeting: {
-            __typename: 'Greeting',
-            message: 'Hello cached',
-            recipient: { __typename: 'Person', name: 'Cached Alice' },
+            __typename: "Greeting",
+            message: "Hello cached",
+            recipient: { __typename: "Person", name: "Cached Alice" },
           },
         },
       });
 
       const { result } = renderHook(
-        () => useQuery(query, { fetchPolicy: 'cache-and-network' }),
+        () => useQuery(query, { fetchPolicy: "cache-and-network" }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
         }
       );
@@ -6844,65 +7954,85 @@ describe('useQuery Hook', () => {
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.data).toEqual({
         greeting: {
-          message: 'Hello cached',
-          __typename: 'Greeting',
-          recipient: { __typename: 'Person', name: 'Cached Alice' },
+          message: "Hello cached",
+          __typename: "Greeting",
+          recipient: { __typename: "Person", name: "Cached Alice" },
         },
       });
 
       link.simulateResult({
         result: {
-          data: { greeting: { __typename: 'Greeting', message: 'Hello world' } },
+          data: {
+            greeting: { __typename: "Greeting", message: "Hello world" },
+          },
           hasNext: true,
         },
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greeting: {
-            __typename: 'Greeting',
-            message: 'Hello world',
-            recipient: { __typename: 'Person', name: 'Cached Alice' },
-          },
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Cached Alice" },
+            },
+          });
+        },
+        { interval: 1 }
+      );
 
       link.simulateResult({
         result: {
           incremental: [
             {
               data: {
-                recipient: { name: 'Alice', __typename: 'Person' },
-                __typename: 'Greeting',
+                recipient: { name: "Alice", __typename: "Person" },
+                __typename: "Greeting",
               },
-              path: ['greeting'],
+              path: ["greeting"],
             },
           ],
           hasNext: false,
         },
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greeting: {
-            __typename: 'Greeting',
-            message: 'Hello world',
-            recipient: { __typename: 'Person', name: 'Alice' },
-          },
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+          });
+        },
+        { interval: 1 }
+      );
     });
 
     it('returns eventually consistent data from deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => {
@@ -6925,13 +8055,13 @@ describe('useQuery Hook', () => {
 
       // We know we are writing partial data to the cache so suppress the console
       // warning.
-      const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
+      const consoleSpy = jest.spyOn(console, "error").mockImplementation();
       cache.writeQuery({
         query,
         data: {
           greeting: {
-            __typename: 'Greeting',
-            recipient: { __typename: 'Person', name: 'Cached Alice' },
+            __typename: "Greeting",
+            recipient: { __typename: "Person", name: "Cached Alice" },
           },
         },
       });
@@ -6940,14 +8070,12 @@ describe('useQuery Hook', () => {
       const { result } = renderHook(
         () =>
           useQuery(query, {
-            fetchPolicy: 'cache-first',
-            returnPartialData: true
+            fetchPolicy: "cache-first",
+            returnPartialData: true,
           }),
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
         }
       );
@@ -6956,64 +8084,84 @@ describe('useQuery Hook', () => {
       expect(result.current.networkStatus).toBe(NetworkStatus.loading);
       expect(result.current.data).toEqual({
         greeting: {
-          __typename: 'Greeting',
-          recipient: { __typename: 'Person', name: 'Cached Alice' },
+          __typename: "Greeting",
+          recipient: { __typename: "Person", name: "Cached Alice" },
         },
       });
 
       link.simulateResult({
         result: {
-          data: { greeting: { message: 'Hello world', __typename: 'Greeting' } },
+          data: {
+            greeting: { message: "Hello world", __typename: "Greeting" },
+          },
           hasNext: true,
         },
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greeting: {
-            __typename: 'Greeting',
-            message: 'Hello world',
-            recipient: { __typename: 'Person', name: 'Cached Alice' },
-          },
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Cached Alice" },
+            },
+          });
+        },
+        { interval: 1 }
+      );
 
       link.simulateResult({
         result: {
           incremental: [
             {
               data: {
-                __typename: 'Greeting',
-                recipient: { name: 'Alice', __typename: 'Person' },
+                __typename: "Greeting",
+                recipient: { name: "Alice", __typename: "Person" },
               },
-              path: ['greeting'],
+              path: ["greeting"],
             },
           ],
           hasNext: false,
         },
       });
 
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.networkStatus).toBe(NetworkStatus.ready);
-      }, { interval: 1 });
-      await waitFor(() => {
-        expect(result.current.data).toEqual({
-          greeting: {
-            __typename: 'Greeting',
-            message: 'Hello world',
-            recipient: { __typename: 'Person', name: 'Alice' },
-          },
-        });
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.loading).toBe(false);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.networkStatus).toBe(NetworkStatus.ready);
+        },
+        { interval: 1 }
+      );
+      await waitFor(
+        () => {
+          expect(result.current.data).toEqual({
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+          });
+        },
+        { interval: 1 }
+      );
     });
   });
 
@@ -7027,7 +8175,7 @@ describe('useQuery Hook', () => {
         fetchPolicy: WatchQueryFetchPolicy,
         initialQueryValue: TestQueryValue | undefined,
         shouldFetchOnFirstRender: boolean,
-        shouldFetchOnSecondRender: boolean
+        shouldFetchOnSecondRender: boolean,
       ]
     >([
       [`cache-first`, emptyData, true, false],
@@ -7044,7 +8192,12 @@ describe('useQuery Hook', () => {
       [`standby`, cacheData, false, false],
     ])(
       "fetchPolicy %s, cache: %p should fetch during `disableNetworkFetches`: %p and after `disableNetworkFetches` has been disabled: %p",
-      async (policy, initialQueryValue, shouldFetchOnFirstRender, shouldFetchOnSecondRender) => {
+      async (
+        policy,
+        initialQueryValue,
+        shouldFetchOnFirstRender,
+        shouldFetchOnSecondRender
+      ) => {
         const query: TypedDocumentNode<TestQueryValue> = gql`
           query CallMe {
             something
@@ -7052,10 +8205,10 @@ describe('useQuery Hook', () => {
         `;
 
         const link = new MockLink([
-          {request: {query}, result: {data: { something: "bar" }}},
-          {request: {query}, result: {data: { something: "baz" }}},
+          { request: { query }, result: { data: { something: "bar" } } },
+          { request: { query }, result: { data: { something: "baz" } } },
         ]);
-        const requestSpy = jest.spyOn(link, 'request');
+        const requestSpy = jest.spyOn(link, "request");
 
         const client = new ApolloClient({
           cache: new InMemoryCache(),
@@ -7067,13 +8220,18 @@ describe('useQuery Hook', () => {
         client.disableNetworkFetches = true;
 
         const { rerender } = renderHook(
-          () => useQuery(query, { fetchPolicy: policy, nextFetchPolicy: policy }),
+          () =>
+            useQuery(query, { fetchPolicy: policy, nextFetchPolicy: policy }),
           {
-            wrapper: ({ children }) => <ApolloProvider client={client}>{children}</ApolloProvider>,
+            wrapper: ({ children }) => (
+              <ApolloProvider client={client}>{children}</ApolloProvider>
+            ),
           }
         );
 
-        expect(requestSpy).toHaveBeenCalledTimes(shouldFetchOnFirstRender ? 1 : 0);
+        expect(requestSpy).toHaveBeenCalledTimes(
+          shouldFetchOnFirstRender ? 1 : 0
+        );
 
         // We need to wait a moment before the rerender for everything to settle down.
         // This part is unfortunately bound to be flaky - but in some cases there is
@@ -7084,8 +8242,26 @@ describe('useQuery Hook', () => {
         client.disableNetworkFetches = false;
 
         rerender();
-        expect(requestSpy).toHaveBeenCalledTimes(shouldFetchOnSecondRender ? 1 : 0);
+        expect(requestSpy).toHaveBeenCalledTimes(
+          shouldFetchOnSecondRender ? 1 : 0
+        );
       }
     );
   });
 });
+
+describe.skip("Type Tests", () => {
+  test("NoInfer prevents adding arbitrary additional variables", () => {
+    const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;
+    const { variables } = useQuery(typedNode, {
+      variables: {
+        bar: 4,
+        // @ts-expect-error
+        nonExistingVariable: "string",
+      },
+    });
+    variables?.bar;
+    // @ts-expect-error
+    variables?.nonExistingVariable;
+  });
+});
diff --git a/src/react/hooks/__tests__/useReactiveVar.test.tsx b/src/react/hooks/__tests__/useReactiveVar.test.tsx
--- a/src/react/hooks/__tests__/useReactiveVar.test.tsx
+++ b/src/react/hooks/__tests__/useReactiveVar.test.tsx
@@ -5,7 +5,7 @@ import { itAsync } from "../../../testing";
 import { makeVar } from "../../../core";
 import { useReactiveVar } from "../useReactiveVar";
 
-const IS_REACT_18 = React.version.startsWith('18');
+const IS_REACT_18 = React.version.startsWith("18");
 
 describe("useReactiveVar Hook", () => {
   it("works with one component", async () => {
@@ -36,7 +36,7 @@ describe("useReactiveVar Hook", () => {
       return null;
     }
 
-    render(<Component/>);
+    render(<Component />);
 
     await waitFor(() => {
       expect(renderCount).toBe(3);
@@ -46,89 +46,92 @@ describe("useReactiveVar Hook", () => {
     });
   });
 
-  itAsync("works when two components share a variable", async (resolve, reject) => {
-    const counterVar = makeVar(0);
+  itAsync(
+    "works when two components share a variable",
+    async (resolve, reject) => {
+      const counterVar = makeVar(0);
 
-    let parentRenderCount = 0;
-    function Parent() {
-      const count = useReactiveVar(counterVar);
+      let parentRenderCount = 0;
+      function Parent() {
+        const count = useReactiveVar(counterVar);
 
-      switch (++parentRenderCount) {
-        case 1:
-          expect(count).toBe(0);
-          break;
-        case 2:
-          expect(count).toBe(1);
-          break;
-        case 3:
-          expect(count).toBe(11);
-          break;
-        default:
-          reject(`too many (${parentRenderCount}) parent renders`);
+        switch (++parentRenderCount) {
+          case 1:
+            expect(count).toBe(0);
+            break;
+          case 2:
+            expect(count).toBe(1);
+            break;
+          case 3:
+            expect(count).toBe(11);
+            break;
+          default:
+            reject(`too many (${parentRenderCount}) parent renders`);
+        }
+
+        return <Child />;
       }
 
-      return <Child/>;
-    }
+      let childRenderCount = 0;
+      function Child() {
+        const count = useReactiveVar(counterVar);
 
-    let childRenderCount = 0;
-    function Child() {
-      const count = useReactiveVar(counterVar);
+        switch (++childRenderCount) {
+          case 1:
+            expect(count).toBe(0);
+            break;
+          case 2:
+            expect(count).toBe(1);
+            break;
+          case 3:
+            expect(count).toBe(11);
+            break;
+          default:
+            reject(`too many (${childRenderCount}) child renders`);
+        }
 
-      switch (++childRenderCount) {
-        case 1:
-          expect(count).toBe(0);
-          break;
-        case 2:
-          expect(count).toBe(1);
-          break;
-        case 3:
-          expect(count).toBe(11);
-          break;
-        default:
-          reject(`too many (${childRenderCount}) child renders`);
+        return null;
       }
 
-      return null;
-    }
-
-    render(<Parent/>);
+      render(<Parent />);
 
-    await waitFor(() => {
-      expect(parentRenderCount).toBe(1);
-    });
+      await waitFor(() => {
+        expect(parentRenderCount).toBe(1);
+      });
 
-    await waitFor(() => {
-      expect(childRenderCount).toBe(1);
-    });
+      await waitFor(() => {
+        expect(childRenderCount).toBe(1);
+      });
 
-    expect(counterVar()).toBe(0);
-    act(() => {
-      counterVar(1);
-    });
+      expect(counterVar()).toBe(0);
+      act(() => {
+        counterVar(1);
+      });
 
-    await waitFor(() => {
-      expect(parentRenderCount).toBe(2);
-    });
-    await waitFor(() => {
-      expect(childRenderCount).toBe(2);
-    });
+      await waitFor(() => {
+        expect(parentRenderCount).toBe(2);
+      });
+      await waitFor(() => {
+        expect(childRenderCount).toBe(2);
+      });
 
-    expect(counterVar()).toBe(1);
-    act(() => {
-      counterVar(counterVar() + 10);
-    });
+      expect(counterVar()).toBe(1);
+      act(() => {
+        counterVar(counterVar() + 10);
+      });
 
-    await waitFor(() => {
-      expect(parentRenderCount).toBe(3);
-    });
-    await waitFor(() => {
-      expect(childRenderCount).toBe(3);
-    });
+      await waitFor(() => {
+        expect(parentRenderCount).toBe(3);
+      });
+      await waitFor(() => {
+        expect(childRenderCount).toBe(3);
+      });
 
-    expect(counterVar()).toBe(11);
+      expect(counterVar()).toBe(11);
 
-    resolve();
-  });
+      resolve();
+    }
+  );
 
   it("does not update if component has been unmounted", async () => {
     const counterVar = makeVar(0);
@@ -168,20 +171,20 @@ describe("useReactiveVar Hook", () => {
       return error.apply(this, args);
     };
 
-    const { unmount } = render(<Component/>);
+    const { unmount } = render(<Component />);
 
     await waitFor(() => {
       expect(attemptedUpdateAfterUnmount).toBe(true);
     });
     await waitFor(() => {
       expect(renderCount).toBe(3);
-    })
+    });
     await waitFor(() => {
       expect(counterVar()).toBe(6);
-    })
+    });
     await waitFor(() => {
       expect(consoleErrorArgs).toEqual([]);
-    })
+    });
     console.error = error;
   });
 
@@ -196,13 +199,13 @@ describe("useReactiveVar Hook", () => {
           counterVar(1);
         }, []);
 
-        return (<div>{count}</div>);
+        return <div>{count}</div>;
       }
 
       function ComponentTwo() {
         const count = useReactiveVar(counterVar);
 
-        return (<div>{count}</div>);
+        return <div>{count}</div>;
       }
 
       render(
@@ -223,7 +226,7 @@ describe("useReactiveVar Hook", () => {
       function ComponentOne() {
         const count = useReactiveVar(counterVar);
 
-        return (<div>{count}</div>);
+        return <div>{count}</div>;
       }
 
       function ComponentTwo() {
@@ -233,7 +236,7 @@ describe("useReactiveVar Hook", () => {
           counterVar(1);
         }, []);
 
-        return (<div>{count}</div>);
+        return <div>{count}</div>;
       }
 
       render(
@@ -264,9 +267,7 @@ describe("useReactiveVar Hook", () => {
           });
         }, []);
 
-        return (
-          <div />
-        );
+        return <div />;
       }
 
       render(
@@ -291,33 +292,36 @@ describe("useReactiveVar Hook", () => {
       resolve();
     });
 
-    itAsync("works with multiple synchronous calls", async (resolve, reject) => {
-      const counterVar = makeVar(0);
-      function Component() {
-        const count = useReactiveVar(counterVar);
-
-        return (<div>{count}</div>);
-      }
-
-      render(<Component />);
-      Promise.resolve().then(() => {
-        counterVar(1);
-        counterVar(2);
-        counterVar(3);
-        counterVar(4);
-        counterVar(5);
-        counterVar(6);
-        counterVar(7);
-        counterVar(8);
-        counterVar(9);
-        counterVar(10);
-      });
+    itAsync(
+      "works with multiple synchronous calls",
+      async (resolve, reject) => {
+        const counterVar = makeVar(0);
+        function Component() {
+          const count = useReactiveVar(counterVar);
 
-      await waitFor(() => {
-        expect(screen.getAllByText("10")).toHaveLength(1);
-      });
+          return <div>{count}</div>;
+        }
 
-      resolve();
-    });
+        render(<Component />);
+        Promise.resolve().then(() => {
+          counterVar(1);
+          counterVar(2);
+          counterVar(3);
+          counterVar(4);
+          counterVar(5);
+          counterVar(6);
+          counterVar(7);
+          counterVar(8);
+          counterVar(9);
+          counterVar(10);
+        });
+
+        await waitFor(() => {
+          expect(screen.getAllByText("10")).toHaveLength(1);
+        });
+
+        resolve();
+      }
+    );
   });
 });
diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx
--- a/src/react/hooks/__tests__/useSubscription.test.tsx
+++ b/src/react/hooks/__tests__/useSubscription.test.tsx
@@ -1,20 +1,22 @@
-import React from 'react';
-import { renderHook, waitFor } from '@testing-library/react';
-import gql from 'graphql-tag';
-
-import { ApolloClient, ApolloError, ApolloLink, concat } from '../../../core';
-import { PROTOCOL_ERRORS_SYMBOL } from '../../../errors';
-import { InMemoryCache as Cache } from '../../../cache';
-import { ApolloProvider, resetApolloContext } from '../../context';
-import { MockSubscriptionLink } from '../../../testing';
-import { useSubscription } from '../useSubscription';
-
-describe('useSubscription Hook', () => {
-  afterEach(() => {
-    resetApolloContext();
-  });
-
-  it('should handle a simple subscription properly', async () => {
+import React from "react";
+import { renderHook, waitFor } from "@testing-library/react";
+import gql from "graphql-tag";
+
+import {
+  ApolloClient,
+  ApolloError,
+  ApolloLink,
+  concat,
+  TypedDocumentNode,
+} from "../../../core";
+import { PROTOCOL_ERRORS_SYMBOL } from "../../../errors";
+import { InMemoryCache as Cache } from "../../../cache";
+import { ApolloProvider } from "../../context";
+import { MockSubscriptionLink } from "../../../testing";
+import { useSubscription } from "../useSubscription";
+
+describe("useSubscription Hook", () => {
+  it("should handle a simple subscription properly", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -23,54 +25,60 @@ describe('useSubscription Hook', () => {
       }
     `;
 
-    const results = ['Audi', 'BMW', 'Mercedes', 'Hyundai'].map(make => ({
-      result: { data: { car: { make } } }
+    const results = ["Audi", "BMW", "Mercedes", "Hyundai"].map((make) => ({
+      result: { data: { car: { make } } },
     }));
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
-
-    const { result } = renderHook(
-      () => useSubscription(subscription),
-      {
-        wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useSubscription(subscription), {
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
+    });
 
     expect(result.current.loading).toBe(true);
     expect(result.current.error).toBe(undefined);
     expect(result.current.data).toBe(undefined);
     setTimeout(() => link.simulateResult(results[0]));
-    await waitFor(() => {
-      expect(result.current.data).toEqual(results[0].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.data).toEqual(results[0].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.loading).toBe(false);
     setTimeout(() => link.simulateResult(results[1]));
-    await waitFor(() => {
-      expect(result.current.data).toEqual(results[1].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.data).toEqual(results[1].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.loading).toBe(false);
     setTimeout(() => link.simulateResult(results[2]));
-    await waitFor(() => {
-      expect(result.current.data).toEqual(results[2].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.data).toEqual(results[2].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.loading).toBe(false);
     setTimeout(() => link.simulateResult(results[3]));
-    await waitFor(() => {
-      expect(result.current.data).toEqual(results[3].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.data).toEqual(results[3].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.loading).toBe(false);
   });
 
-  it('should call onError after error results', async () => {
+  it("should call onError after error results", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -79,8 +87,8 @@ describe('useSubscription Hook', () => {
       }
     `;
 
-    const results = ['Audi', 'BMW', 'Mercedes', 'Hyundai'].map(make => ({
-      result: { data: { car: { make } } }
+    const results = ["Audi", "BMW", "Mercedes", "Hyundai"].map((make) => ({
+      result: { data: { car: { make } } },
     }));
 
     const errorResult = {
@@ -91,38 +99,41 @@ describe('useSubscription Hook', () => {
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
-
     const onError = jest.fn();
     const { result } = renderHook(
       () => useSubscription(subscription, { onError }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(result.current.loading).toBe(true);
     expect(result.current.error).toBe(undefined);
     expect(result.current.data).toBe(undefined);
     setTimeout(() => link.simulateResult(results[0]));
-    await waitFor(() => {
-      expect(result.current.loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current.loading).toBe(false);
     expect(result.current.data).toEqual(results[0].result.data);
     setTimeout(() => link.simulateResult(errorResult));
-    await waitFor(() => {
-      expect(onError).toHaveBeenCalledTimes(1);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(onError).toHaveBeenCalledTimes(1);
+      },
+      { interval: 1 }
+    );
   });
 
-  it('should call onComplete after subscription is complete', async () => {
+  it("should call onComplete after subscription is complete", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -131,37 +142,37 @@ describe('useSubscription Hook', () => {
       }
     `;
 
-    const results = [{
-      result: { data: { car: { make: 'Audi' } } }
-    }];
+    const results = [
+      {
+        result: { data: { car: { make: "Audi" } } },
+      },
+    ];
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const onComplete = jest.fn();
-    renderHook(
-      () => useSubscription(subscription, { onComplete }),
-      {
-        wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        ),
-      },
-    );
+    renderHook(() => useSubscription(subscription, { onComplete }), {
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
+    });
 
     link.simulateResult(results[0]);
 
     setTimeout(() => link.simulateComplete());
-    await waitFor(() => {
-      expect(onComplete).toHaveBeenCalledTimes(1);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(onComplete).toHaveBeenCalledTimes(1);
+      },
+      { interval: 1 }
+    );
   });
 
-  it('should cleanup after the subscription component has been unmounted', async () => {
+  it("should cleanup after the subscription component has been unmounted", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -172,37 +183,39 @@ describe('useSubscription Hook', () => {
 
     const results = [
       {
-        result: { data: { car: { make: 'Pagani' } } }
-      }
+        result: { data: { car: { make: "Pagani" } } },
+      },
     ];
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const onData = jest.fn();
     const { result, unmount } = renderHook(
-      () => useSubscription(subscription, {
-        onData,
-      }),
+      () =>
+        useSubscription(subscription, {
+          onData,
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(result.current.loading).toBe(true);
     expect(result.current.error).toBe(undefined);
     expect(result.current.data).toBe(undefined);
     setTimeout(() => link.simulateResult(results[0]));
-    await waitFor(() => {
-      expect(result.current.loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current.error).toBe(undefined);
     expect(result.current.data).toBe(results[0].result.data);
     setTimeout(() => {
@@ -219,7 +232,7 @@ describe('useSubscription Hook', () => {
     expect(onData).toHaveBeenCalledTimes(1);
   });
 
-  it('should never execute a subscription with the skip option', async () => {
+  it("should never execute a subscription with the skip option", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -233,46 +246,50 @@ describe('useSubscription Hook', () => {
     link.onSetup(onSetup);
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const onData = jest.fn();
 
     const { result, unmount, rerender } = renderHook(
-      ({ variables }) => useSubscription(subscription, {
-        variables,
-        skip: true,
-        onData,
-      }),
+      ({ variables }) =>
+        useSubscription(subscription, {
+          variables,
+          skip: true,
+          onData,
+        }),
       {
         initialProps: {
           variables: {
-            foo: 'bar'
-          }
+            foo: "bar",
+          },
         },
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        )
-      },
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      }
     );
 
     expect(result.current.loading).toBe(false);
     expect(result.current.error).toBe(undefined);
     expect(result.current.data).toBe(undefined);
 
-    rerender({ variables: { foo: 'bar2' }});
-    await expect(waitFor(() => {
-      expect(result.current.data).not.toBe(undefined);
-    }, { interval: 1, timeout: 20 })).rejects.toThrow();
+    rerender({ variables: { foo: "bar2" } });
+    await expect(
+      waitFor(
+        () => {
+          expect(result.current.data).not.toBe(undefined);
+        },
+        { interval: 1, timeout: 20 }
+      )
+    ).rejects.toThrow();
 
     expect(onSetup).toHaveBeenCalledTimes(0);
     expect(onData).toHaveBeenCalledTimes(0);
     unmount();
   });
 
-  it('should create a subscription after skip has changed from true to a falsy value', async () => {
+  it("should create a subscription after skip has changed from true to a falsy value", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -283,28 +300,26 @@ describe('useSubscription Hook', () => {
 
     const results = [
       {
-        result: { data: { car: { make: 'Pagani' } } }
+        result: { data: { car: { make: "Pagani" } } },
       },
       {
-        result: { data: { car: { make: 'Scoop' } } }
-      }
+        result: { data: { car: { make: "Scoop" } } },
+      },
     ];
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
     const { result, rerender } = renderHook(
       ({ skip }) => useSubscription(subscription, { skip }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
         initialProps: { skip: true },
-      },
+      }
     );
 
     expect(result.current.loading).toBe(false);
@@ -320,9 +335,12 @@ describe('useSubscription Hook', () => {
       link.simulateResult(results[0]);
     });
 
-    await waitFor(() => {
-      expect(result.current.loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current.data).toEqual(results[0].result.data);
     expect(result.current.error).toBe(undefined);
 
@@ -338,9 +356,14 @@ describe('useSubscription Hook', () => {
     expect(result.current.data).toBe(undefined);
     expect(result.current.error).toBe(undefined);
 
-    await expect(waitFor(() => {
-      expect(result.current.data).not.toBe(undefined);
-    }, { interval: 1, timeout: 20 })).rejects.toThrow();
+    await expect(
+      waitFor(
+        () => {
+          expect(result.current.data).not.toBe(undefined);
+        },
+        { interval: 1, timeout: 20 }
+      )
+    ).rejects.toThrow();
 
     // ensure state persists across rerenders
     rerender({ skip: false });
@@ -352,14 +375,17 @@ describe('useSubscription Hook', () => {
       link.simulateResult(results[1]);
     });
 
-    await waitFor(() => {
-      expect(result.current.loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current.data).toEqual(results[1].result.data);
     expect(result.current.error).toBe(undefined);
   });
 
-  it('should share context set in options', async () => {
+  it("should share context set in options", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -368,32 +394,31 @@ describe('useSubscription Hook', () => {
       }
     `;
 
-    const results = ['Audi', 'BMW'].map(make => ({
-      result: { data: { car: { make } } }
+    const results = ["Audi", "BMW"].map((make) => ({
+      result: { data: { car: { make } } },
     }));
 
     let context: string;
     const link = new MockSubscriptionLink();
     const contextLink = new ApolloLink((operation, forward) => {
-      context = operation.getContext()?.make
+      context = operation.getContext()?.make;
       return forward(operation);
     });
     const client = new ApolloClient({
       link: concat(contextLink, link),
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const { result } = renderHook(
-      () => useSubscription(subscription, {
-        context: { make: 'Audi' },
-      }),
+      () =>
+        useSubscription(subscription, {
+          context: { make: "Audi" },
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(result.current.loading).toBe(true);
@@ -403,9 +428,12 @@ describe('useSubscription Hook', () => {
       link.simulateResult(results[0]);
     }, 100);
 
-    await waitFor(() => {
-      expect(result.current.data).toEqual(results[0].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.data).toEqual(results[0].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.loading).toBe(false);
     expect(result.current.error).toBe(undefined);
 
@@ -413,16 +441,19 @@ describe('useSubscription Hook', () => {
       link.simulateResult(results[1]);
     });
 
-    await waitFor(() => {
-      expect(result.current.data).toEqual(results[1].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.data).toEqual(results[1].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.loading).toBe(false);
     expect(result.current.error).toBe(undefined);
 
-    expect(context!).toBe('Audi');
+    expect(context!).toBe("Audi");
   });
 
-  it('should handle multiple subscriptions properly', async () => {
+  it("should handle multiple subscriptions properly", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -431,14 +462,14 @@ describe('useSubscription Hook', () => {
       }
     `;
 
-    const results = ['Audi', 'BMW'].map(make => ({
-      result: { data: { car: { make } } }
+    const results = ["Audi", "BMW"].map((make) => ({
+      result: { data: { car: { make } } },
     }));
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const { result } = renderHook(
@@ -448,11 +479,9 @@ describe('useSubscription Hook', () => {
       }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(result.current.sub1.loading).toBe(true);
@@ -466,9 +495,12 @@ describe('useSubscription Hook', () => {
       link.simulateResult(results[0]);
     });
 
-    await waitFor(() => {
-      expect(result.current.sub1.data).toEqual(results[0].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.sub1.data).toEqual(results[0].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.sub1.loading).toBe(false);
     expect(result.current.sub1.error).toBe(undefined);
     expect(result.current.sub2.loading).toBe(false);
@@ -479,9 +511,12 @@ describe('useSubscription Hook', () => {
       link.simulateResult(results[1]);
     });
 
-    await waitFor(() => {
-      expect(result.current.sub1.data).toEqual(results[1].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.sub1.data).toEqual(results[1].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.sub1.loading).toBe(false);
     expect(result.current.sub1.error).toBe(undefined);
     expect(result.current.sub2.loading).toBe(false);
@@ -489,8 +524,8 @@ describe('useSubscription Hook', () => {
     expect(result.current.sub2.data).toEqual(results[1].result.data);
   });
 
-  it('should handle immediate completions gracefully', async () => {
-    const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+  it("should handle immediate completions gracefully", async () => {
+    const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
 
     const subscription = gql`
       subscription {
@@ -503,19 +538,14 @@ describe('useSubscription Hook', () => {
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
-    const { result } = renderHook(
-      () => useSubscription(subscription),
-      {
-        wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        ),
-      },
-    );
+    const { result } = renderHook(() => useSubscription(subscription), {
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>{children}</ApolloProvider>
+      ),
+    });
 
     setTimeout(() => {
       // Simulating the behavior of HttpLink, which calls next and complete in sequence.
@@ -525,21 +555,26 @@ describe('useSubscription Hook', () => {
     expect(result.current.loading).toBe(true);
     expect(result.current.error).toBe(undefined);
     expect(result.current.data).toBe(undefined);
-    await waitFor(() => {
-      expect(result.current.loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
     expect(result.current.error).toBe(undefined);
     expect(result.current.data).toBe(null);
 
     expect(errorSpy).toHaveBeenCalledTimes(1);
-    expect(errorSpy.mock.calls[0][0]).toBe(
-      "Missing field 'car' while writing result {}",
-    );
+    expect(errorSpy.mock.calls[0]).toStrictEqual([
+      "Missing field '%s' while writing result %o",
+      "car",
+      Object.create(null),
+    ]);
     errorSpy.mockRestore();
   });
 
-  it('should handle immediate completions with multiple subscriptions gracefully', async () => {
-    const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+  it("should handle immediate completions with multiple subscriptions gracefully", async () => {
+    const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -562,11 +597,9 @@ describe('useSubscription Hook', () => {
       }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(result.current.sub1.loading).toBe(true);
@@ -584,9 +617,12 @@ describe('useSubscription Hook', () => {
       link.simulateResult({ result: { data: null } }, /* complete */ true);
     });
 
-    await waitFor(() => {
-      expect(result.current.sub1.loading).toBe(false);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.sub1.loading).toBe(false);
+      },
+      { interval: 1 }
+    );
 
     expect(result.current.sub1.error).toBe(undefined);
     expect(result.current.sub1.data).toBe(null);
@@ -598,20 +634,26 @@ describe('useSubscription Hook', () => {
     expect(result.current.sub3.data).toBe(null);
 
     expect(errorSpy).toHaveBeenCalledTimes(3);
-    expect(errorSpy.mock.calls[0][0]).toBe(
-      "Missing field 'car' while writing result {}",
-    );
-    expect(errorSpy.mock.calls[1][0]).toBe(
-      "Missing field 'car' while writing result {}",
-    );
-    expect(errorSpy.mock.calls[2][0]).toBe(
-      "Missing field 'car' while writing result {}",
-    );
+    expect(errorSpy.mock.calls[0]).toStrictEqual([
+      "Missing field '%s' while writing result %o",
+      "car",
+      Object.create(null),
+    ]);
+    expect(errorSpy.mock.calls[1]).toStrictEqual([
+      "Missing field '%s' while writing result %o",
+      "car",
+      Object.create(null),
+    ]);
+    expect(errorSpy.mock.calls[2]).toStrictEqual([
+      "Missing field '%s' while writing result %o",
+      "car",
+      Object.create(null),
+    ]);
     errorSpy.mockRestore();
   });
 
   test("should warn when using 'onSubscriptionData' and 'onData' together", () => {
-    const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -627,26 +669,29 @@ describe('useSubscription Hook', () => {
     });
 
     renderHook(
-      () => useSubscription(subscription, {
-        onData: jest.fn(),
-        onSubscriptionData: jest.fn(),
-      }),
+      () =>
+        useSubscription(subscription, {
+          onData: jest.fn(),
+          onSubscriptionData: jest.fn(),
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(warningSpy).toHaveBeenCalledTimes(1);
-    expect(warningSpy).toHaveBeenCalledWith(expect.stringContaining("supports only the 'onSubscriptionData' or 'onData' option"));
+    expect(warningSpy).toHaveBeenCalledWith(
+      expect.stringContaining(
+        "supports only the 'onSubscriptionData' or 'onData' option"
+      )
+    );
     warningSpy.mockRestore();
   });
 
   test("prefers 'onData' when using 'onSubscriptionData' and 'onData' together", async () => {
-    jest.spyOn(console, 'warn').mockImplementation(() => {});
+    jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -657,8 +702,8 @@ describe('useSubscription Hook', () => {
 
     const results = [
       {
-        result: { data: { car: { make: 'Pagani' } } }
-      }
+        result: { data: { car: { make: "Pagani" } } },
+      },
     ];
 
     const link = new MockSubscriptionLink();
@@ -671,28 +716,30 @@ describe('useSubscription Hook', () => {
     const onSubscriptionData = jest.fn();
 
     renderHook(
-      () => useSubscription(subscription, {
-        onData,
-        onSubscriptionData,
-      }),
+      () =>
+        useSubscription(subscription, {
+          onData,
+          onSubscriptionData,
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     setTimeout(() => link.simulateResult(results[0]));
-    await waitFor(() => {
-      expect(onData).toHaveBeenCalledTimes(1);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(onData).toHaveBeenCalledTimes(1);
+      },
+      { interval: 1 }
+    );
     expect(onSubscriptionData).toHaveBeenCalledTimes(0);
   });
 
   test("uses 'onSubscriptionData' when 'onData' is absent", async () => {
-    const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -703,8 +750,8 @@ describe('useSubscription Hook', () => {
 
     const results = [
       {
-        result: { data: { car: { make: 'Pagani' } } }
-      }
+        result: { data: { car: { make: "Pagani" } } },
+      },
     ];
 
     const link = new MockSubscriptionLink();
@@ -716,27 +763,29 @@ describe('useSubscription Hook', () => {
     const onSubscriptionData = jest.fn();
 
     renderHook(
-      () => useSubscription(subscription, {
-        onSubscriptionData,
-      }),
+      () =>
+        useSubscription(subscription, {
+          onSubscriptionData,
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     setTimeout(() => link.simulateResult(results[0]));
-    await waitFor(() => {
-      expect(onSubscriptionData).toHaveBeenCalledTimes(1);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(onSubscriptionData).toHaveBeenCalledTimes(1);
+      },
+      { interval: 1 }
+    );
     warningSpy.mockRestore();
   });
 
   test("only warns once using `onSubscriptionData`", () => {
-    const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -752,16 +801,15 @@ describe('useSubscription Hook', () => {
     });
 
     const { rerender } = renderHook(
-      () => useSubscription(subscription, {
-        onSubscriptionData: jest.fn(),
-      }),
+      () =>
+        useSubscription(subscription, {
+          onSubscriptionData: jest.fn(),
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     rerender();
@@ -771,7 +819,7 @@ describe('useSubscription Hook', () => {
   });
 
   test("should warn when using 'onComplete' and 'onSubscriptionComplete' together", () => {
-    const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -787,26 +835,29 @@ describe('useSubscription Hook', () => {
     });
 
     renderHook(
-      () => useSubscription(subscription, {
-        onComplete: jest.fn(),
-        onSubscriptionComplete: jest.fn(),
-      }),
+      () =>
+        useSubscription(subscription, {
+          onComplete: jest.fn(),
+          onSubscriptionComplete: jest.fn(),
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     expect(warningSpy).toHaveBeenCalledTimes(1);
-    expect(warningSpy).toHaveBeenCalledWith(expect.stringContaining("supports only the 'onSubscriptionComplete' or 'onComplete' option"));
+    expect(warningSpy).toHaveBeenCalledWith(
+      expect.stringContaining(
+        "supports only the 'onSubscriptionComplete' or 'onComplete' option"
+      )
+    );
     warningSpy.mockRestore();
   });
 
   test("prefers 'onComplete' when using 'onComplete' and 'onSubscriptionComplete' together", async () => {
-    const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -815,9 +866,11 @@ describe('useSubscription Hook', () => {
       }
     `;
 
-    const results = [{
-      result: { data: { car: { make: 'Audi' } } }
-    }];
+    const results = [
+      {
+        result: { data: { car: { make: "Audi" } } },
+      },
+    ];
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
@@ -829,31 +882,33 @@ describe('useSubscription Hook', () => {
     const onSubscriptionComplete = jest.fn();
 
     renderHook(
-      () => useSubscription(subscription, {
-        onComplete,
-        onSubscriptionComplete,
-      }),
+      () =>
+        useSubscription(subscription, {
+          onComplete,
+          onSubscriptionComplete,
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     link.simulateResult(results[0]);
 
     setTimeout(() => link.simulateComplete());
-    await waitFor(() => {
-      expect(onComplete).toHaveBeenCalledTimes(1);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(onComplete).toHaveBeenCalledTimes(1);
+      },
+      { interval: 1 }
+    );
     expect(onSubscriptionComplete).toHaveBeenCalledTimes(0);
     warningSpy.mockRestore();
   });
 
   test("uses 'onSubscriptionComplete' when 'onComplete' is absent", async () => {
-    const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -862,9 +917,11 @@ describe('useSubscription Hook', () => {
       }
     `;
 
-    const results = [{
-      result: { data: { car: { make: 'Audi' } } }
-    }];
+    const results = [
+      {
+        result: { data: { car: { make: "Audi" } } },
+      },
+    ];
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
@@ -875,29 +932,31 @@ describe('useSubscription Hook', () => {
     const onSubscriptionComplete = jest.fn();
 
     renderHook(
-      () => useSubscription(subscription, {
-        onSubscriptionComplete,
-      }),
+      () =>
+        useSubscription(subscription, {
+          onSubscriptionComplete,
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     link.simulateResult(results[0]);
 
     setTimeout(() => link.simulateComplete());
-    await waitFor(() => {
-      expect(onSubscriptionComplete).toHaveBeenCalledTimes(1);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(onSubscriptionComplete).toHaveBeenCalledTimes(1);
+      },
+      { interval: 1 }
+    );
     warningSpy.mockRestore();
   });
 
   test("only warns once using `onSubscriptionComplete`", () => {
-    const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    const warningSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
     const subscription = gql`
       subscription {
         car {
@@ -913,16 +972,15 @@ describe('useSubscription Hook', () => {
     });
 
     const { rerender } = renderHook(
-      () => useSubscription(subscription, {
-        onSubscriptionComplete: jest.fn(),
-      }),
+      () =>
+        useSubscription(subscription, {
+          onSubscriptionComplete: jest.fn(),
+        }),
       {
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
+          <ApolloProvider client={client}>{children}</ApolloProvider>
         ),
-      },
+      }
     );
 
     rerender();
@@ -931,8 +989,8 @@ describe('useSubscription Hook', () => {
     warningSpy.mockRestore();
   });
 
-  describe('multipart subscriptions', () => {
-    it('should handle a simple subscription properly', async () => {
+  describe("multipart subscriptions", () => {
+    it("should handle a simple subscription properly", async () => {
       const subscription = gql`
         subscription ANewDieWasCreated {
           aNewDieWasCreated {
@@ -951,54 +1009,55 @@ describe('useSubscription Hook', () => {
             extensions: {
               [PROTOCOL_ERRORS_SYMBOL]: [
                 {
-                  message: 'cannot read message from websocket',
+                  message: "cannot read message from websocket",
                   extensions: [
                     {
-                      code: "WEBSOCKET_MESSAGE_ERROR"
-                    }
+                      code: "WEBSOCKET_MESSAGE_ERROR",
+                    },
                   ],
                 },
               ],
-            }
+            },
           },
         },
-      ]
+      ];
       const link = new MockSubscriptionLink();
       const client = new ApolloClient({
         link,
-        cache: new Cache({ addTypename: false })
+        cache: new Cache({ addTypename: false }),
       });
       let renderCount = 0;
 
       const { result } = renderHook(
         () => {
           renderCount++;
-          return useSubscription(subscription)
+          return useSubscription(subscription);
         },
         {
           wrapper: ({ children }) => (
-            <ApolloProvider client={client}>
-              {children}
-            </ApolloProvider>
+            <ApolloProvider client={client}>{children}</ApolloProvider>
           ),
-        },
+        }
       );
       expect(result.current.loading).toBe(true);
       expect(result.current.error).toBe(undefined);
       expect(result.current.data).toBe(undefined);
       link.simulateResult(results[0]);
       expect(renderCount).toBe(1);
-      await waitFor(() => {
-        expect(result.current.error).toBeInstanceOf(ApolloError);
-      }, { interval: 1 });
+      await waitFor(
+        () => {
+          expect(result.current.error).toBeInstanceOf(ApolloError);
+        },
+        { interval: 1 }
+      );
       expect(result.current.error!.protocolErrors[0].message).toBe(
         "cannot read message from websocket"
       );
     });
   });
 
-  it('should handle simple subscription after old in-flight teardown immediately \
-followed by new in-flight setup', async () => {
+  it("should handle simple subscription after old in-flight teardown immediately \
+followed by new in-flight setup", async () => {
     const subscription = gql`
       subscription {
         car {
@@ -1007,62 +1066,82 @@ followed by new in-flight setup', async () => {
       }
     `;
 
-    const results = ['Audi', 'BMW'].map(make => ({
+    const results = ["Audi", "BMW"].map((make) => ({
       result: { data: { car: { make } } },
     }));
 
     const link = new MockSubscriptionLink();
     const client = new ApolloClient({
       link,
-      cache: new Cache({ addTypename: false })
+      cache: new Cache({ addTypename: false }),
     });
 
     const { result, unmount, rerender } = renderHook(
       ({ coin }) => {
         const heads = useSubscription(subscription, {
           variables: {},
-          skip: coin === 'tails',
-          context: { coin: 'heads' }
+          skip: coin === "tails",
+          context: { coin: "heads" },
         });
         const tails = useSubscription(subscription, {
           variables: {},
-          skip: coin === 'heads',
-          context: { coin: 'tails' }
+          skip: coin === "heads",
+          context: { coin: "tails" },
         });
         return { heads, tails };
       },
       {
         initialProps: {
-          coin: 'heads'
+          coin: "heads",
         },
         wrapper: ({ children }) => (
-          <ApolloProvider client={client}>
-            {children}
-          </ApolloProvider>
-        )
-      },
+          <ApolloProvider client={client}>{children}</ApolloProvider>
+        ),
+      }
     );
 
-    rerender({ coin: 'tails' });
+    rerender({ coin: "tails" });
 
-    await new Promise(resolve => setTimeout(() => resolve('wait'), 20));
+    await new Promise((resolve) => setTimeout(() => resolve("wait"), 20));
 
     link.simulateResult(results[0]);
 
-    await waitFor(() => {
-      expect(result.current.tails.data).toEqual(results[0].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.tails.data).toEqual(results[0].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.heads.data).toBeUndefined();
 
-    rerender({ coin: 'heads' });
+    rerender({ coin: "heads" });
 
     link.simulateResult(results[1]);
 
-    await waitFor(() => {
-      expect(result.current.heads.data).toEqual(results[1].result.data);
-    }, { interval: 1 });
+    await waitFor(
+      () => {
+        expect(result.current.heads.data).toEqual(results[1].result.data);
+      },
+      { interval: 1 }
+    );
     expect(result.current.tails.data).toBeUndefined();
 
     unmount();
   });
 });
+
+describe.skip("Type Tests", () => {
+  test("NoInfer prevents adding arbitrary additional variables", () => {
+    const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;
+    const { variables } = useSubscription(typedNode, {
+      variables: {
+        bar: 4,
+        // @ts-expect-error
+        nonExistingVariable: "string",
+      },
+    });
+    variables?.bar;
+    // @ts-expect-error
+    variables?.nonExistingVariable;
+  });
+});
diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx
@@ -0,0 +1,10324 @@
+import React, { Fragment, StrictMode, Suspense } from "react";
+import {
+  act,
+  screen,
+  render,
+  renderHook,
+  waitFor,
+  RenderHookOptions,
+} from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { ErrorBoundary } from "react-error-boundary";
+import { GraphQLError } from "graphql";
+import { InvariantError } from "ts-invariant";
+import { equal } from "@wry/equality";
+import { expectTypeOf } from "expect-type";
+
+import {
+  gql,
+  ApolloCache,
+  ApolloClient,
+  ApolloError,
+  ApolloLink,
+  DocumentNode,
+  InMemoryCache,
+  Observable,
+  OperationVariables,
+  SubscribeToMoreOptions,
+  TypedDocumentNode,
+  split,
+  NetworkStatus,
+  ApolloQueryResult,
+  ErrorPolicy,
+} from "../../../core";
+import {
+  DeepPartial,
+  compact,
+  concatPagination,
+  getMainDefinition,
+  offsetLimitPagination,
+} from "../../../utilities";
+import {
+  MockedProvider,
+  MockedResponse,
+  MockSubscriptionLink,
+  MockLink,
+} from "../../../testing";
+import { ApolloProvider } from "../../context";
+import { SuspenseQueryHookFetchPolicy, skipToken } from "../../../react";
+import { useSuspenseQuery } from "../useSuspenseQuery";
+import {
+  RefetchWritePolicy,
+  WatchQueryFetchPolicy,
+} from "../../../core/watchQueryOptions";
+
+type RenderSuspenseHookOptions<Props, TSerializedCache = {}> = Omit<
+  RenderHookOptions<Props>,
+  "wrapper"
+> & {
+  client?: ApolloClient<TSerializedCache>;
+  link?: ApolloLink;
+  cache?: ApolloCache<TSerializedCache>;
+  mocks?: MockedResponse[];
+  strictMode?: boolean;
+};
+
+interface Renders<Result> {
+  errors: Error[];
+  errorCount: number;
+  suspenseCount: number;
+  count: number;
+  frames: Result[];
+}
+
+interface SimpleQueryData {
+  greeting: string;
+}
+
+function renderSuspenseHook<Result, Props>(
+  render: (initialProps: Props) => Result,
+  options: RenderSuspenseHookOptions<Props> = Object.create(null)
+) {
+  function SuspenseFallback() {
+    renders.suspenseCount++;
+
+    return <div>loading</div>;
+  }
+
+  const renders: Renders<Result> = {
+    errors: [],
+    errorCount: 0,
+    suspenseCount: 0,
+    count: 0,
+    frames: [],
+  };
+
+  const { mocks = [], strictMode, ...renderHookOptions } = options;
+
+  const client =
+    options.client ||
+    new ApolloClient({
+      cache: options.cache || new InMemoryCache(),
+      link: options.link || new MockLink(mocks),
+    });
+
+  const view = renderHook(
+    (props) => {
+      renders.count++;
+
+      const view = render(props);
+
+      renders.frames.push(view);
+
+      return view;
+    },
+    {
+      ...renderHookOptions,
+      wrapper: ({ children }) => {
+        const Wrapper = strictMode ? StrictMode : Fragment;
+
+        return (
+          <Wrapper>
+            <Suspense fallback={<SuspenseFallback />}>
+              <ErrorBoundary
+                fallback={<div>Error</div>}
+                onError={(error) => {
+                  renders.errorCount++;
+                  renders.errors.push(error);
+                }}
+              >
+                <ApolloProvider client={client}>{children}</ApolloProvider>
+              </ErrorBoundary>
+            </Suspense>
+          </Wrapper>
+        );
+      },
+    }
+  );
+
+  return { ...view, renders };
+}
+
+function useSimpleQueryCase() {
+  const query: TypedDocumentNode<SimpleQueryData> = gql`
+    query UserQuery {
+      greeting
+    }
+  `;
+
+  const mocks = [
+    {
+      request: { query },
+      result: { data: { greeting: "Hello" } },
+    },
+  ];
+
+  return { query, mocks };
+}
+
+function usePaginatedCase() {
+  interface QueryData {
+    letters: {
+      letter: string;
+      position: number;
+    }[];
+  }
+
+  interface Variables {
+    limit?: number;
+    offset?: number;
+  }
+
+  const query: TypedDocumentNode<QueryData, Variables> = gql`
+    query letters($limit: Int, $offset: Int) {
+      letters(limit: $limit) {
+        letter
+        position
+      }
+    }
+  `;
+
+  const data = "ABCDEFG".split("").map((letter, index) => ({
+    __typename: "Letter",
+    letter,
+    position: index + 1,
+  }));
+
+  const link = new ApolloLink((operation) => {
+    const { offset = 0, limit = 2 } = operation.variables;
+    const letters = data.slice(offset, offset + limit);
+
+    return new Observable((observer) => {
+      setTimeout(() => {
+        observer.next({ data: { letters } });
+        observer.complete();
+      }, 10);
+    });
+  });
+
+  return { query, link, data };
+}
+
+interface ErrorCaseData {
+  currentUser: {
+    id: string;
+    name: string | null;
+  };
+}
+
+function useErrorCase<TData extends ErrorCaseData>(
+  {
+    data,
+    networkError,
+    graphQLErrors,
+  }: {
+    data?: TData;
+    networkError?: Error;
+    graphQLErrors?: GraphQLError[];
+  } = Object.create(null)
+) {
+  const query: TypedDocumentNode<TData, never> = gql`
+    query MyQuery {
+      currentUser {
+        id
+        name
+      }
+    }
+  `;
+
+  const mock: MockedResponse<TData> = compact({
+    request: { query },
+    result: (data || graphQLErrors) && compact({ data, errors: graphQLErrors }),
+    error: networkError,
+  });
+
+  return { query, mocks: [mock] };
+}
+
+interface VariablesCaseData {
+  character: {
+    id: string;
+    name: string;
+  };
+}
+
+interface VariablesCaseVariables {
+  id: string;
+}
+
+function useVariablesQueryCase() {
+  const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"];
+
+  const query: TypedDocumentNode<
+    VariablesCaseData,
+    VariablesCaseVariables
+  > = gql`
+    query CharacterQuery($id: ID!) {
+      character(id: $id) {
+        id
+        name
+      }
+    }
+  `;
+
+  const mocks = CHARACTERS.map((name, index) => ({
+    request: { query, variables: { id: String(index + 1) } },
+    result: {
+      data: {
+        character: { __typename: "Character", id: String(index + 1), name },
+      },
+    },
+  }));
+
+  return { query, mocks };
+}
+
+function wait(delay: number) {
+  return new Promise((resolve) => setTimeout(resolve, delay));
+}
+
+describe("useSuspenseQuery", () => {
+  it("validates the GraphQL query as a query", () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const query = gql`
+      mutation ShouldThrow {
+        createException
+      }
+    `;
+
+    expect(() => {
+      renderHook(() => useSuspenseQuery(query), {
+        wrapper: ({ children }) => <MockedProvider>{children}</MockedProvider>,
+      });
+    }).toThrowError(
+      new InvariantError(
+        "Running a Query requires a graphql Query, but a Mutation was used instead."
+      )
+    );
+
+    consoleSpy.mockRestore();
+  });
+
+  it("ensures a valid fetch policy is used", () => {
+    const INVALID_FETCH_POLICIES = ["cache-only", "standby"];
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+    const { query } = useSimpleQueryCase();
+
+    INVALID_FETCH_POLICIES.forEach((fetchPolicy: any) => {
+      expect(() => {
+        renderHook(() => useSuspenseQuery(query, { fetchPolicy }), {
+          wrapper: ({ children }) => (
+            <MockedProvider>{children}</MockedProvider>
+          ),
+        });
+      }).toThrowError(
+        new InvariantError(
+          `The fetch policy \`${fetchPolicy}\` is not supported with suspense.`
+        )
+      );
+    });
+
+    consoleSpy.mockRestore();
+  });
+
+  it("ensures a valid fetch policy is used when defined via global options", () => {
+    const INVALID_FETCH_POLICIES: WatchQueryFetchPolicy[] = [
+      "cache-only",
+      "standby",
+    ];
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+    const { query } = useSimpleQueryCase();
+
+    INVALID_FETCH_POLICIES.forEach((fetchPolicy) => {
+      expect(() => {
+        const client = new ApolloClient({
+          cache: new InMemoryCache(),
+          link: new MockLink([]),
+          defaultOptions: {
+            watchQuery: {
+              fetchPolicy,
+            },
+          },
+        });
+
+        renderHook(() => useSuspenseQuery(query), {
+          wrapper: ({ children }) => (
+            <ApolloProvider client={client}>{children}</ApolloProvider>
+          ),
+        });
+      }).toThrowError(
+        new InvariantError(
+          `The fetch policy \`${fetchPolicy}\` is not supported with suspense.`
+        )
+      );
+    });
+
+    consoleSpy.mockRestore();
+  });
+
+  it("suspends a query and returns results", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { mocks }
+    );
+
+    // ensure the hook suspends immediately
+    expect(renders.suspenseCount).toBe(1);
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        error: undefined,
+      });
+    });
+
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.count).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends a query with variables and returns results", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { id: "1" } }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        error: undefined,
+      });
+    });
+
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.count).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("returns the same results for the same variables", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) => useSuspenseQuery(query, { variables: { id } }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        error: undefined,
+      });
+    });
+
+    const previousResult = result.current;
+
+    rerender({ id: "1" });
+
+    expect(result.current).toBe(previousResult);
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("ensures result is referentially stable", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const { result, rerender } = renderSuspenseHook(
+      ({ id }) => useSuspenseQuery(query, { variables: { id } }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    expect(screen.getByText("loading")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        error: undefined,
+      });
+    });
+
+    const previousResult = result.current;
+
+    rerender({ id: "1" });
+
+    expect(result.current).toBe(previousResult);
+  });
+
+  it("ensures refetch, fetchMore, and subscribeToMore are referentially stable even after result data has changed", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    const { result } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      client,
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        error: undefined,
+      });
+    });
+
+    const previousResult = result.current;
+
+    client.writeQuery({
+      query,
+      data: { greeting: "Updated cache greeting" },
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual({
+        greeting: "Updated cache greeting",
+      });
+    });
+
+    expect(result.current.fetchMore).toBe(previousResult.fetchMore);
+    expect(result.current.refetch).toBe(previousResult.refetch);
+    expect(result.current.subscribeToMore).toBe(previousResult.subscribeToMore);
+  });
+
+  it('enables canonical results when canonizeResults is "true"', async () => {
+    interface Result {
+      __typename: string;
+      value: number;
+    }
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Result: {
+          keyFields: false,
+        },
+      },
+    });
+
+    const query: TypedDocumentNode<{ results: Result[] }> = gql`
+      query {
+        results {
+          value
+        }
+      }
+    `;
+
+    const results: Result[] = [
+      { __typename: "Result", value: 0 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 2 },
+      { __typename: "Result", value: 3 },
+      { __typename: "Result", value: 5 },
+    ];
+
+    cache.writeQuery({
+      query,
+      data: { results },
+    });
+
+    const { result } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { canonizeResults: true }),
+      { cache }
+    );
+
+    const { data } = result.current;
+    const resultSet = new Set(data.results);
+    const values = Array.from(resultSet).map((item) => item.value);
+
+    expect(data).toEqual({ results });
+    expect(data.results.length).toBe(6);
+    expect(resultSet.size).toBe(5);
+    expect(values).toEqual([0, 1, 2, 3, 5]);
+  });
+
+  it("can disable canonical results when the cache's canonizeResults setting is true", async () => {
+    interface Result {
+      __typename: string;
+      value: number;
+    }
+
+    const cache = new InMemoryCache({
+      canonizeResults: true,
+      typePolicies: {
+        Result: {
+          keyFields: false,
+        },
+      },
+    });
+
+    const query: TypedDocumentNode<{ results: Result[] }> = gql`
+      query {
+        results {
+          value
+        }
+      }
+    `;
+
+    const results: Result[] = [
+      { __typename: "Result", value: 0 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 2 },
+      { __typename: "Result", value: 3 },
+      { __typename: "Result", value: 5 },
+    ];
+
+    cache.writeQuery({
+      query,
+      data: { results },
+    });
+
+    const { result } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { canonizeResults: false }),
+      { cache }
+    );
+
+    const { data } = result.current;
+    const resultSet = new Set(data.results);
+    const values = Array.from(resultSet).map((item) => item.value);
+
+    expect(data).toEqual({ results });
+    expect(data.results.length).toBe(6);
+    expect(resultSet.size).toBe(6);
+    expect(values).toEqual([0, 1, 1, 2, 3, 5]);
+  });
+
+  it("tears down the query on unmount", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const client = new ApolloClient({
+      link: new ApolloLink(() => Observable.of(mocks[0].result)),
+      cache: new InMemoryCache(),
+    });
+
+    const { result, unmount } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { client }
+    );
+
+    await waitFor(() =>
+      expect(result.current.data).toEqual(mocks[0].result.data)
+    );
+
+    expect(client.getObservableQueries().size).toBe(1);
+    expect(client).toHaveSuspenseCacheEntryUsing(query);
+
+    unmount();
+
+    // We need to wait a tick since the cleanup is run in a setTimeout to
+    // prevent strict mode bugs.
+    await wait(0);
+
+    expect(client.getObservableQueries().size).toBe(0);
+    expect(client).not.toHaveSuspenseCacheEntryUsing(query);
+  });
+
+  it("tears down all queries when rendering with multiple variable sets", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    const { rerender, result, unmount } = renderSuspenseHook(
+      ({ id }) => useSuspenseQuery(query, { variables: { id } }),
+      { client, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() =>
+      expect(result.current.data).toEqual(mocks[0].result.data)
+    );
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mocks[1].result.data);
+    });
+
+    unmount();
+
+    // We need to wait a tick since the cleanup is run in a setTimeout to
+    // prevent strict mode bugs.
+    await wait(0);
+
+    expect(client.getObservableQueries().size).toBe(0);
+
+    expect(client).not.toHaveSuspenseCacheEntryUsing(query, {
+      variables: { id: "1" },
+    });
+    expect(client).not.toHaveSuspenseCacheEntryUsing(query, {
+      variables: { id: "2" },
+    });
+  });
+
+  it("tears down all queries when multiple clients are used", async () => {
+    const { query } = useVariablesQueryCase();
+
+    const client1 = new ApolloClient({
+      link: new MockLink([
+        {
+          request: { query, variables: { id: "1" } },
+          result: { data: { character: { id: "1", name: "Client 1" } } },
+        },
+      ]),
+      cache: new InMemoryCache(),
+    });
+
+    const client2 = new ApolloClient({
+      link: new MockLink([
+        {
+          request: { query, variables: { id: "1" } },
+          result: { data: { character: { id: "1", name: "Client 2" } } },
+        },
+      ]),
+      cache: new InMemoryCache(),
+    });
+
+    const { rerender, result, unmount } = renderSuspenseHook(
+      ({ client }) =>
+        useSuspenseQuery(query, { client, variables: { id: "1" } }),
+      { initialProps: { client: client1 } }
+    );
+
+    await waitFor(() =>
+      expect(result.current.data).toEqual({
+        character: { id: "1", name: "Client 1" },
+      })
+    );
+
+    rerender({ client: client2 });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual({
+        character: { id: "1", name: "Client 2" },
+      });
+    });
+
+    const variables = { id: "1" };
+
+    unmount();
+
+    // We need to wait a tick since the cleanup is run in a setTimeout to
+    // prevent strict mode bugs.
+    await wait(0);
+
+    expect(client1.getObservableQueries().size).toBe(0);
+    expect(client2.getObservableQueries().size).toBe(0);
+    expect(client1).not.toHaveSuspenseCacheEntryUsing(query, {
+      variables,
+    });
+    expect(client2).not.toHaveSuspenseCacheEntryUsing(query, {
+      variables,
+    });
+  });
+
+  it("tears down the query if the component never renders again after suspending", async () => {
+    jest.useFakeTimers();
+    const { query } = useSimpleQueryCase();
+    const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      const [showGreeting, setShowGreeting] = React.useState(true);
+
+      return (
+        <ApolloProvider client={client}>
+          <button onClick={() => setShowGreeting(false)}>Hide greeting</button>
+          {showGreeting && (
+            <Suspense fallback="Loading greeting...">
+              <Greeting />
+            </Suspense>
+          )}
+        </ApolloProvider>
+      );
+    }
+
+    function Greeting() {
+      const { data } = useSuspenseQuery(query);
+
+      return <span>{data.greeting}</span>;
+    }
+
+    render(<App />);
+
+    // Ensure <Greeting /> suspends immediately
+    expect(screen.getByText("Loading greeting...")).toBeInTheDocument();
+
+    // Hide the greeting before it finishes loading data
+    await act(() => user.click(screen.getByText("Hide greeting")));
+
+    expect(screen.queryByText("Loading greeting...")).not.toBeInTheDocument();
+
+    link.simulateResult({ result: { data: { greeting: "Hello" } } });
+    link.simulateComplete();
+
+    expect(client.getObservableQueries().size).toBe(1);
+    expect(client).toHaveSuspenseCacheEntryUsing(query);
+
+    jest.advanceTimersByTime(30_000);
+
+    expect(client.getObservableQueries().size).toBe(0);
+    expect(client).not.toHaveSuspenseCacheEntryUsing(query);
+
+    jest.useRealTimers();
+
+    // Avoid act warnings for a suspended resource
+    // eslint-disable-next-line testing-library/no-unnecessary-act
+    await act(() => wait(0));
+  });
+
+  it("has configurable auto dispose timer if the component never renders again after suspending", async () => {
+    jest.useFakeTimers();
+    const { query } = useSimpleQueryCase();
+    const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+      defaultOptions: {
+        react: {
+          suspense: {
+            autoDisposeTimeoutMs: 5000,
+          },
+        },
+      },
+    });
+
+    function App() {
+      const [showGreeting, setShowGreeting] = React.useState(true);
+
+      return (
+        <ApolloProvider client={client}>
+          <button onClick={() => setShowGreeting(false)}>Hide greeting</button>
+          {showGreeting && (
+            <Suspense fallback="Loading greeting...">
+              <Greeting />
+            </Suspense>
+          )}
+        </ApolloProvider>
+      );
+    }
+
+    function Greeting() {
+      const { data } = useSuspenseQuery(query);
+
+      return <span>{data.greeting}</span>;
+    }
+
+    render(<App />);
+
+    // Ensure <Greeting /> suspends immediately
+    expect(screen.getByText("Loading greeting...")).toBeInTheDocument();
+
+    // Hide the greeting before it finishes loading data
+    await act(() => user.click(screen.getByText("Hide greeting")));
+
+    expect(screen.queryByText("Loading greeting...")).not.toBeInTheDocument();
+
+    link.simulateResult({ result: { data: { greeting: "Hello" } } });
+    link.simulateComplete();
+
+    expect(client.getObservableQueries().size).toBe(1);
+    expect(client).toHaveSuspenseCacheEntryUsing(query);
+
+    jest.advanceTimersByTime(5_000);
+
+    expect(client.getObservableQueries().size).toBe(0);
+    expect(client).not.toHaveSuspenseCacheEntryUsing(query);
+
+    jest.useRealTimers();
+
+    // Avoid act warnings for a suspended resource
+    // eslint-disable-next-line testing-library/no-unnecessary-act
+    await act(() => wait(0));
+  });
+
+  it("cancels auto dispose if the component renders before timer finishes", async () => {
+    jest.useFakeTimers();
+    const { query } = useSimpleQueryCase();
+    const link = new ApolloLink(() => {
+      return new Observable((observer) => {
+        setTimeout(() => {
+          observer.next({ data: { greeting: "Hello" } });
+          observer.complete();
+        }, 10);
+      });
+    });
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback="Loading greeting...">
+            <Greeting />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function Greeting() {
+      const { data } = useSuspenseQuery(query);
+
+      return <span>{data.greeting}</span>;
+    }
+
+    render(<App />);
+
+    // Ensure <Greeting /> suspends immediately
+    expect(screen.getByText("Loading greeting...")).toBeInTheDocument();
+
+    jest.advanceTimersByTime(10);
+
+    await waitFor(() => {
+      expect(screen.getByText("Hello")).toBeInTheDocument();
+    });
+
+    jest.advanceTimersByTime(30_000);
+
+    expect(client.getObservableQueries().size).toBe(1);
+    expect(client).toHaveSuspenseCacheEntryUsing(query);
+
+    jest.useRealTimers();
+  });
+
+  it("allows the client to be overridden", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const globalClient = new ApolloClient({
+      link: new ApolloLink(() =>
+        Observable.of({ data: { greeting: "global hello" } })
+      ),
+      cache: new InMemoryCache(),
+    });
+
+    const localClient = new ApolloClient({
+      link: new ApolloLink(() =>
+        Observable.of({ data: { greeting: "local hello" } })
+      ),
+      cache: new InMemoryCache(),
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { client: localClient }),
+      { client: globalClient }
+    );
+
+    await waitFor(() =>
+      expect(result.current.data).toEqual({ greeting: "local hello" })
+    );
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "local hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("allows the client to be overridden in strict mode", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const globalClient = new ApolloClient({
+      link: new ApolloLink(() =>
+        Observable.of({ data: { greeting: "global hello" } })
+      ),
+      cache: new InMemoryCache(),
+    });
+
+    const localClient = new ApolloClient({
+      link: new ApolloLink(() =>
+        Observable.of({ data: { greeting: "local hello" } })
+      ),
+      cache: new InMemoryCache(),
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { client: localClient }),
+      { strictMode: true, client: globalClient }
+    );
+
+    await waitFor(() =>
+      expect(result.current.data).toEqual({ greeting: "local hello" })
+    );
+
+    // React double invokes the render function in strict mode so we expect
+    // to render 2 frames after the initial suspense.
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "local hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "local hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("returns the client used in the result", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const client = new ApolloClient({
+      link: new ApolloLink(() =>
+        Observable.of({ data: { greeting: "hello" } })
+      ),
+      cache: new InMemoryCache(),
+    });
+
+    const { result } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      client,
+    });
+
+    // wait for query to finish suspending to avoid warnings
+    await waitFor(() => {
+      expect(result.current.data).toEqual({ greeting: "hello" });
+    });
+
+    expect(result.current.client).toBe(client);
+  });
+
+  it("suspends when changing variables", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) => useSuspenseQuery(query, { variables: { id } }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends and fetches data from new client when changing clients", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const client1 = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink([
+        {
+          request: { query },
+          result: { data: { greeting: "Hello client 1" } },
+        },
+      ]),
+    });
+
+    const client2 = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink([
+        {
+          request: { query },
+          result: { data: { greeting: "Hello client 2" } },
+        },
+      ]),
+    });
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ client }) => useSuspenseQuery(query, { client }),
+      { initialProps: { client: client1 } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello client 1" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ client: client2 });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello client 2" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello client 1" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello client 2" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("allows custom query key so two components that share same query and variables do not interfere with each other", async () => {
+    interface Data {
+      todo: {
+        id: number;
+        name: string;
+        completed: boolean;
+      };
+    }
+
+    interface Variables {
+      id: number;
+    }
+
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query GetTodo($id: ID!) {
+        todo(id: $id) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: 1 } },
+        result: {
+          data: { todo: { id: 1, name: "Take out trash", completed: false } },
+        },
+        delay: 20,
+      },
+      // refetch
+      {
+        request: { query, variables: { id: 1 } },
+        result: {
+          data: { todo: { id: 1, name: "Take out trash", completed: true } },
+        },
+        delay: 20,
+      },
+    ];
+
+    const user = userEvent.setup();
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function Spinner({ name }: { name: string }) {
+      return <span>Loading {name}</span>;
+    }
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback={<Spinner name="first" />}>
+            <Todo name="first" />
+          </Suspense>
+          <Suspense fallback={<Spinner name="second" />}>
+            <Todo name="second" />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function Todo({ name }: { name: string }) {
+      const { data, refetch } = useSuspenseQuery(query, {
+        // intentionally use no-cache to allow us to verify each suspense
+        // component is independent of each other
+        fetchPolicy: "no-cache",
+        variables: { id: 1 },
+        queryKey: [name],
+      });
+
+      return (
+        <div>
+          <button onClick={() => refetch()}>Refetch {name}</button>
+          <span data-testid={[name, "data"].join(".")}>
+            {data.todo.name} {data.todo.completed && "(completed)"}
+          </span>
+        </div>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.getByText("Loading first")).toBeInTheDocument();
+    expect(screen.getByText("Loading second")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("first.data")).toHaveTextContent(
+        "Take out trash"
+      );
+    });
+
+    expect(screen.getByTestId("second.data")).toHaveTextContent(
+      "Take out trash"
+    );
+
+    await act(() => user.click(screen.getByText("Refetch first")));
+
+    // Ensure that refetching the first todo does not update the second todo
+    // as well
+    expect(screen.getByText("Loading first")).toBeInTheDocument();
+    expect(screen.queryByText("Loading second")).not.toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("first.data")).toHaveTextContent(
+        "Take out trash (completed)"
+      );
+    });
+
+    // Ensure that refetching the first todo did not affect the second
+    expect(screen.getByTestId("second.data")).toHaveTextContent(
+      "Take out trash"
+    );
+  });
+
+  it("suspends and refetches data when changing query keys", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello first fetch" } },
+        delay: 20,
+      },
+      {
+        request: { query },
+        result: { data: { greeting: "Hello second fetch" } },
+        delay: 20,
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ queryKey }) =>
+        // intentionally use a fetch policy that will execute a network request
+        useSuspenseQuery(query, { queryKey, fetchPolicy: "network-only" }),
+      { mocks, initialProps: { queryKey: ["first"] } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ queryKey: ["second"] });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends and refetches data when part of the query key changes", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello first fetch" } },
+        delay: 20,
+      },
+      {
+        request: { query },
+        result: { data: { greeting: "Hello second fetch" } },
+        delay: 20,
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ queryKey }) =>
+        // intentionally use a fetch policy that will execute a network request
+        useSuspenseQuery(query, { queryKey, fetchPolicy: "network-only" }),
+      { mocks, initialProps: { queryKey: ["greeting", 1] } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ queryKey: ["greeting", 2] });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends and refetches when using plain string query keys", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello first fetch" } },
+        delay: 20,
+      },
+      {
+        request: { query },
+        result: { data: { greeting: "Hello second fetch" } },
+        delay: 20,
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ queryKey }) =>
+        // intentionally use a fetch policy that will execute a network request
+        useSuspenseQuery(query, { queryKey, fetchPolicy: "network-only" }),
+      { mocks, initialProps: { queryKey: "first" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ queryKey: "second" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends and refetches when using numeric query keys", async () => {
+    const { query } = useSimpleQueryCase();
+
+    const mocks = [
+      {
+        request: { query },
+        result: { data: { greeting: "Hello first fetch" } },
+        delay: 20,
+      },
+      {
+        request: { query },
+        result: { data: { greeting: "Hello second fetch" } },
+        delay: 20,
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ queryKey }) =>
+        // intentionally use a fetch policy that will execute a network request
+        useSuspenseQuery(query, { queryKey, fetchPolicy: "network-only" }),
+      { mocks, initialProps: { queryKey: 1 } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ queryKey: 2 });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello first fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello second fetch" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("responds to cache updates after changing variables", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) => useSuspenseQuery(query, { variables: { id } }),
+      { client, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    client.writeQuery({
+      query,
+      variables: { id: "2" },
+      data: { character: { id: "2", name: "Cached hero" } },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { character: { id: "2", name: "Cached hero" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.count).toBe(5);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { character: { id: "2", name: "Cached hero" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("uses cached result and does not suspend when switching back to already used variables while using `cache-first` fetch policy", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(query, {
+          fetchPolicy: "cache-first",
+          variables: { id },
+        }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "1" });
+
+    expect(result.current).toMatchObject({
+      ...mocks[0].result,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    expect(renders.count).toBe(5);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("uses cached result with network request and does not suspend when switching back to already used variables while using `cache-and-network` fetch policy", async () => {
+    const query: TypedDocumentNode<
+      VariablesCaseData,
+      VariablesCaseVariables
+    > = gql`
+      query CharacterQuery($id: ID!) {
+        character(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: {
+            character: { __typename: "Character", id: "1", name: "Spider-Man" },
+          },
+        },
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "2",
+              name: "Black Widow",
+            },
+          },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Spider-Man (refetch)",
+            },
+          },
+        },
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(query, {
+          fetchPolicy: "cache-and-network",
+          variables: { id },
+        }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "1" });
+
+    expect(result.current).toMatchObject({
+      ...mocks[0].result,
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(6);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("refetches and suspends when switching back to already used variables while using `network-only` fetch policy", async () => {
+    const query: TypedDocumentNode<
+      VariablesCaseData,
+      VariablesCaseVariables
+    > = gql`
+      query CharacterQuery($id: ID!) {
+        character(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: {
+            character: { __typename: "Character", id: "1", name: "Spider-Man" },
+          },
+        },
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "2",
+              name: "Black Widow",
+            },
+          },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Spider-Man (refetch)",
+            },
+          },
+        },
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(query, {
+          fetchPolicy: "network-only",
+          variables: { id },
+        }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "1" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(6);
+    expect(renders.suspenseCount).toBe(3);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("refetches and suspends when switching back to already used variables while using `no-cache` fetch policy", async () => {
+    const query: TypedDocumentNode<
+      VariablesCaseData,
+      VariablesCaseVariables
+    > = gql`
+      query CharacterQuery($id: ID!) {
+        character(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: {
+            character: { __typename: "Character", id: "1", name: "Spider-Man" },
+          },
+        },
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "2",
+              name: "Black Widow",
+            },
+          },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Spider-Man (refetch)",
+            },
+          },
+        },
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(query, {
+          fetchPolicy: "no-cache",
+          variables: { id },
+        }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "1" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(6);
+    expect(renders.suspenseCount).toBe(3);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("responds to cache updates after changing back to already fetched variables", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ id }) => useSuspenseQuery(query, { variables: { id } }),
+      { client, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "1" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    client.writeQuery({
+      query,
+      variables: { id: "1" },
+      data: { character: { id: "1", name: "Cached hero" } },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { character: { id: "1", name: "Cached hero" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.count).toBe(6);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { character: { id: "1", name: "Cached hero" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('does not suspend when data is in the cache and using a "cache-first" fetch policy', async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: { greeting: "hello from cache" },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }),
+      { cache, mocks }
+    );
+
+    expect(result.current).toMatchObject({
+      data: { greeting: "hello from cache" },
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    expect(renders.count).toBe(1);
+    expect(renders.suspenseCount).toBe(0);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "hello from cache" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('does not initiate a network request when data is in the cache and using a "cache-first" fetch policy', async () => {
+    let fetchCount = 0;
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const link = new ApolloLink(() => {
+      return new Observable((observer) => {
+        fetchCount++;
+
+        const mock = mocks[0];
+
+        observer.next(mock.result);
+        observer.complete();
+      });
+    });
+
+    cache.writeQuery({
+      query,
+      data: { greeting: "hello from cache" },
+    });
+
+    renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }),
+      { cache, link, initialProps: { id: "1" } }
+    );
+
+    expect(fetchCount).toBe(0);
+  });
+
+  it('suspends when partial data is in the cache and using a "cache-first" fetch policy', async () => {
+    const fullQuery = gql`
+      query {
+        character {
+          id
+          name
+        }
+      }
+    `;
+
+    const partialQuery = gql`
+      query {
+        character {
+          id
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: fullQuery },
+        result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { id: "1" } },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(fullQuery, { fetchPolicy: "cache-first" }),
+      { cache, mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => {
+    const fullQuery = gql`
+      query {
+        character {
+          id
+          name
+        }
+      }
+    `;
+
+    const partialQuery = gql`
+      query {
+        character {
+          id
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: fullQuery },
+        result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { id: "1" } },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(fullQuery, {
+          fetchPolicy: "cache-first",
+          returnPartialData: true,
+        }),
+      { cache, mocks }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: { character: { id: "1" } },
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(0);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { character: { id: "1" } },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => {
+    const { query: fullQuery, mocks } = useVariablesQueryCase();
+
+    const partialQuery = gql`
+      query ($id: ID!) {
+        character(id: $id) {
+          id
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { id: "1" } },
+      variables: { id: "1" },
+    });
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(fullQuery, {
+          fetchPolicy: "cache-first",
+          returnPartialData: true,
+          variables: { id },
+        }),
+      { cache, mocks, initialProps: { id: "1" } }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: { character: { id: "1" } },
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { character: { id: "1" } },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('suspends when data is in the cache and using a "network-only" fetch policy', async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: { greeting: "hello from cache" },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "network-only" }),
+      { cache, mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => {
+    const fullQuery = gql`
+      query {
+        character {
+          id
+          name
+        }
+      }
+    `;
+
+    const partialQuery = gql`
+      query {
+        character {
+          id
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: fullQuery },
+        result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { id: "1" } },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(fullQuery, {
+          fetchPolicy: "network-only",
+          returnPartialData: true,
+        }),
+      { cache, mocks }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('suspends and does not overwrite cache when data is in the cache and using a "no-cache" fetch policy', async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: { greeting: "hello from cache" },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "no-cache" }),
+      { cache, mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    const cachedData = cache.readQuery({ query });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+    expect(cachedData).toEqual({ greeting: "hello from cache" });
+  });
+
+  it('maintains results when rerendering a query using a "no-cache" fetch policy', async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "no-cache" }),
+      { cache, mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+
+    rerender();
+
+    expect(result.current).toMatchObject({
+      ...mocks[0].result,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => {
+    const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
+
+    const fullQuery = gql`
+      query {
+        character {
+          id
+          name
+        }
+      }
+    `;
+
+    const partialQuery = gql`
+      query {
+        character {
+          id
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: fullQuery },
+        result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { id: "1" } },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(fullQuery, {
+          fetchPolicy: "no-cache",
+          returnPartialData: true,
+        }),
+      { cache, mocks }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('warns when using returnPartialData with a "no-cache" fetch policy', async () => {
+    const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
+
+    const { query, mocks } = useSimpleQueryCase();
+
+    renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          fetchPolicy: "no-cache",
+          returnPartialData: true,
+        }),
+      { mocks }
+    );
+
+    expect(console.warn).toHaveBeenCalledTimes(1);
+    expect(console.warn).toHaveBeenCalledWith(
+      "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy."
+    );
+
+    consoleSpy.mockRestore();
+  });
+
+  it('does not suspend when data is in the cache and using a "cache-and-network" fetch policy', async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: { greeting: "hello from cache" },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "cache-and-network" }),
+      { cache, mocks }
+    );
+
+    expect(result.current).toMatchObject({
+      data: { greeting: "hello from cache" },
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(0);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "hello from cache" },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => {
+    const fullQuery = gql`
+      query {
+        character {
+          id
+          name
+        }
+      }
+    `;
+
+    const partialQuery = gql`
+      query {
+        character {
+          id
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: fullQuery },
+        result: { data: { character: { id: "1", name: "Doctor Strange" } } },
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { id: "1" } },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(fullQuery, {
+          fetchPolicy: "cache-and-network",
+          returnPartialData: true,
+        }),
+      { cache, mocks }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: { character: { id: "1" } },
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(0);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { character: { id: "1" } },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => {
+    const { query: fullQuery, mocks } = useVariablesQueryCase();
+
+    const partialQuery = gql`
+      query ($id: ID!) {
+        character(id: $id) {
+          id
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { id: "1" } },
+      variables: { id: "1" },
+    });
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(fullQuery, {
+          fetchPolicy: "cache-and-network",
+          returnPartialData: true,
+          variables: { id },
+        }),
+      { cache, mocks, initialProps: { id: "1" } }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: { character: { id: "1" } },
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { character: { id: "1" } },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "cache-and-network",
+  ])(
+    'writes to the cache when using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const { query, mocks } = useVariablesQueryCase();
+
+      const cache = new InMemoryCache();
+
+      const { result } = renderSuspenseHook(
+        ({ id }) => useSuspenseQuery(query, { fetchPolicy, variables: { id } }),
+        { cache, mocks, initialProps: { id: "1" } }
+      );
+
+      await waitFor(() => {
+        expect(result.current.data).toEqual(mocks[0].result.data);
+      });
+
+      const cachedData = cache.readQuery({ query, variables: { id: "1" } });
+
+      expect(cachedData).toEqual(mocks[0].result.data);
+    }
+  );
+
+  it('does not write to the cache when using a "no-cache" fetch policy', async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const { result } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(query, { fetchPolicy: "no-cache", variables: { id } }),
+      { cache, mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mocks[0].result.data);
+    });
+
+    const cachedData = cache.readQuery({ query, variables: { id: "1" } });
+
+    expect(cachedData).toBeNull();
+  });
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "cache-and-network",
+  ])(
+    'responds to cache updates when using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const { query, mocks } = useSimpleQueryCase();
+
+      const client = new ApolloClient({
+        cache: new InMemoryCache(),
+        link: new MockLink(mocks),
+      });
+
+      const { result, renders } = renderSuspenseHook(
+        () => useSuspenseQuery(query, { fetchPolicy }),
+        { client }
+      );
+
+      await waitFor(() => {
+        expect(result.current.data).toEqual(mocks[0].result.data);
+      });
+
+      client.writeQuery({
+        query,
+        data: { greeting: "Updated hello" },
+      });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: { greeting: "Updated hello" },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+      expect(renders.suspenseCount).toBe(1);
+      expect(renders.count).toBe(3);
+      expect(renders.frames).toMatchObject([
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          data: { greeting: "Updated hello" },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    }
+  );
+
+  it('does not respond to cache updates when using a "no-cache" fetch policy', async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "no-cache" }),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mocks[0].result.data);
+    });
+
+    client.writeQuery({
+      query,
+      data: { greeting: "Updated hello" },
+    });
+
+    // Wait for a while to ensure no updates happen asynchronously
+    await wait(100);
+
+    expect(result.current).toMatchObject({
+      ...mocks[0].result,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.count).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "no-cache",
+    "cache-and-network",
+  ])(
+    're-suspends the component when changing variables and using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const { query, mocks } = useVariablesQueryCase();
+
+      const { result, rerender, renders } = renderSuspenseHook(
+        ({ id }) => useSuspenseQuery(query, { fetchPolicy, variables: { id } }),
+        { mocks, initialProps: { id: "1" } }
+      );
+
+      expect(renders.suspenseCount).toBe(1);
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          ...mocks[0].result,
+          error: undefined,
+        });
+      });
+
+      rerender({ id: "2" });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          ...mocks[1].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      // Renders:
+      // 1. Initiate fetch and suspend
+      // 2. Unsuspend and return results from initial fetch
+      // 3. Change variables and suspend
+      // 5. Unsuspend and return results from refetch
+      expect(renders.count).toBe(4);
+      expect(renders.suspenseCount).toBe(2);
+      expect(renders.frames).toMatchObject([
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          ...mocks[1].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    }
+  );
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "no-cache",
+    "cache-and-network",
+  ])(
+    're-suspends the component when changing queries and using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const query1: TypedDocumentNode<{ hello: string }> = gql`
+        query Query1 {
+          hello
+        }
+      `;
+
+      const query2: TypedDocumentNode<{ world: string }> = gql`
+        query Query2 {
+          world
+        }
+      `;
+
+      const mocks = [
+        {
+          request: { query: query1 },
+          result: { data: { hello: "query1" } },
+        },
+        {
+          request: { query: query2 },
+          result: { data: { world: "query2" } },
+        },
+      ];
+
+      const { result, rerender, renders } = renderSuspenseHook(
+        ({ query }) => useSuspenseQuery(query, { fetchPolicy }),
+        { mocks, initialProps: { query: query1 as DocumentNode } }
+      );
+
+      expect(renders.suspenseCount).toBe(1);
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      rerender({ query: query2 });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          ...mocks[1].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      // Renders:
+      // 1. Initiate fetch and suspend
+      // 2. Unsuspend and return results from initial fetch
+      // 3. Change queries and suspend
+      // 5. Unsuspend and return results from refetch
+      expect(renders.count).toBe(4);
+      expect(renders.suspenseCount).toBe(2);
+      expect(renders.frames).toMatchObject([
+        {
+          ...mocks[0].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          ...mocks[1].result,
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    }
+  );
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "no-cache",
+    "cache-and-network",
+  ])(
+    'ensures data is fetched the correct amount of times when changing variables and using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const { query, mocks } = useVariablesQueryCase();
+
+      let fetchCount = 0;
+
+      const link = new ApolloLink((operation) => {
+        return new Observable((observer) => {
+          fetchCount++;
+
+          const mock = mocks.find(({ request }) =>
+            equal(request.variables, operation.variables)
+          );
+
+          if (!mock) {
+            throw new Error("Could not find mock for operation");
+          }
+
+          observer.next(mock.result);
+          observer.complete();
+        });
+      });
+
+      const { result, rerender } = renderSuspenseHook(
+        ({ id }) => useSuspenseQuery(query, { fetchPolicy, variables: { id } }),
+        { link, initialProps: { id: "1" } }
+      );
+
+      await waitFor(() => {
+        expect(result.current.data).toEqual(mocks[0].result.data);
+      });
+
+      expect(fetchCount).toBe(1);
+
+      rerender({ id: "2" });
+
+      await waitFor(() => {
+        expect(result.current.data).toEqual(mocks[1].result.data);
+      });
+
+      expect(fetchCount).toBe(2);
+    }
+  );
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "no-cache",
+    "cache-and-network",
+  ])(
+    'ensures data is fetched and suspended the correct amount of times in strict mode while using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const { query, mocks } = useVariablesQueryCase();
+
+      let fetchCount = 0;
+
+      const link = new ApolloLink((operation) => {
+        return new Observable((observer) => {
+          fetchCount++;
+
+          const mock = mocks.find(({ request }) =>
+            equal(request.variables, operation.variables)
+          );
+
+          if (!mock) {
+            throw new Error("Could not find mock for operation");
+          }
+
+          observer.next(mock.result);
+          observer.complete();
+        });
+      });
+
+      const { result, renders } = renderSuspenseHook(
+        ({ id }) => useSuspenseQuery(query, { fetchPolicy, variables: { id } }),
+        { strictMode: true, link, initialProps: { id: "1" } }
+      );
+
+      await waitFor(() => {
+        expect(result.current.data).toEqual(mocks[0].result.data);
+      });
+
+      expect(fetchCount).toBe(1);
+
+      // React double invokes the render function in strict mode so the suspense
+      // fallback is rendered twice before the promise is resolved
+      // https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects
+      expect(renders.suspenseCount).toBe(2);
+    }
+  );
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "cache-and-network",
+  ])(
+    'responds to cache updates in strict mode while using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const { query, mocks } = useSimpleQueryCase();
+
+      const client = new ApolloClient({
+        cache: new InMemoryCache(),
+        link: new MockLink(mocks),
+      });
+
+      const { result } = renderSuspenseHook(
+        () => useSuspenseQuery(query, { fetchPolicy }),
+        { strictMode: true, client }
+      );
+
+      await waitFor(() => {
+        expect(result.current.data).toEqual(mocks[0].result.data);
+      });
+
+      client.writeQuery({
+        query,
+        data: { greeting: "Updated hello" },
+      });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: { greeting: "Updated hello" },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+    }
+  );
+
+  // https://github.com/apollographql/apollo-client/issues/10478
+  it("responds to cache updates when data is already in the cache while using a cache-first fetch policy", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+
+    client.writeQuery({
+      query,
+      data: { greeting: "Hello from cache" },
+    });
+
+    const { result } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }),
+      { client }
+    );
+
+    expect(result.current.data).toEqual({ greeting: "Hello from cache" });
+
+    // Allow time for the subscription in the hook to set itself up since it is
+    // wrapped in a setTimeout (to handle Strict mode bugs). Without this
+    // `wait`, `subscribe` isn't called until after our test updates the cache
+    // via `writeQuery`, which then emits the most recent result, which is the
+    // updated value.
+    await wait(0);
+
+    client.writeQuery({
+      query,
+      data: { greeting: "Updated hello" },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Updated hello" },
+        error: undefined,
+      });
+    });
+  });
+
+  it("uses the default fetch policy from the client when none provided in options", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const client = new ApolloClient({
+      cache,
+      link: new MockLink(mocks),
+      defaultOptions: {
+        watchQuery: {
+          fetchPolicy: "network-only",
+        },
+      },
+    });
+
+    client.writeQuery({ query, data: { greeting: "hello from cache" } });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mocks[0].result.data);
+    });
+
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("uses default variables from the client when none provided in options", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+      defaultOptions: {
+        watchQuery: {
+          variables: { id: "2" },
+        },
+      },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("uses default variables from the client when none provided in options in strict mode", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+      defaultOptions: {
+        watchQuery: {
+          variables: { id: "2" },
+        },
+      },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { strictMode: true, client }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    // React double invokes the render function in strict mode so we expect 2
+    // frames to be rendered here.
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("merges global default variables with local variables", async () => {
+    const query = gql`
+      query MergedVariablesQuery {
+        vars
+      }
+    `;
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new ApolloLink((operation) => {
+        return new Observable((observer) => {
+          observer.next({ data: { vars: operation.variables } });
+          observer.complete();
+        });
+      }),
+      defaultOptions: {
+        watchQuery: {
+          variables: { source: "global", globalOnlyVar: true },
+        },
+      },
+    });
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ source }) =>
+        useSuspenseQuery(query, {
+          fetchPolicy: "network-only",
+          variables: { source, localOnlyVar: true },
+        }),
+      { client, initialProps: { source: "local" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          vars: { source: "local", globalOnlyVar: true, localOnlyVar: true },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ source: "rerender" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          vars: { source: "rerender", globalOnlyVar: true, localOnlyVar: true },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          vars: { source: "local", globalOnlyVar: true, localOnlyVar: true },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          vars: { source: "rerender", globalOnlyVar: true, localOnlyVar: true },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("can unset a globally defined variable", async () => {
+    const query: TypedDocumentNode<{ vars: Record<string, any> }> = gql`
+      query MergedVariablesQuery {
+        vars
+      }
+    `;
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new ApolloLink((operation) => {
+        return new Observable((observer) => {
+          observer.next({ data: { vars: operation.variables } });
+          observer.complete();
+        });
+      }),
+      defaultOptions: {
+        watchQuery: {
+          variables: { source: "global", globalOnlyVar: true },
+        },
+      },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          variables: { source: "local", globalOnlyVar: undefined },
+        }),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { vars: { source: "local" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    // Check to make sure the property itself is not defined, not just set to
+    // undefined. Unfortunately this is not caught by toMatchObject as
+    // toMatchObject only checks a if the subset of options are equal, not if
+    // they have strictly the same keys and values.
+    expect(result.current.data.vars).not.toHaveProperty("globalOnlyVar");
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: { vars: { source: "local" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("passes context to the link", async () => {
+    const query = gql`
+      query ContextQuery {
+        context
+      }
+    `;
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new ApolloLink((operation) => {
+        return new Observable((observer) => {
+          const { valueA, valueB } = operation.getContext();
+
+          observer.next({ data: { context: { valueA, valueB } } });
+          observer.complete();
+        });
+      }),
+    });
+
+    const { result } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          context: { valueA: "A", valueB: "B" },
+        }),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { context: { valueA: "A", valueB: "B" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+  });
+
+  it("throws network errors by default", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const { query, mocks } = useErrorCase({
+      networkError: new Error("Could not fetch"),
+    });
+
+    const { renders } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      mocks,
+    });
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toEqual(new Error("Could not fetch"));
+    expect(error.graphQLErrors).toEqual([]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it("throws graphql errors by default", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const { query, mocks } = useErrorCase({
+      graphQLErrors: [new GraphQLError("`id` should not be null")],
+    });
+
+    const { renders } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      mocks,
+    });
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toBeNull();
+    expect(error.graphQLErrors).toEqual([
+      new GraphQLError("`id` should not be null"),
+    ]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it("tears down subscription when throwing an error", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const { query, mocks } = useErrorCase({
+      networkError: new Error("Could not fetch"),
+    });
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+
+    const { renders } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      client,
+    });
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(client.getObservableQueries().size).toBe(0);
+
+    consoleSpy.mockRestore();
+  });
+
+  it("tears down subscription when throwing an error on refetch", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          errors: [new GraphQLError("Something went wrong")],
+        },
+      },
+    ];
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { id: "1" } }),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(client.getObservableQueries().size).toBe(0);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('throws network errors when errorPolicy is set to "none"', async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const { query, mocks } = useErrorCase({
+      networkError: new Error("Could not fetch"),
+    });
+
+    const { renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "none" }),
+      { mocks }
+    );
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toEqual(new Error("Could not fetch"));
+    expect(error.graphQLErrors).toEqual([]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('throws graphql errors when errorPolicy is set to "none"', async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const { query, mocks } = useErrorCase({
+      graphQLErrors: [new GraphQLError("`id` should not be null")],
+    });
+
+    const { renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "none" }),
+      { mocks }
+    );
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toBeNull();
+    expect(error.graphQLErrors).toEqual([
+      new GraphQLError("`id` should not be null"),
+    ]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('handles multiple graphql errors when errorPolicy is set to "none"', async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const graphQLErrors = [
+      new GraphQLError("Fool me once"),
+      new GraphQLError("Fool me twice"),
+    ];
+
+    const { query, mocks } = useErrorCase({ graphQLErrors });
+
+    const { renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "none" }),
+      { mocks }
+    );
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error!.networkError).toBeNull();
+    expect(error!.graphQLErrors).toEqual(graphQLErrors);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('throws network errors when errorPolicy is set to "ignore"', async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+    const networkError = new Error("Could not fetch");
+
+    const { query, mocks } = useErrorCase({ networkError });
+
+    const { renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "ignore" }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(renders.errorCount).toBe(1);
+    });
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error!.networkError).toEqual(networkError);
+    expect(error!.graphQLErrors).toEqual([]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('does not throw or return graphql errors when errorPolicy is set to "ignore"', async () => {
+    const { query, mocks } = useErrorCase({
+      graphQLErrors: [new GraphQLError("`id` should not be null")],
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "ignore" }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: undefined,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.errorCount).toBe(0);
+    expect(renders.errors).toEqual([]);
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      { data: undefined, networkStatus: NetworkStatus.ready, error: undefined },
+    ]);
+  });
+
+  it('returns partial data results and discards GraphQL errors when errorPolicy is set to "ignore"', async () => {
+    const { query, mocks } = useErrorCase({
+      data: { currentUser: { id: "1", name: null } },
+      graphQLErrors: [new GraphQLError("`name` could not be found")],
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "ignore" }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { currentUser: { id: "1", name: null } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: { currentUser: { id: "1", name: null } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('discards multiple graphql errors when errorPolicy is set to "ignore"', async () => {
+    const { query, mocks } = useErrorCase({
+      graphQLErrors: [
+        new GraphQLError("Fool me once"),
+        new GraphQLError("Fool me twice"),
+      ],
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "ignore" }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: undefined,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      { data: undefined, networkStatus: NetworkStatus.ready, error: undefined },
+    ]);
+  });
+
+  it('responds to cache updates and clears errors after an error returns when errorPolicy is set to "ignore"', async () => {
+    const graphQLError = new GraphQLError("`id` should not be null");
+
+    const { query, mocks } = useErrorCase({ graphQLErrors: [graphQLError] });
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "ignore" }),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: undefined,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    client.writeQuery({
+      query,
+      data: {
+        currentUser: {
+          id: "1",
+          name: "Cache User",
+        },
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          currentUser: {
+            id: "1",
+            name: "Cache User",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.frames).toMatchObject([
+      {
+        data: undefined,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { currentUser: { id: "1", name: "Cache User" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('throws network errors when errorPolicy is set to "all"', async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const networkError = new Error("Could not fetch");
+
+    const { query, mocks } = useErrorCase({ networkError });
+
+    const { renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(renders.errorCount).toBe(1);
+    });
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error!.networkError).toEqual(networkError);
+    expect(error!.graphQLErrors).toEqual([]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('does not throw and returns graphql errors when errorPolicy is set to "all"', async () => {
+    const graphQLError = new GraphQLError("`id` should not be null");
+
+    const { query, mocks } = useErrorCase({ graphQLErrors: [graphQLError] });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: undefined,
+        error: new ApolloError({ graphQLErrors: [graphQLError] }),
+      });
+    });
+
+    expect(renders.errorCount).toBe(0);
+    expect(renders.errors).toEqual([]);
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: undefined,
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({ graphQLErrors: [graphQLError] }),
+      },
+    ]);
+
+    const { error } = result.current;
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error!.networkError).toBeNull();
+    expect(error!.graphQLErrors).toEqual([graphQLError]);
+  });
+
+  it('responds to cache updates and clears errors after an error returns when errorPolicy is set to "all"', async () => {
+    const graphQLError = new GraphQLError("`id` should not be null");
+
+    const { query, mocks } = useErrorCase({ graphQLErrors: [graphQLError] });
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { client }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: undefined,
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({ graphQLErrors: [graphQLError] }),
+      });
+    });
+
+    client.writeQuery({
+      query,
+      data: {
+        currentUser: {
+          id: "1",
+          name: "Cache User",
+        },
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          currentUser: {
+            id: "1",
+            name: "Cache User",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.frames).toMatchObject([
+      {
+        data: undefined,
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({ graphQLErrors: [graphQLError] }),
+      },
+      {
+        data: { currentUser: { id: "1", name: "Cache User" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('handles multiple graphql errors when errorPolicy is set to "all"', async () => {
+    const graphQLErrors = [
+      new GraphQLError("Fool me once"),
+      new GraphQLError("Fool me twice"),
+    ];
+
+    const { query, mocks } = useErrorCase({ graphQLErrors });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { mocks }
+    );
+
+    const expectedError = new ApolloError({ graphQLErrors });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: undefined,
+        error: expectedError,
+      });
+    });
+
+    expect(renders.errorCount).toBe(0);
+    expect(renders.errors).toEqual([]);
+    expect(renders.count).toBe(2);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: undefined,
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      },
+    ]);
+
+    const { error } = result.current;
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error!.networkError).toBeNull();
+    expect(error!.graphQLErrors).toEqual(graphQLErrors);
+  });
+
+  it('returns partial data and keeps errors when errorPolicy is set to "all"', async () => {
+    const graphQLError = new GraphQLError("`name` could not be found");
+
+    const { query, mocks } = useErrorCase({
+      data: { currentUser: { id: "1", name: null } },
+      graphQLErrors: [graphQLError],
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { mocks }
+    );
+
+    const expectedError = new ApolloError({ graphQLErrors: [graphQLError] });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { currentUser: { id: "1", name: null } },
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: { currentUser: { id: "1", name: null } },
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      },
+    ]);
+  });
+
+  it('persists errors between rerenders when errorPolicy is set to "all"', async () => {
+    const graphQLError = new GraphQLError("`name` could not be found");
+
+    const { query, mocks } = useErrorCase({
+      graphQLErrors: [graphQLError],
+    });
+
+    const { result, rerender } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { mocks }
+    );
+
+    const expectedError = new ApolloError({ graphQLErrors: [graphQLError] });
+
+    await waitFor(() => {
+      expect(result.current.error).toEqual(expectedError);
+    });
+
+    rerender();
+
+    expect(result.current.error).toEqual(expectedError);
+  });
+
+  it('clears errors when changing variables and errorPolicy is set to "all"', async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const graphQLErrors = [new GraphQLError("Could not fetch user 1")];
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          errors: graphQLErrors,
+        },
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: { user: { id: "2", name: "Captain Marvel" } },
+        },
+      },
+    ];
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ id }) =>
+        useSuspenseQuery(query, { errorPolicy: "all", variables: { id } }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    const expectedError = new ApolloError({ graphQLErrors });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: undefined,
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      });
+    });
+
+    rerender({ id: "2" });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.errorCount).toBe(0);
+    expect(renders.errors).toEqual([]);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: undefined,
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("re-suspends when calling `refetch`", async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel (updated)" } },
+        },
+      },
+    ];
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { id: "1" } }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("properly resolves `refetch` when returning a result that is deeply equal to data in the cache", async () => {
+    type Variables = {
+      id: string;
+    };
+    interface Data {
+      todo: {
+        id: string;
+        name: string;
+        completed: boolean;
+      };
+    }
+    const user = userEvent.setup();
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query TodoItemQuery($id: ID!) {
+        todo(id: $id) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+
+    const mocks: MockedResponse<Data, Variables>[] = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Todo id="1" />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Todo({ id }: { id: string }) {
+      const { data, refetch } = useSuspenseQuery(query, {
+        variables: { id },
+      });
+
+      const { todo } = data;
+
+      return (
+        <div>
+          <button onClick={() => refetch()}>Refetch</button>
+          <div data-testid="todo">
+            {todo.name}
+            {todo.completed && " (completed)"}
+          </div>
+        </div>
+      );
+    }
+
+    render(<App />);
+
+    expect(await screen.findByText("Loading")).toBeInTheDocument();
+
+    const todo = await screen.findByTestId("todo");
+
+    expect(todo).toHaveTextContent("Clean room");
+
+    await act(() => user.click(screen.getByText("Refetch")));
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    await waitFor(() => {
+      // Suspense will hide the component until the suspense boundary has
+      // finished loading so it is still in the DOM.
+      expect(todo).toBeVisible();
+    });
+
+    expect(todo).toHaveTextContent("Clean room");
+  });
+
+  it("re-suspends when calling `refetch` with new variables", async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: { user: { id: "2", name: "Captain America" } },
+        },
+      },
+    ];
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { id: "1" } }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch({ id: "2" });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("re-suspends multiple times when calling `refetch` multiple times", async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel (updated)" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel (updated again)" } },
+        },
+      },
+    ];
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { id: "1" } }),
+      { mocks, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(6);
+    expect(renders.suspenseCount).toBe(3);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("throws errors when errors are returned after calling `refetch`", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          errors: [new GraphQLError("Something went wrong")],
+        },
+      },
+    ];
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { id: "1" } }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(renders.errorCount).toBe(1);
+    });
+
+    expect(renders.errors).toEqual([
+      new ApolloError({
+        graphQLErrors: [new GraphQLError("Something went wrong")],
+      }),
+    ]);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          errors: [new GraphQLError("Something went wrong")],
+        },
+      },
+    ];
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          errorPolicy: "ignore",
+          variables: { id: "1" },
+        }),
+      { mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    await act(async () => {
+      await result.current.refetch();
+    });
+
+    expect(renders.errorCount).toBe(0);
+    expect(renders.errors).toEqual([]);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          errors: [new GraphQLError("Something went wrong")],
+        },
+      },
+    ];
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          errorPolicy: "all",
+          variables: { id: "1" },
+        }),
+      { mocks }
+    );
+
+    const expectedError = new ApolloError({
+      graphQLErrors: [new GraphQLError("Something went wrong")],
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      });
+    });
+
+    expect(renders.errorCount).toBe(0);
+    expect(renders.errors).toEqual([]);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      },
+    ]);
+  });
+
+  it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: null } },
+          errors: [new GraphQLError("Something went wrong")],
+        },
+      },
+    ];
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          errorPolicy: "all",
+          variables: { id: "1" },
+        }),
+      { mocks }
+    );
+
+    const expectedError = new ApolloError({
+      graphQLErrors: [new GraphQLError("Something went wrong")],
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: mocks[1].result.data,
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      });
+    });
+
+    expect(renders.errorCount).toBe(0);
+    expect(renders.errors).toEqual([]);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: mocks[1].result.data,
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      },
+    ]);
+  });
+
+  it("re-suspends when calling `fetchMore` with different variables", async () => {
+    const { data, query, link } = usePaginatedCase();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { limit: 2 } }),
+      { link }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { letters: data.slice(0, 2) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.fetchMore({ variables: { offset: 2 } });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { letters: data.slice(2, 4) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { letters: data.slice(0, 2) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { letters: data.slice(2, 4) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("properly resolves `fetchMore` when returning a result that is deeply equal to data in the cache", async () => {
+    const { query, link } = usePaginatedCase();
+
+    const user = userEvent.setup();
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Letters offset={0} />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Letters({ offset }: { offset: number }) {
+      const { data, fetchMore } = useSuspenseQuery(query, {
+        variables: { offset },
+      });
+
+      return (
+        <div>
+          <button onClick={() => fetchMore({ variables: { offset } })}>
+            Fetch more
+          </button>
+          <div data-testid="letters">
+            {data.letters.map(({ letter }) => letter).join("")}
+          </div>
+        </div>
+      );
+    }
+
+    render(<App />);
+
+    expect(await screen.findByText("Loading")).toBeInTheDocument();
+
+    const letters = await screen.findByTestId("letters");
+
+    expect(letters).toHaveTextContent("AB");
+
+    await act(() => user.click(screen.getByText("Fetch more")));
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    await waitFor(() => {
+      // Suspense will hide the component until the suspense boundary has
+      // finished loading so it is still in the DOM.
+      expect(letters).toBeVisible();
+    });
+
+    expect(letters).toHaveTextContent("AB");
+  });
+
+  it("suspends when refetching after returning cached data for the initial fetch", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: { greeting: "hello from cache" },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { cache, mocks }
+    );
+
+    expect(result.current).toMatchObject({
+      data: { greeting: "hello from cache" },
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: "hello from cache" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { greeting: "Hello" },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("properly uses `updateQuery` when calling `fetchMore`", async () => {
+    const { data, query, link } = usePaginatedCase();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { limit: 2 } }),
+      { link }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { letters: data.slice(0, 2) },
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.fetchMore({
+        variables: { offset: 2 },
+        updateQuery: (prev, { fetchMoreResult }) => ({
+          letters: prev.letters.concat(fetchMoreResult.letters),
+        }),
+      });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { letters: data.slice(0, 4) },
+        error: undefined,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: { letters: data.slice(0, 2) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { letters: data.slice(0, 4) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => {
+    const { data, query, link } = usePaginatedCase();
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            letters: concatPagination(),
+          },
+        },
+      },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { limit: 2 } }),
+      { cache, link }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { letters: data.slice(0, 2) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.fetchMore({ variables: { offset: 2 } });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { letters: data.slice(0, 4) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: { letters: data.slice(0, 2) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { letters: data.slice(0, 4) },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('honors refetchWritePolicy set to "overwrite"', async () => {
+    const query: TypedDocumentNode<
+      { primes: number[] },
+      { min: number; max: number }
+    > = gql`
+      query GetPrimes($min: number, $max: number) {
+        primes(min: $min, max: $max)
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { min: 0, max: 12 } },
+        result: { data: { primes: [2, 3, 5, 7, 11] } },
+      },
+      {
+        request: { query, variables: { min: 12, max: 30 } },
+        result: { data: { primes: [13, 17, 19, 23, 29] } },
+        delay: 10,
+      },
+    ];
+
+    const mergeParams: [number[] | undefined, number[]][] = [];
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            primes: {
+              keyArgs: false,
+              merge(existing: number[] | undefined, incoming: number[]) {
+                mergeParams.push([existing, incoming]);
+                return existing ? existing.concat(incoming) : incoming;
+              },
+            },
+          },
+        },
+      },
+    });
+
+    const { result } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          variables: { min: 0, max: 12 },
+          refetchWritePolicy: "overwrite",
+        }),
+      { cache, mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
+
+    act(() => {
+      result.current.refetch({ min: 12, max: 30 });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([
+      [undefined, [2, 3, 5, 7, 11]],
+      [undefined, [13, 17, 19, 23, 29]],
+    ]);
+  });
+
+  it('honors refetchWritePolicy set to "merge"', async () => {
+    const query: TypedDocumentNode<
+      { primes: number[] },
+      { min: number; max: number }
+    > = gql`
+      query GetPrimes($min: number, $max: number) {
+        primes(min: $min, max: $max)
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { min: 0, max: 12 } },
+        result: { data: { primes: [2, 3, 5, 7, 11] } },
+      },
+      {
+        request: { query, variables: { min: 12, max: 30 } },
+        result: { data: { primes: [13, 17, 19, 23, 29] } },
+        delay: 10,
+      },
+    ];
+
+    const mergeParams: [number[] | undefined, number[]][] = [];
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            primes: {
+              keyArgs: false,
+              merge(existing: number[] | undefined, incoming: number[]) {
+                mergeParams.push([existing, incoming]);
+                return existing ? existing.concat(incoming) : incoming;
+              },
+            },
+          },
+        },
+      },
+    });
+
+    const { result } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          variables: { min: 0, max: 12 },
+          refetchWritePolicy: "merge",
+        }),
+      { cache, mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
+
+    act(() => {
+      result.current.refetch({ min: 12, max: 30 });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([
+      [undefined, [2, 3, 5, 7, 11]],
+      [
+        [2, 3, 5, 7, 11],
+        [13, 17, 19, 23, 29],
+      ],
+    ]);
+  });
+
+  it('defaults refetchWritePolicy to "overwrite"', async () => {
+    const query: TypedDocumentNode<
+      { primes: number[] },
+      { min: number; max: number }
+    > = gql`
+      query GetPrimes($min: number, $max: number) {
+        primes(min: $min, max: $max)
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { min: 0, max: 12 } },
+        result: { data: { primes: [2, 3, 5, 7, 11] } },
+      },
+      {
+        request: { query, variables: { min: 12, max: 30 } },
+        result: { data: { primes: [13, 17, 19, 23, 29] } },
+        delay: 10,
+      },
+    ];
+
+    const mergeParams: [number[] | undefined, number[]][] = [];
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            primes: {
+              keyArgs: false,
+              merge(existing: number[] | undefined, incoming: number[]) {
+                mergeParams.push([existing, incoming]);
+                return existing ? existing.concat(incoming) : incoming;
+              },
+            },
+          },
+        },
+      },
+    });
+
+    const { result } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { min: 0, max: 12 } }),
+      { cache, mocks }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
+
+    act(() => {
+      result.current.refetch({ min: 12, max: 30 });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([
+      [undefined, [2, 3, 5, 7, 11]],
+      [undefined, [13, 17, 19, 23, 29]],
+    ]);
+  });
+
+  it("does not suspend when `skip` is true", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { skip: true }),
+      { cache, mocks }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: undefined,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+  });
+
+  it("does not suspend when using `skipToken` token as options", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, skipToken),
+      { cache, mocks }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: undefined,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+  });
+
+  it("suspends when `skip` becomes `false` after it was `true`", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ skip }) => useSuspenseQuery(query, { skip }),
+      { cache, mocks, initialProps: { skip: true } }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: undefined,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    rerender({ skip: false });
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      { data: undefined, networkStatus: NetworkStatus.ready, error: undefined },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends when switching away from `skipToken` in options", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ skip }) => useSuspenseQuery(query, skip ? skipToken : void 0),
+      { mocks, initialProps: { skip: true } }
+    );
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(result.current).toMatchObject({
+      data: undefined,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    rerender({ skip: false });
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      { data: undefined, networkStatus: NetworkStatus.ready, error: undefined },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("renders skip result, does not suspend, and maintains `data` when `skip` becomes `true` after it was `false`", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ skip }) => useSuspenseQuery(query, { skip }),
+      { cache, mocks, initialProps: { skip: false } }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ skip: true });
+
+    expect(renders.suspenseCount).toBe(1);
+
+    expect(result.current).toMatchObject({
+      ...mocks[0].result,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("renders skip result, does not suspend, and maintains `data` when skipping a query with `skipToken` as options after it was enabled", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const cache = new InMemoryCache();
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ skip }) => useSuspenseQuery(query, skip ? skipToken : void 0),
+      { cache, mocks, initialProps: { skip: false } }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ skip: true });
+
+    expect(renders.suspenseCount).toBe(1);
+
+    expect(result.current).toMatchObject({
+      ...mocks[0].result,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("does not make network requests when `skip` is `true`", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    let fetchCount = 0;
+
+    const link = new ApolloLink((operation) => {
+      return new Observable((observer) => {
+        fetchCount++;
+
+        const mock = mocks.find(({ request }) =>
+          equal(request.variables, operation.variables)
+        );
+
+        if (!mock) {
+          throw new Error("Could not find mock for operation");
+        }
+
+        observer.next(mock.result);
+        observer.complete();
+      });
+    });
+
+    const { result, rerender } = renderSuspenseHook(
+      ({ skip, id }) => useSuspenseQuery(query, { skip, variables: { id } }),
+      { mocks, link, initialProps: { skip: true, id: "1" } }
+    );
+
+    expect(fetchCount).toBe(0);
+
+    rerender({ skip: false, id: "1" });
+
+    expect(fetchCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ skip: true, id: "2" });
+
+    expect(fetchCount).toBe(1);
+  });
+
+  it("does not make network requests when using `skipToken` for options", async () => {
+    const { query, mocks } = useVariablesQueryCase();
+
+    let fetchCount = 0;
+
+    const link = new ApolloLink((operation) => {
+      return new Observable((observer) => {
+        fetchCount++;
+
+        const mock = mocks.find(({ request }) =>
+          equal(request.variables, operation.variables)
+        );
+
+        if (!mock) {
+          throw new Error("Could not find mock for operation");
+        }
+
+        observer.next(mock.result);
+        observer.complete();
+      });
+    });
+
+    const { result, rerender } = renderSuspenseHook(
+      ({ skip, id }) =>
+        useSuspenseQuery(query, skip ? skipToken : { variables: { id } }),
+      { mocks, link, initialProps: { skip: true, id: "1" } }
+    );
+
+    expect(fetchCount).toBe(0);
+
+    rerender({ skip: false, id: "1" });
+
+    expect(fetchCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ skip: true, id: "2" });
+
+    expect(fetchCount).toBe(1);
+  });
+
+  it("`skip` result is referentially stable", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const { result, rerender } = renderSuspenseHook(
+      ({ skip }) => useSuspenseQuery(query, { skip }),
+      { mocks, initialProps: { skip: true } }
+    );
+
+    const skipResult = result.current;
+
+    rerender({ skip: true });
+
+    expect(result.current).toBe(skipResult);
+
+    rerender({ skip: false });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mocks[0].result.data);
+    });
+
+    const fetchedSkipResult = result.current;
+
+    rerender({ skip: false });
+
+    expect(fetchedSkipResult).toBe(fetchedSkipResult);
+  });
+
+  it("`skip` result is referentially stable when using `skipToken` as options", async () => {
+    const { query, mocks } = useSimpleQueryCase();
+
+    const { result, rerender } = renderSuspenseHook(
+      ({ skip }) => useSuspenseQuery(query, skip ? skipToken : void 0),
+      { mocks, initialProps: { skip: true } }
+    );
+
+    const skipResult = result.current;
+
+    rerender({ skip: true });
+
+    expect(result.current).toBe(skipResult);
+
+    rerender({ skip: false });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mocks[0].result.data);
+    });
+
+    const fetchedSkipResult = result.current;
+
+    rerender({ skip: false });
+
+    expect(fetchedSkipResult).toBe(fetchedSkipResult);
+  });
+
+  it("properly resolves when `skip` becomes false when returning a result that is deeply equal to data in the cache", async () => {
+    type Variables = {
+      id: string;
+    };
+    interface Data {
+      todo: {
+        id: string;
+        name: string;
+        completed: boolean;
+      };
+    }
+    const user = userEvent.setup();
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query TodoItemQuery($id: ID!) {
+        todo(id: $id) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+
+    const mocks: MockedResponse<Data, Variables>[] = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Todo id="1" />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Todo({ id }: { id: string }) {
+      const [skip, setSkip] = React.useState(false);
+      const { data } = useSuspenseQuery(query, {
+        // Force a network request that returns the same data from the cache
+        fetchPolicy: "network-only",
+        skip,
+        variables: { id },
+      });
+
+      const todo = data?.todo;
+
+      return (
+        <>
+          <button onClick={() => setSkip((skip) => !skip)}>Toggle skip</button>
+          {todo && (
+            <div data-testid="todo">
+              {todo.name}
+              {todo.completed && " (completed)"}
+            </div>
+          )}
+        </>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    const todo = await screen.findByTestId("todo");
+    expect(todo).toHaveTextContent("Clean room");
+
+    // skip false -> true
+    await act(() => user.click(screen.getByText("Toggle skip")));
+    expect(todo).toHaveTextContent("Clean room");
+
+    // skip true -> false
+    await act(() => user.click(screen.getByText("Toggle skip")));
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(todo).toBeVisible();
+    });
+
+    expect(todo).toHaveTextContent("Clean room");
+  });
+
+  it("`skip` option works with `startTransition`", async () => {
+    type Variables = {
+      id: string;
+    };
+    interface Data {
+      todo: {
+        id: string;
+        name: string;
+        completed: boolean;
+      };
+    }
+    const user = userEvent.setup();
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query TodoItemQuery($id: ID!) {
+        todo(id: $id) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+    const mocks: MockedResponse<Data, Variables>[] = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      const [id, setId] = React.useState<string | null>(null);
+      const [isPending, startTransition] = React.useTransition();
+
+      return (
+        <ApolloProvider client={client}>
+          <button
+            disabled={isPending}
+            onClick={() => {
+              startTransition(() => {
+                setId("1");
+              });
+            }}
+          >
+            Fetch to-do 1
+          </button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Todo id={id} />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Todo({ id }: { id: string | null }) {
+      const { data } = useSuspenseQuery(query, {
+        skip: !id,
+        variables: { id: id ?? "0" },
+      });
+
+      const todo = data?.todo;
+
+      return todo ? (
+        <div data-testid="todo">
+          {todo.name}
+          {todo.completed && " (completed)"}
+        </div>
+      ) : null;
+    }
+
+    render(<App />);
+
+    expect(screen.queryByTestId("todo")).not.toBeInTheDocument();
+
+    const button = screen.getByText("Fetch to-do 1");
+    await act(() => user.click(button));
+    // startTransition will avoid rendering the suspense fallback for already
+    // revealed content if the state update inside the transition causes the
+    // component to suspend.
+    //
+    // Here we should not see the suspense fallback while the component suspends
+    // until the todo is finished loading. Seeing the suspense fallback is an
+    // indication that we are suspending the component too late in the process.
+    expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+    // We can ensure this works with isPending from useTransition in the process
+    expect(button).toBeDisabled();
+    // Eventually we should see the updated todo content once its done
+    // suspending.
+    expect(await screen.findByTestId("todo")).toHaveTextContent("Clean room");
+  });
+
+  it("`skipToken` works with `startTransition` when used for options", async () => {
+    type Variables = {
+      id: string;
+    };
+    interface Data {
+      todo: {
+        id: string;
+        name: string;
+        completed: boolean;
+      };
+    }
+    const user = userEvent.setup();
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query TodoItemQuery($id: ID!) {
+        todo(id: $id) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+    const mocks: MockedResponse<Data, Variables>[] = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      const [id, setId] = React.useState<string | null>(null);
+      const [isPending, startTransition] = React.useTransition();
+
+      return (
+        <ApolloProvider client={client}>
+          <button
+            disabled={isPending}
+            onClick={() => {
+              startTransition(() => {
+                setId("1");
+              });
+            }}
+          >
+            Fetch to-do 1
+          </button>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Todo id={id} />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Todo({ id }: { id: string | null }) {
+      const { data } = useSuspenseQuery(
+        query,
+        id ? { variables: { id } } : skipToken
+      );
+
+      const todo = data?.todo;
+
+      return todo ? (
+        <div data-testid="todo">
+          {todo.name}
+          {todo.completed && " (completed)"}
+        </div>
+      ) : null;
+    }
+
+    render(<App />);
+
+    expect(screen.queryByTestId("todo")).not.toBeInTheDocument();
+
+    const button = screen.getByText("Fetch to-do 1");
+    await act(() => user.click(button));
+    // startTransition will avoid rendering the suspense fallback for already
+    // revealed content if the state update inside the transition causes the
+    // component to suspend.
+    //
+    // Here we should not see the suspense fallback while the component suspends
+    // until the todo is finished loading. Seeing the suspense fallback is an
+    // indication that we are suspending the component too late in the process.
+    expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+    // We can ensure this works with isPending from useTransition in the process
+    expect(button).toBeDisabled();
+    // Eventually we should see the updated todo content once its done
+    // suspending.
+    expect(await screen.findByTestId("todo")).toHaveTextContent("Clean room");
+  });
+
+  it("applies `errorPolicy` on next fetch when it changes between renders", async () => {
+    const { query, mocks: simpleMocks } = useSimpleQueryCase();
+
+    const successMock = simpleMocks[0];
+
+    const mocks = [
+      successMock,
+      {
+        request: { query },
+        result: {
+          errors: [new GraphQLError("oops")],
+        },
+      },
+    ];
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ errorPolicy }) => useSuspenseQuery(query, { errorPolicy }),
+      { mocks, initialProps: { errorPolicy: "none" as ErrorPolicy } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...successMock.result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ errorPolicy: "all" });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...successMock.result,
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }),
+      });
+    });
+
+    expect(renders.errorCount).toBe(0);
+    expect(
+      renders.frames.map((f) => ({
+        data: f.data,
+        error: f.error,
+        networkStatus: f.networkStatus,
+      }))
+    ).toMatchObject([
+      {
+        ...successMock.result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...successMock.result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...successMock.result,
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }),
+      },
+    ]);
+  });
+
+  it("applies `context` on next fetch when it changes between renders", async () => {
+    const query = gql`
+      query {
+        context
+      }
+    `;
+
+    const link = new ApolloLink((operation) => {
+      return Observable.of({
+        data: {
+          context: operation.getContext(),
+        },
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ context }) => useSuspenseQuery(query, { context }),
+      { client, initialProps: { context: { phase: "initialValue" } } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { context: { phase: "initialValue" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ context: { phase: "rerender" } });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          context: { phase: "rerender" },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.frames).toMatchObject([
+      {
+        data: { context: { phase: "initialValue" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { context: { phase: "initialValue" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { context: { phase: "rerender" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  // NOTE: We only test the `false` -> `true` path here. If the option changes
+  // from `true` -> `false`, the data has already been canonized, so it has no
+  // effect on the output.
+  it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => {
+    interface Result {
+      __typename: string;
+      value: number;
+    }
+
+    interface Data {
+      results: Result[];
+    }
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Result: {
+          keyFields: false,
+        },
+      },
+    });
+
+    const query: TypedDocumentNode<Data> = gql`
+      query {
+        results {
+          value
+        }
+      }
+    `;
+
+    const results: Result[] = [
+      { __typename: "Result", value: 0 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 1 },
+      { __typename: "Result", value: 2 },
+      { __typename: "Result", value: 3 },
+      { __typename: "Result", value: 5 },
+    ];
+
+    cache.writeQuery({
+      query,
+      data: { results },
+    });
+
+    function verifyCanonicalResults(data: Data, canonized: boolean) {
+      const resultSet = new Set(data.results);
+      const values = Array.from(resultSet).map((item) => item.value);
+
+      expect(data).toEqual({ results });
+
+      if (canonized) {
+        expect(data.results.length).toBe(6);
+        expect(resultSet.size).toBe(5);
+        expect(values).toEqual([0, 1, 2, 3, 5]);
+      } else {
+        expect(data.results.length).toBe(6);
+        expect(resultSet.size).toBe(6);
+        expect(values).toEqual([0, 1, 1, 2, 3, 5]);
+      }
+    }
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ canonizeResults }) => useSuspenseQuery(query, { canonizeResults }),
+      { cache, initialProps: { canonizeResults: false } }
+    );
+
+    verifyCanonicalResults(result.current.data, false);
+
+    rerender({ canonizeResults: true });
+
+    verifyCanonicalResults(result.current.data, true);
+    expect(renders.count).toBe(2);
+  });
+
+  it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => {
+    const query: TypedDocumentNode<
+      { primes: number[] },
+      { min: number; max: number }
+    > = gql`
+      query GetPrimes($min: number, $max: number) {
+        primes(min: $min, max: $max)
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { min: 0, max: 12 } },
+        result: { data: { primes: [2, 3, 5, 7, 11] } },
+      },
+      {
+        request: { query, variables: { min: 12, max: 30 } },
+        result: { data: { primes: [13, 17, 19, 23, 29] } },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { min: 30, max: 50 } },
+        result: { data: { primes: [31, 37, 41, 43, 47] } },
+        delay: 10,
+      },
+    ];
+
+    const mergeParams: [number[] | undefined, number[]][] = [];
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            primes: {
+              keyArgs: false,
+              merge(existing: number[] | undefined, incoming: number[]) {
+                mergeParams.push([existing, incoming]);
+                return existing ? existing.concat(incoming) : incoming;
+              },
+            },
+          },
+        },
+      },
+    });
+
+    const { result, rerender } = renderSuspenseHook(
+      ({ refetchWritePolicy }) =>
+        useSuspenseQuery(query, {
+          variables: { min: 0, max: 12 },
+          refetchWritePolicy,
+        }),
+      {
+        cache,
+        mocks,
+        initialProps: { refetchWritePolicy: "merge" as RefetchWritePolicy },
+      }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]);
+
+    act(() => {
+      result.current.refetch({ min: 12, max: 30 });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([
+      [undefined, [2, 3, 5, 7, 11]],
+      [
+        [2, 3, 5, 7, 11],
+        [13, 17, 19, 23, 29],
+      ],
+    ]);
+
+    rerender({ refetchWritePolicy: "overwrite" });
+
+    act(() => {
+      result.current.refetch({ min: 30, max: 50 });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(mergeParams).toEqual([
+      [undefined, [2, 3, 5, 7, 11]],
+      [
+        [2, 3, 5, 7, 11],
+        [13, 17, 19, 23, 29],
+      ],
+      [undefined, [31, 37, 41, 43, 47]],
+    ]);
+  });
+
+  it("applies `returnPartialData` on next fetch when it changes between renders", async () => {
+    const fullQuery = gql`
+      query {
+        character {
+          __typename
+          id
+          name
+        }
+      }
+    `;
+
+    const partialQuery = gql`
+      query {
+        character {
+          __typename
+          id
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query: fullQuery },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Doctor Strange",
+            },
+          },
+        },
+      },
+      {
+        request: { query: fullQuery },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Doctor Strange (refetched)",
+            },
+          },
+        },
+        delay: 100,
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query: partialQuery,
+      data: { character: { __typename: "Character", id: "1" } },
+    });
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ returnPartialData }) =>
+        useSuspenseQuery(fullQuery, { returnPartialData }),
+      { cache, mocks, initialProps: { returnPartialData: false } }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ returnPartialData: true });
+
+    cache.modify({
+      id: cache.identify({ __typename: "Character", id: "1" }),
+      fields: {
+        name: (_, { DELETE }) => DELETE,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual({
+        character: { __typename: "Character", id: "1" },
+      });
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(5);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { character: { __typename: "Character", id: "1" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: { character: { __typename: "Character", id: "1" } },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => {
+    const query = gql`
+      query {
+        character {
+          __typename
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "1",
+              name: "Doctor Strange",
+            },
+          },
+        },
+        delay: 10,
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: {
+        character: {
+          __typename: "Character",
+          id: "1",
+          name: "Doctor Strangecache",
+        },
+      },
+    });
+
+    const { result, /* renders, */ rerender } = renderSuspenseHook(
+      ({ fetchPolicy }) => useSuspenseQuery(query, { fetchPolicy }),
+      {
+        cache,
+        mocks,
+        initialProps: {
+          fetchPolicy: "cache-first" as SuspenseQueryHookFetchPolicy,
+        },
+      }
+    );
+
+    expect(result.current).toMatchObject({
+      data: {
+        character: {
+          __typename: "Character",
+          id: "1",
+          name: "Doctor Strangecache",
+        },
+      },
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    rerender({ fetchPolicy: "no-cache" });
+
+    const cacheKey = cache.identify({ __typename: "Character", id: "1" })!;
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual({
+        character: {
+          __typename: "Character",
+          id: "1",
+          name: "Doctor Strange",
+        },
+      });
+    });
+
+    // Because we switched to a `no-cache` fetch policy, we should not see the
+    // newly fetched data in the cache after the fetch occured.
+    expect(cache.extract()[cacheKey]).toEqual({
+      __typename: "Character",
+      id: "1",
+      name: "Doctor Strangecache",
+    });
+
+    // TODO: Determine why there is an extra render. Unfortunately this is hard
+    // to track down because the test passes if I run only this test or add a
+    // `console.log` statement to the `handleNext` function in `QueryReference`.
+    // expect(renders.count).toBe(4);
+    // expect(renders.suspenseCount).toBe(1);
+    // expect(renders.frames).toMatchObject([
+    //   {
+    //     data: {
+    //       character: {
+    //         __typename: 'Character',
+    //         id: '1',
+    //         name: 'Doctor Strangecache',
+    //       },
+    //     },
+    //     networkStatus: NetworkStatus.ready,
+    //     error: undefined,
+    //   },
+    //   {
+    //     data: {
+    //       character: {
+    //         __typename: 'Character',
+    //         id: '1',
+    //         name: 'Doctor Strangecache',
+    //       },
+    //     },
+    //     networkStatus: NetworkStatus.ready,
+    //     error: undefined,
+    //   },
+    //   {
+    //     ...mocks[0].result,
+    //     networkStatus: NetworkStatus.ready,
+    //     error: undefined,
+    //   },
+    // ]);
+  });
+
+  it("properly handles changing options along with changing `variables`", async () => {
+    const query = gql`
+      query ($id: ID!) {
+        character(id: $id) {
+          __typename
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          errors: [new GraphQLError("oops")],
+        },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: {
+            character: {
+              __typename: "Character",
+              id: "2",
+              name: "Hulk",
+            },
+          },
+        },
+        delay: 10,
+      },
+    ];
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      variables: {
+        id: "1",
+      },
+      data: {
+        character: {
+          __typename: "Character",
+          id: "1",
+          name: "Doctor Strangecache",
+        },
+      },
+    });
+
+    const { result, renders, rerender } = renderSuspenseHook(
+      ({ errorPolicy, variables }) =>
+        useSuspenseQuery(query, { errorPolicy, variables }),
+      {
+        cache,
+        mocks,
+        initialProps: {
+          errorPolicy: "none" as ErrorPolicy,
+          variables: { id: "1" },
+        },
+      }
+    );
+
+    expect(result.current).toMatchObject({
+      data: {
+        character: {
+          __typename: "Character",
+          id: "1",
+          name: "Doctor Strangecache",
+        },
+      },
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    rerender({ errorPolicy: "none", variables: { id: "2" } });
+
+    expect(renders.suspenseCount).toBe(1);
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          character: {
+            __typename: "Character",
+            id: "2",
+            name: "Hulk",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    rerender({ errorPolicy: "all", variables: { id: "1" } });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    const expectedError = new ApolloError({
+      graphQLErrors: [new GraphQLError("oops")],
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          character: {
+            __typename: "Character",
+            id: "1",
+            name: "Doctor Strangecache",
+          },
+        },
+        networkStatus: NetworkStatus.error,
+        error: expectedError,
+      });
+    });
+
+    expect(renders.errorCount).toBe(0);
+  });
+
+  it("does not oversubscribe when suspending multiple times", async () => {
+    const query = gql`
+      query UserQuery($id: String!) {
+        user(id: $id) {
+          id
+          name
+        }
+      }
+    `;
+
+    const mocks = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel (updated)" } },
+        },
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { user: { id: "1", name: "Captain Marvel (updated again)" } },
+        },
+      },
+    ];
+
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: new MockLink(mocks),
+    });
+
+    const { result } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { id: "1" } }),
+      { client, initialProps: { id: "1" } }
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[0].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[1].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    act(() => {
+      result.current.refetch();
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        ...mocks[2].result,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(client.getObservableQueries().size).toBe(1);
+  });
+
+  it("suspends deferred queries until initial chunk loads then streams in data as it loads", async () => {
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { link }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    link.simulateResult({
+      result: {
+        data: { greeting: { message: "Hello world", __typename: "Greeting" } },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: { greeting: { message: "Hello world", __typename: "Greeting" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            data: {
+              recipient: { name: "Alice", __typename: "Person" },
+              __typename: "Greeting",
+            },
+            path: ["greeting"],
+          },
+        ],
+        hasNext: false,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: { greeting: { message: "Hello world", __typename: "Greeting" } },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it.each<SuspenseQueryHookFetchPolicy>([
+    "cache-first",
+    "network-only",
+    "no-cache",
+    "cache-and-network",
+  ])(
+    'suspends deferred queries until initial chunk loads then streams in data as it loads when using a "%s" fetch policy',
+    async (fetchPolicy) => {
+      const query = gql`
+        query {
+          greeting {
+            message
+            ... on Greeting @defer {
+              recipient {
+                name
+              }
+            }
+          }
+        }
+      `;
+
+      const link = new MockSubscriptionLink();
+
+      const { result, renders } = renderSuspenseHook(
+        () => useSuspenseQuery(query, { fetchPolicy }),
+        { link }
+      );
+
+      expect(renders.suspenseCount).toBe(1);
+
+      link.simulateResult({
+        result: {
+          data: {
+            greeting: { message: "Hello world", __typename: "Greeting" },
+          },
+          hasNext: true,
+        },
+      });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: {
+            greeting: { message: "Hello world", __typename: "Greeting" },
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      link.simulateResult({
+        result: {
+          incremental: [
+            {
+              data: {
+                recipient: { name: "Alice", __typename: "Person" },
+                __typename: "Greeting",
+              },
+              path: ["greeting"],
+            },
+          ],
+          hasNext: false,
+        },
+      });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: {
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      expect(renders.count).toBe(3);
+      expect(renders.suspenseCount).toBe(1);
+      expect(renders.frames).toMatchObject([
+        {
+          data: {
+            greeting: { message: "Hello world", __typename: "Greeting" },
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          data: {
+            greeting: {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    }
+  );
+
+  it('does not suspend deferred queries with data in the cache and using a "cache-first" fetch policy', async () => {
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: {
+        greeting: {
+          __typename: "Greeting",
+          message: "Hello world",
+          recipient: { __typename: "Person", name: "Alice" },
+        },
+      },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }),
+      { cache }
+    );
+
+    expect(result.current).toMatchObject({
+      data: {
+        greeting: {
+          message: "Hello world",
+          __typename: "Greeting",
+          recipient: { __typename: "Person", name: "Alice" },
+        },
+      },
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    expect(renders.suspenseCount).toBe(0);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => {
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+    const cache = new InMemoryCache();
+
+    // We are intentionally writing partial data to the cache. Supress console
+    // warnings to avoid unnecessary noise in the test.
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+    cache.writeQuery({
+      query,
+      data: {
+        greeting: {
+          __typename: "Greeting",
+          recipient: { __typename: "Person", name: "Cached Alice" },
+        },
+      },
+    });
+    consoleSpy.mockRestore();
+
+    const { result, renders } = renderSuspenseHook(
+      () =>
+        useSuspenseQuery(query, {
+          fetchPolicy: "cache-first",
+          returnPartialData: true,
+        }),
+      { cache, link }
+    );
+
+    expect(result.current).toMatchObject({
+      data: {
+        greeting: {
+          __typename: "Greeting",
+          recipient: { __typename: "Person", name: "Cached Alice" },
+        },
+      },
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    link.simulateResult({
+      result: {
+        data: { greeting: { message: "Hello world", __typename: "Greeting" } },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            data: {
+              __typename: "Greeting",
+              recipient: { name: "Alice", __typename: "Person" },
+            },
+            path: ["greeting"],
+          },
+        ],
+        hasNext: false,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(0);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => {
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+    const cache = new InMemoryCache();
+    const client = new ApolloClient({ cache, link });
+
+    cache.writeQuery({
+      query,
+      data: {
+        greeting: {
+          __typename: "Greeting",
+          message: "Hello cached",
+          recipient: { __typename: "Person", name: "Cached Alice" },
+        },
+      },
+    });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { fetchPolicy: "cache-and-network" }),
+      { client }
+    );
+
+    expect(result.current).toMatchObject({
+      data: {
+        greeting: {
+          message: "Hello cached",
+          __typename: "Greeting",
+          recipient: { __typename: "Person", name: "Cached Alice" },
+        },
+      },
+      networkStatus: NetworkStatus.loading,
+      error: undefined,
+    });
+
+    link.simulateResult({
+      result: {
+        data: { greeting: { __typename: "Greeting", message: "Hello world" } },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            data: {
+              recipient: { name: "Alice", __typename: "Person" },
+              __typename: "Greeting",
+            },
+            path: ["greeting"],
+          },
+        ],
+        hasNext: false,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(0);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello cached",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.loading,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Cached Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: { __typename: "Person", name: "Alice" },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends deferred queries with lists and properly patches results", async () => {
+    const query = gql`
+      query {
+        greetings {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { link }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    link.simulateResult({
+      result: {
+        data: {
+          greetings: [
+            { __typename: "Greeting", message: "Hello world" },
+            { __typename: "Greeting", message: "Hello again" },
+          ],
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greetings: [
+            { __typename: "Greeting", message: "Hello world" },
+            { __typename: "Greeting", message: "Hello again" },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            data: {
+              __typename: "Greeting",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+            path: ["greetings", 0],
+          },
+        ],
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+            {
+              __typename: "Greeting",
+              message: "Hello again",
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            data: {
+              __typename: "Greeting",
+              recipient: { __typename: "Person", name: "Bob" },
+            },
+            path: ["greetings", 1],
+          },
+        ],
+        hasNext: false,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+            {
+              __typename: "Greeting",
+              message: "Hello again",
+              recipient: { __typename: "Person", name: "Bob" },
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          greetings: [
+            { __typename: "Greeting", message: "Hello world" },
+            { __typename: "Greeting", message: "Hello again" },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+            {
+              __typename: "Greeting",
+              message: "Hello again",
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: { __typename: "Person", name: "Alice" },
+            },
+            {
+              __typename: "Greeting",
+              message: "Hello again",
+              recipient: { __typename: "Person", name: "Bob" },
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("suspends queries with deferred fragments in lists and properly merges arrays", async () => {
+    const query = gql`
+      query DeferVariation {
+        allProducts {
+          delivery {
+            ...MyFragment @defer
+          }
+          sku
+          id
+        }
+      }
+
+      fragment MyFragment on DeliveryEstimates {
+        estimatedDelivery
+        fastestDelivery
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { link }
+    );
+
+    expect(renders.suspenseCount).toBe(1);
+
+    link.simulateResult({
+      result: {
+        data: {
+          allProducts: [
+            {
+              __typename: "Product",
+              delivery: {
+                __typename: "DeliveryEstimates",
+              },
+              id: "apollo-federation",
+              sku: "federation",
+            },
+            {
+              __typename: "Product",
+              delivery: {
+                __typename: "DeliveryEstimates",
+              },
+              id: "apollo-studio",
+              sku: "studio",
+            },
+          ],
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          allProducts: [
+            {
+              __typename: "Product",
+              delivery: {
+                __typename: "DeliveryEstimates",
+              },
+              id: "apollo-federation",
+              sku: "federation",
+            },
+            {
+              __typename: "Product",
+              delivery: {
+                __typename: "DeliveryEstimates",
+              },
+              id: "apollo-studio",
+              sku: "studio",
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        hasNext: true,
+        incremental: [
+          {
+            data: {
+              __typename: "DeliveryEstimates",
+              estimatedDelivery: "6/25/2021",
+              fastestDelivery: "6/24/2021",
+            },
+            path: ["allProducts", 0, "delivery"],
+          },
+          {
+            data: {
+              __typename: "DeliveryEstimates",
+              estimatedDelivery: "6/25/2021",
+              fastestDelivery: "6/24/2021",
+            },
+            path: ["allProducts", 1, "delivery"],
+          },
+        ],
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          allProducts: [
+            {
+              __typename: "Product",
+              delivery: {
+                __typename: "DeliveryEstimates",
+                estimatedDelivery: "6/25/2021",
+                fastestDelivery: "6/24/2021",
+              },
+              id: "apollo-federation",
+              sku: "federation",
+            },
+            {
+              __typename: "Product",
+              delivery: {
+                __typename: "DeliveryEstimates",
+                estimatedDelivery: "6/25/2021",
+                fastestDelivery: "6/24/2021",
+              },
+              id: "apollo-studio",
+              sku: "studio",
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+  });
+
+  it("incrementally rerenders data returned by a `refetch` for a deferred query", async () => {
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache();
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({ link, cache });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { client }
+    );
+
+    link.simulateResult({
+      result: {
+        data: { greeting: { __typename: "Greeting", message: "Hello world" } },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult(
+      {
+        result: {
+          incremental: [
+            {
+              data: {
+                recipient: { name: "Alice", __typename: "Person" },
+              },
+              path: ["greeting"],
+            },
+          ],
+          hasNext: false,
+        },
+      },
+      true
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: {
+              __typename: "Person",
+              name: "Alice",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    let refetchPromise: Promise<ApolloQueryResult<unknown>>;
+    act(() => {
+      refetchPromise = result.current.refetch();
+    });
+
+    link.simulateResult({
+      result: {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Goodbye",
+          },
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Goodbye",
+            recipient: {
+              __typename: "Person",
+              name: "Alice",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult(
+      {
+        result: {
+          incremental: [
+            {
+              data: {
+                recipient: { name: "Bob", __typename: "Person" },
+              },
+              path: ["greeting"],
+            },
+          ],
+          hasNext: false,
+        },
+      },
+      true
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Goodbye",
+            recipient: {
+              __typename: "Person",
+              name: "Bob",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    await expect(refetchPromise!).resolves.toEqual({
+      data: {
+        greeting: {
+          __typename: "Greeting",
+          message: "Goodbye",
+          recipient: {
+            __typename: "Person",
+            name: "Bob",
+          },
+        },
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    expect(renders.count).toBe(6);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: {
+              __typename: "Person",
+              name: "Alice",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Goodbye",
+            recipient: {
+              __typename: "Person",
+              name: "Alice",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Goodbye",
+            recipient: {
+              __typename: "Person",
+              name: "Bob",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("incrementally renders data returned after skipping a deferred query", async () => {
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache();
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({ link, cache });
+
+    const { result, rerender, renders } = renderSuspenseHook(
+      ({ skip }) => useSuspenseQuery(query, { skip }),
+      { client, initialProps: { skip: true } }
+    );
+
+    expect(result.current).toMatchObject({
+      data: undefined,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    rerender({ skip: false });
+
+    expect(renders.suspenseCount).toBe(1);
+
+    link.simulateResult({
+      result: {
+        data: { greeting: { __typename: "Greeting", message: "Hello world" } },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult(
+      {
+        result: {
+          incremental: [
+            {
+              data: {
+                recipient: { name: "Alice", __typename: "Person" },
+              },
+              path: ["greeting"],
+            },
+          ],
+          hasNext: false,
+        },
+      },
+      true
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: {
+              __typename: "Person",
+              name: "Alice",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(4);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      { data: undefined, networkStatus: NetworkStatus.ready, error: undefined },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greeting: {
+            __typename: "Greeting",
+            message: "Hello world",
+            recipient: {
+              __typename: "Person",
+              name: "Alice",
+            },
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  // TODO: This test is a bit of a lie. `fetchMore` should incrementally
+  // rerender when using `@defer` but there is currently a bug in the core
+  // implementation that prevents updates until the final result is returned.
+  // This test reflects the behavior as it exists today, but will need
+  // to be updated once the core bug is fixed.
+  //
+  // NOTE: A duplicate it.failng test has been added right below this one with
+  // the expected behavior added in (i.e. the commented code in this test). Once
+  // the core bug is fixed, this test can be removed in favor of the other test.
+  //
+  // https://github.com/apollographql/apollo-client/issues/11034
+  it("rerenders data returned by `fetchMore` for a deferred query", async () => {
+    const query = gql`
+      query ($offset: Int) {
+        greetings(offset: $offset) {
+          message
+          ... @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            greetings: offsetLimitPagination(),
+          },
+        },
+      },
+    });
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({ link, cache });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { variables: { offset: 0 } }),
+      { client }
+    );
+
+    link.simulateResult({
+      result: {
+        data: {
+          greetings: [{ __typename: "Greeting", message: "Hello world" }],
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult(
+      {
+        result: {
+          incremental: [
+            {
+              data: {
+                recipient: { name: "Alice", __typename: "Person" },
+              },
+              path: ["greetings", 0],
+            },
+          ],
+          hasNext: false,
+        },
+      },
+      true
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: {
+                __typename: "Person",
+                name: "Alice",
+              },
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    let fetchMorePromise: Promise<ApolloQueryResult<unknown>>;
+    act(() => {
+      fetchMorePromise = result.current.fetchMore({ variables: { offset: 1 } });
+    });
+
+    link.simulateResult({
+      result: {
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Goodbye",
+            },
+          ],
+        },
+        hasNext: true,
+      },
+    });
+
+    // TODO: Re-enable once the core bug is fixed
+    // await waitFor(() => {
+    //   expect(result.current).toMatchObject({
+    //     data: {
+    //       greetings: [
+    //         {
+    //           __typename: 'Greeting',
+    //           message: 'Hello world',
+    //           recipient: {
+    //             __typename: 'Person',
+    //             name: 'Alice',
+    //           },
+    //         },
+    //         {
+    //           __typename: 'Greeting',
+    //           message: 'Goodbye',
+    //         },
+    //       ],
+    //     },
+    //     networkStatus: NetworkStatus.ready,
+    //     error: undefined,
+    //   });
+    // });
+
+    link.simulateResult(
+      {
+        result: {
+          incremental: [
+            {
+              data: {
+                recipient: { name: "Bob", __typename: "Person" },
+              },
+              path: ["greetings", 0],
+            },
+          ],
+          hasNext: false,
+        },
+      },
+      true
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: {
+                __typename: "Person",
+                name: "Alice",
+              },
+            },
+            {
+              __typename: "Greeting",
+              message: "Goodbye",
+              recipient: {
+                __typename: "Person",
+                name: "Bob",
+              },
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    await expect(fetchMorePromise!).resolves.toEqual({
+      data: {
+        greetings: [
+          {
+            __typename: "Greeting",
+            message: "Goodbye",
+            recipient: {
+              __typename: "Person",
+              name: "Bob",
+            },
+          },
+        ],
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    expect(renders.count).toBe(5);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: {
+                __typename: "Person",
+                name: "Alice",
+              },
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      // TODO: Re-enable when the core `fetchMore` bug is fixed
+      // {
+      //   data: {
+      //     greetings: [
+      //       {
+      //         __typename: 'Greeting',
+      //         message: 'Hello world',
+      //         recipient: {
+      //           __typename: 'Person',
+      //           name: 'Alice',
+      //         },
+      //       },
+      //       {
+      //         __typename: 'Greeting',
+      //         message: 'Goodbye',
+      //       },
+      //     ],
+      //   },
+      //   networkStatus: NetworkStatus.ready,
+      //   error: undefined,
+      // },
+      {
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Hello world",
+              recipient: {
+                __typename: "Person",
+                name: "Alice",
+              },
+            },
+            {
+              __typename: "Greeting",
+              message: "Goodbye",
+              recipient: {
+                __typename: "Person",
+                name: "Bob",
+              },
+            },
+          ],
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  // TODO: This is a duplicate of the test above, but with the expected behavior
+  // added (hence the `it.failing`). Remove the previous test once issue #11034
+  // is fixed.
+  //
+  // https://github.com/apollographql/apollo-client/issues/11034
+  it.failing(
+    "incrementally rerenders data returned by a `fetchMore` for a deferred query",
+    async () => {
+      const query = gql`
+        query ($offset: Int) {
+          greetings(offset: $offset) {
+            message
+            ... @defer {
+              recipient {
+                name
+              }
+            }
+          }
+        }
+      `;
+
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              greetings: offsetLimitPagination(),
+            },
+          },
+        },
+      });
+      const link = new MockSubscriptionLink();
+      const client = new ApolloClient({ link, cache });
+
+      const { result, renders } = renderSuspenseHook(
+        () => useSuspenseQuery(query, { variables: { offset: 0 } }),
+        { client }
+      );
+
+      link.simulateResult({
+        result: {
+          data: {
+            greetings: [{ __typename: "Greeting", message: "Hello world" }],
+          },
+          hasNext: true,
+        },
+      });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      link.simulateResult(
+        {
+          result: {
+            incremental: [
+              {
+                data: {
+                  recipient: { name: "Alice", __typename: "Person" },
+                },
+                path: ["greetings", 0],
+              },
+            ],
+            hasNext: false,
+          },
+        },
+        true
+      );
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+                recipient: {
+                  __typename: "Person",
+                  name: "Alice",
+                },
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      let fetchMorePromise: Promise<ApolloQueryResult<unknown>>;
+      act(() => {
+        fetchMorePromise = result.current.fetchMore({
+          variables: { offset: 1 },
+        });
+      });
+
+      link.simulateResult({
+        result: {
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Goodbye",
+              },
+            ],
+          },
+          hasNext: true,
+        },
+      });
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+                recipient: {
+                  __typename: "Person",
+                  name: "Alice",
+                },
+              },
+              {
+                __typename: "Greeting",
+                message: "Goodbye",
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      link.simulateResult(
+        {
+          result: {
+            incremental: [
+              {
+                data: {
+                  recipient: { name: "Bob", __typename: "Person" },
+                },
+                path: ["greetings", 0],
+              },
+            ],
+            hasNext: false,
+          },
+        },
+        true
+      );
+
+      await waitFor(() => {
+        expect(result.current).toMatchObject({
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+                recipient: {
+                  __typename: "Person",
+                  name: "Alice",
+                },
+              },
+              {
+                __typename: "Greeting",
+                message: "Goodbye",
+                recipient: {
+                  __typename: "Person",
+                  name: "Bob",
+                },
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        });
+      });
+
+      await expect(fetchMorePromise!).resolves.toEqual({
+        data: {
+          greetings: [
+            {
+              __typename: "Greeting",
+              message: "Goodbye",
+              recipient: {
+                __typename: "Person",
+                name: "Bob",
+              },
+            },
+          ],
+        },
+        loading: false,
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+
+      expect(renders.count).toBe(5);
+      expect(renders.suspenseCount).toBe(2);
+      expect(renders.frames).toMatchObject([
+        {
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+                recipient: {
+                  __typename: "Person",
+                  name: "Alice",
+                },
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+                recipient: {
+                  __typename: "Person",
+                  name: "Alice",
+                },
+              },
+              {
+                __typename: "Greeting",
+                message: "Goodbye",
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+        {
+          data: {
+            greetings: [
+              {
+                __typename: "Greeting",
+                message: "Hello world",
+                recipient: {
+                  __typename: "Person",
+                  name: "Alice",
+                },
+              },
+              {
+                __typename: "Greeting",
+                message: "Goodbye",
+                recipient: {
+                  __typename: "Person",
+                  name: "Bob",
+                },
+              },
+            ],
+          },
+          networkStatus: NetworkStatus.ready,
+          error: undefined,
+        },
+      ]);
+    }
+  );
+
+  it("throws network errors returned by deferred queries", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { renders } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      link,
+    });
+
+    link.simulateResult({
+      error: new Error("Could not fetch"),
+    });
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toEqual(new Error("Could not fetch"));
+    expect(error.graphQLErrors).toEqual([]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it("throws graphql errors returned by deferred queries", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { renders } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      link,
+    });
+
+    link.simulateResult({
+      result: {
+        errors: [new GraphQLError("Could not fetch greeting")],
+      },
+    });
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toBeNull();
+    expect(error.graphQLErrors).toEqual([
+      new GraphQLError("Could not fetch greeting"),
+    ]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it("throws errors returned by deferred queries that include partial data", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const query = gql`
+      query {
+        greeting {
+          message
+          ... on Greeting @defer {
+            recipient {
+              name
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { renders } = renderSuspenseHook(() => useSuspenseQuery(query), {
+      link,
+    });
+
+    link.simulateResult({
+      result: {
+        data: { greeting: null },
+        errors: [new GraphQLError("Could not fetch greeting")],
+      },
+    });
+
+    await waitFor(() => expect(renders.errorCount).toBe(1));
+
+    expect(renders.errors.length).toBe(1);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toEqual([]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toBeNull();
+    expect(error.graphQLErrors).toEqual([
+      new GraphQLError("Could not fetch greeting"),
+    ]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it("discards partial data and throws errors returned in incremental chunks", async () => {
+    const consoleSpy = jest.spyOn(console, "error").mockImplementation();
+
+    const query = gql`
+      query {
+        hero {
+          name
+          heroFriends {
+            id
+            name
+            ... @defer {
+              homeWorld
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query),
+      { link }
+    );
+
+    link.simulateResult({
+      result: {
+        data: {
+          hero: {
+            name: "R2-D2",
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+          },
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            path: ["hero", "heroFriends", 0],
+            errors: [
+              new GraphQLError(
+                "homeWorld for character with ID 1000 could not be fetched.",
+                { path: ["hero", "heroFriends", 0, "homeWorld"] }
+              ),
+            ],
+            data: {
+              homeWorld: null,
+            },
+          },
+          // This chunk is ignored since errorPolicy `none` throws away partial
+          // data
+          {
+            path: ["hero", "heroFriends", 1],
+            data: {
+              homeWorld: "Alderaan",
+            },
+          },
+        ],
+        hasNext: false,
+      },
+    });
+
+    await waitFor(() => {
+      expect(renders.errorCount).toBe(1);
+    });
+
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+
+    const [error] = renders.errors as ApolloError[];
+
+    expect(error).toBeInstanceOf(ApolloError);
+    expect(error.networkError).toBeNull();
+    expect(error.graphQLErrors).toEqual([
+      new GraphQLError(
+        "homeWorld for character with ID 1000 could not be fetched.",
+        { path: ["hero", "heroFriends", 0, "homeWorld"] }
+      ),
+    ]);
+
+    consoleSpy.mockRestore();
+  });
+
+  it("adds partial data and does not throw errors returned in incremental chunks but returns them in `error` property with errorPolicy set to `all`", async () => {
+    const query = gql`
+      query {
+        hero {
+          name
+          heroFriends {
+            id
+            name
+            ... @defer {
+              homeWorld
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { link }
+    );
+
+    link.simulateResult({
+      result: {
+        data: {
+          hero: {
+            name: "R2-D2",
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+          },
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            path: ["hero", "heroFriends", 0],
+            errors: [
+              new GraphQLError(
+                "homeWorld for character with ID 1000 could not be fetched.",
+                { path: ["hero", "heroFriends", 0, "homeWorld"] }
+              ),
+            ],
+            data: {
+              homeWorld: null,
+            },
+          },
+          // Unlike the default (errorPolicy = `none`), this data will be
+          // added to the final result
+          {
+            path: ["hero", "heroFriends", 1],
+            data: {
+              homeWorld: "Alderaan",
+            },
+          },
+        ],
+        hasNext: false,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+                homeWorld: null,
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+                homeWorld: "Alderaan",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({
+          graphQLErrors: [
+            new GraphQLError(
+              "homeWorld for character with ID 1000 could not be fetched.",
+              { path: ["hero", "heroFriends", 0, "homeWorld"] }
+            ),
+          ],
+        }),
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+                homeWorld: null,
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+                homeWorld: "Alderaan",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({
+          graphQLErrors: [
+            new GraphQLError(
+              "homeWorld for character with ID 1000 could not be fetched.",
+              { path: ["hero", "heroFriends", 0, "homeWorld"] }
+            ),
+          ],
+        }),
+      },
+    ]);
+  });
+
+  it("adds partial data and discards errors returned in incremental chunks with errorPolicy set to `ignore`", async () => {
+    const query = gql`
+      query {
+        hero {
+          name
+          heroFriends {
+            id
+            name
+            ... @defer {
+              homeWorld
+            }
+          }
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "ignore" }),
+      { link }
+    );
+
+    link.simulateResult({
+      result: {
+        data: {
+          hero: {
+            name: "R2-D2",
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+          },
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult({
+      result: {
+        incremental: [
+          {
+            path: ["hero", "heroFriends", 0],
+            errors: [
+              new GraphQLError(
+                "homeWorld for character with ID 1000 could not be fetched.",
+                { path: ["hero", "heroFriends", 0, "homeWorld"] }
+              ),
+            ],
+            data: {
+              homeWorld: null,
+            },
+          },
+          {
+            path: ["hero", "heroFriends", 1],
+            data: {
+              homeWorld: "Alderaan",
+            },
+          },
+        ],
+        hasNext: false,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+                homeWorld: null,
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+                homeWorld: "Alderaan",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(3);
+    expect(renders.suspenseCount).toBe(1);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              {
+                id: "1000",
+                name: "Luke Skywalker",
+                homeWorld: null,
+              },
+              {
+                id: "1003",
+                name: "Leia Organa",
+                homeWorld: "Alderaan",
+              },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("can refetch and respond to cache updates after encountering an error in an incremental chunk for a deferred query when `errorPolicy` is `all`", async () => {
+    const query = gql`
+      query {
+        hero {
+          name
+          heroFriends {
+            id
+            name
+            ... @defer {
+              homeWorld
+            }
+          }
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache();
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({ link, cache });
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "all" }),
+      { client }
+    );
+
+    link.simulateResult({
+      result: {
+        data: {
+          hero: {
+            name: "R2-D2",
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+          },
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult(
+      {
+        result: {
+          incremental: [
+            {
+              path: ["hero", "heroFriends", 0],
+              errors: [
+                new GraphQLError(
+                  "homeWorld for character with ID 1000 could not be fetched.",
+                  { path: ["hero", "heroFriends", 0, "homeWorld"] }
+                ),
+              ],
+              data: {
+                homeWorld: null,
+              },
+            },
+            {
+              path: ["hero", "heroFriends", 1],
+              data: {
+                homeWorld: "Alderaan",
+              },
+            },
+          ],
+          hasNext: false,
+        },
+      },
+      true
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({
+          graphQLErrors: [
+            new GraphQLError(
+              "homeWorld for character with ID 1000 could not be fetched.",
+              { path: ["hero", "heroFriends", 0, "homeWorld"] }
+            ),
+          ],
+        }),
+      });
+    });
+
+    let refetchPromise: Promise<ApolloQueryResult<unknown>>;
+    act(() => {
+      refetchPromise = result.current.refetch();
+    });
+
+    link.simulateResult({
+      result: {
+        data: {
+          hero: {
+            name: "R2-D2",
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+          },
+        },
+        hasNext: true,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    link.simulateResult(
+      {
+        result: {
+          incremental: [
+            {
+              path: ["hero", "heroFriends", 0],
+              data: {
+                homeWorld: "Alderaan",
+              },
+            },
+            {
+              path: ["hero", "heroFriends", 1],
+              data: {
+                homeWorld: "Alderaan",
+              },
+            },
+          ],
+          hasNext: false,
+        },
+      },
+      true
+    );
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" },
+              { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    await expect(refetchPromise!).resolves.toEqual({
+      data: {
+        hero: {
+          heroFriends: [
+            { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" },
+            { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" },
+          ],
+          name: "R2-D2",
+        },
+      },
+      loading: false,
+      networkStatus: NetworkStatus.ready,
+      error: undefined,
+    });
+
+    cache.updateQuery({ query }, (data) => ({
+      hero: {
+        ...data.hero,
+        name: "C3PO",
+      },
+    }));
+
+    await waitFor(() => {
+      expect(result.current).toMatchObject({
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" },
+              { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" },
+            ],
+            name: "C3PO",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      });
+    });
+
+    expect(renders.count).toBe(7);
+    expect(renders.suspenseCount).toBe(2);
+    expect(renders.frames).toMatchObject([
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.error,
+        error: new ApolloError({
+          graphQLErrors: [
+            new GraphQLError(
+              "homeWorld for character with ID 1000 could not be fetched.",
+              { path: ["hero", "heroFriends", 0, "homeWorld"] }
+            ),
+          ],
+        }),
+      },
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker" },
+              { id: "1003", name: "Leia Organa" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" },
+              { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" },
+            ],
+            name: "R2-D2",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+      {
+        data: {
+          hero: {
+            heroFriends: [
+              { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" },
+              { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" },
+            ],
+            name: "C3PO",
+          },
+        },
+        networkStatus: NetworkStatus.ready,
+        error: undefined,
+      },
+    ]);
+  });
+
+  it("can subscribe to subscriptions and react to cache updates via `subscribeToMore`", async () => {
+    interface SubscriptionData {
+      greetingUpdated: string;
+    }
+
+    interface QueryData {
+      greeting: string;
+    }
+
+    type UpdateQueryFn = NonNullable<
+      SubscribeToMoreOptions<
+        QueryData | undefined,
+        OperationVariables,
+        SubscriptionData
+      >["updateQuery"]
+    >;
+
+    const { mocks, query } = useSimpleQueryCase();
+
+    const wsLink = new MockSubscriptionLink();
+    const mockLink = new MockLink(mocks);
+
+    const link = split(
+      ({ query }) => {
+        const definition = getMainDefinition(query);
+
+        return (
+          definition.kind === "OperationDefinition" &&
+          definition.operation === "subscription"
+        );
+      },
+      wsLink,
+      mockLink
+    );
+
+    const { result, renders } = renderSuspenseHook(
+      () => useSuspenseQuery(query, { errorPolicy: "ignore" }),
+      { link }
+    );
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual({ greeting: "Hello" });
+    });
+
+    const updateQuery = jest.fn<
+      ReturnType<UpdateQueryFn>,
+      Parameters<UpdateQueryFn>
+    >((_, { subscriptionData: { data } }) => {
+      return { greeting: data.greetingUpdated };
+    });
+
+    result.current.subscribeToMore<SubscriptionData>({
+      document: gql`
+        subscription {
+          greetingUpdated
+        }
+      `,
+      updateQuery,
+    });
+
+    wsLink.simulateResult({
+      result: {
+        data: {
+          greetingUpdated: "Subscription hello",
+        },
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual({
+        greeting: "Subscription hello",
+      });
+    });
+
+    expect(updateQuery).toHaveBeenCalledTimes(1);
+    expect(updateQuery).toHaveBeenCalledWith(
+      { greeting: "Hello" },
+      {
+        subscriptionData: {
+          data: { greetingUpdated: "Subscription hello" },
+        },
+        variables: {},
+      }
+    );
+
+    expect(renders.count).toBe(3);
+    expect(renders.frames).toMatchObject([
+      { data: { greeting: "Hello" }, networkStatus: NetworkStatus.ready },
+      {
+        data: { greeting: "Subscription hello" },
+        networkStatus: NetworkStatus.ready,
+      },
+    ]);
+  });
+
+  it("works with useDeferredValue", async () => {
+    const user = userEvent.setup();
+
+    interface Variables {
+      query: string;
+    }
+
+    interface Data {
+      search: { query: string };
+    }
+
+    const QUERY: TypedDocumentNode<Data, Variables> = gql`
+      query SearchQuery($query: String!) {
+        search(query: $query) {
+          query
+        }
+      }
+    `;
+
+    const link = new ApolloLink(({ variables }) => {
+      return new Observable((observer) => {
+        setTimeout(() => {
+          observer.next({
+            data: { search: { query: variables.query } },
+          });
+          observer.complete();
+        }, 10);
+      });
+    });
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      const [query, setValue] = React.useState("");
+      const deferredQuery = React.useDeferredValue(query);
+
+      return (
+        <ApolloProvider client={client}>
+          <label htmlFor="searchInput">Search</label>
+          <input
+            id="searchInput"
+            type="text"
+            value={query}
+            onChange={(e) => setValue(e.target.value)}
+          />
+          <Suspense fallback={<SuspenseFallback />}>
+            <Results query={deferredQuery} />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Results({ query }: { query: string }) {
+      const { data } = useSuspenseQuery(QUERY, { variables: { query } });
+
+      return <div data-testid="result">{data.search.query}</div>;
+    }
+
+    render(<App />);
+
+    const input = screen.getByLabelText("Search");
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    expect(await screen.findByTestId("result")).toBeInTheDocument();
+
+    await act(() => user.type(input, "ab"));
+
+    await waitFor(() => {
+      expect(screen.getByTestId("result")).toHaveTextContent("ab");
+    });
+
+    await act(() => user.type(input, "c"));
+
+    // useDeferredValue will try rerendering the component with the new value
+    // in the background. If it suspends with the new value, React will show the
+    // stale UI until the component is done suspending.
+    //
+    // Here we should not see the suspense fallback while the component suspends
+    // until the search finishes loading. Seeing the suspense fallback is an
+    // indication that we are suspending the component too late in the process.
+    expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+    expect(screen.getByTestId("result")).toHaveTextContent("ab");
+
+    // Eventually we should see the updated text content once its done
+    // suspending.
+    await waitFor(() => {
+      expect(screen.getByTestId("result")).toHaveTextContent("abc");
+    });
+  });
+
+  it("works with startTransition to change variables", async () => {
+    type Variables = {
+      id: string;
+    };
+
+    interface Data {
+      todo: {
+        id: string;
+        name: string;
+        completed: boolean;
+      };
+    }
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query TodoItemQuery($id: ID!) {
+        todo(id: $id) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+
+    const mocks: MockedResponse<Data, Variables>[] = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { id: "2" } },
+        result: {
+          data: { todo: { id: "2", name: "Take out trash", completed: true } },
+        },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      const [id, setId] = React.useState("1");
+
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Todo id={id} onChange={setId} />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Todo({
+      id,
+      onChange,
+    }: {
+      id: string;
+      onChange: (id: string) => void;
+    }) {
+      const { data } = useSuspenseQuery(query, { variables: { id } });
+      const [isPending, startTransition] = React.useTransition();
+      const { todo } = data;
+
+      return (
+        <>
+          <button
+            onClick={() => {
+              startTransition(() => {
+                onChange("2");
+              });
+            }}
+          >
+            Refresh
+          </button>
+          <div data-testid="todo" aria-busy={isPending}>
+            {todo.name}
+            {todo.completed && " (completed)"}
+          </div>
+        </>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    expect(await screen.findByTestId("todo")).toBeInTheDocument();
+
+    const todo = screen.getByTestId("todo");
+    const button = screen.getByText("Refresh");
+
+    expect(todo).toHaveTextContent("Clean room");
+
+    await act(() => user.click(button));
+
+    // startTransition will avoid rendering the suspense fallback for already
+    // revealed content if the state update inside the transition causes the
+    // component to suspend.
+    //
+    // Here we should not see the suspense fallback while the component suspends
+    // until the todo is finished loading. Seeing the suspense fallback is an
+    // indication that we are suspending the component too late in the process.
+    expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+
+    // We can ensure this works with isPending from useTransition in the process
+    expect(todo).toHaveAttribute("aria-busy", "true");
+
+    // Ensure we are showing the stale UI until the new todo has loaded
+    expect(todo).toHaveTextContent("Clean room");
+
+    // Eventually we should see the updated todo content once its done
+    // suspending.
+    await waitFor(() => {
+      expect(todo).toHaveTextContent("Take out trash (completed)");
+    });
+  });
+
+  it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => {
+    type Variables = {
+      id: string;
+    };
+
+    interface Data {
+      todo: {
+        id: string;
+        name: string;
+        completed: boolean;
+      };
+    }
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query TodoItemQuery($id: ID!) {
+        todo(id: $id) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+
+    const mocks: MockedResponse<Data, Variables>[] = [
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: false } },
+        },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { id: "1" } },
+        result: {
+          data: { todo: { id: "1", name: "Clean room", completed: true } },
+        },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache(),
+    });
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Todo id="1" />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Todo({ id }: { id: string }) {
+      const { data, refetch } = useSuspenseQuery(query, { variables: { id } });
+      const [isPending, startTransition] = React.useTransition();
+      const { todo } = data;
+
+      return (
+        <>
+          <button
+            onClick={() => {
+              startTransition(() => {
+                refetch();
+              });
+            }}
+          >
+            Refresh
+          </button>
+          <div data-testid="todo" aria-busy={isPending}>
+            {todo.name}
+            {todo.completed && " (completed)"}
+          </div>
+        </>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    expect(await screen.findByTestId("todo")).toBeInTheDocument();
+
+    const todo = screen.getByTestId("todo");
+    const button = screen.getByText("Refresh");
+
+    expect(todo).toHaveTextContent("Clean room");
+
+    await act(() => user.click(button));
+
+    // startTransition will avoid rendering the suspense fallback for already
+    // revealed content if the state update inside the transition causes the
+    // component to suspend.
+    //
+    // Here we should not see the suspense fallback while the component suspends
+    // until the todo is finished loading. Seeing the suspense fallback is an
+    // indication that we are suspending the component too late in the process.
+    expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+
+    // We can ensure this works with isPending from useTransition in the process
+    expect(todo).toHaveAttribute("aria-busy", "true");
+
+    // Ensure we are showing the stale UI until the new todo has loaded
+    expect(todo).toHaveTextContent("Clean room");
+
+    // Eventually we should see the updated todo content once its done
+    // suspending.
+    await waitFor(() => {
+      expect(todo).toHaveTextContent("Clean room (completed)");
+    });
+  });
+
+  it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => {
+    type Variables = {
+      offset: number;
+    };
+
+    interface Todo {
+      __typename: "Todo";
+      id: string;
+      name: string;
+      completed: boolean;
+    }
+
+    interface Data {
+      todos: Todo[];
+    }
+    const user = userEvent.setup();
+
+    const query: TypedDocumentNode<Data, Variables> = gql`
+      query TodosQuery($offset: Int!) {
+        todos(offset: $offset) {
+          id
+          name
+          completed
+        }
+      }
+    `;
+
+    const mocks: MockedResponse<Data, Variables>[] = [
+      {
+        request: { query, variables: { offset: 0 } },
+        result: {
+          data: {
+            todos: [
+              {
+                __typename: "Todo",
+                id: "1",
+                name: "Clean room",
+                completed: false,
+              },
+            ],
+          },
+        },
+        delay: 10,
+      },
+      {
+        request: { query, variables: { offset: 1 } },
+        result: {
+          data: {
+            todos: [
+              {
+                __typename: "Todo",
+                id: "2",
+                name: "Take out trash",
+                completed: true,
+              },
+            ],
+          },
+        },
+        delay: 10,
+      },
+    ];
+
+    const client = new ApolloClient({
+      link: new MockLink(mocks),
+      cache: new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              todos: offsetLimitPagination(),
+            },
+          },
+        },
+      }),
+    });
+
+    function App() {
+      return (
+        <ApolloProvider client={client}>
+          <Suspense fallback={<SuspenseFallback />}>
+            <Todos />
+          </Suspense>
+        </ApolloProvider>
+      );
+    }
+
+    function SuspenseFallback() {
+      return <p>Loading</p>;
+    }
+
+    function Todos() {
+      const { data, fetchMore } = useSuspenseQuery(query, {
+        variables: { offset: 0 },
+      });
+      const [isPending, startTransition] = React.useTransition();
+      const { todos } = data;
+
+      return (
+        <>
+          <button
+            onClick={() => {
+              startTransition(() => {
+                fetchMore({ variables: { offset: 1 } });
+              });
+            }}
+          >
+            Load more
+          </button>
+          <div data-testid="todos" aria-busy={isPending}>
+            {todos.map((todo) => (
+              <div data-testid={`todo:${todo.id}`} key={todo.id}>
+                {todo.name}
+                {todo.completed && " (completed)"}
+              </div>
+            ))}
+          </div>
+        </>
+      );
+    }
+
+    render(<App />);
+
+    expect(screen.getByText("Loading")).toBeInTheDocument();
+
+    expect(await screen.findByTestId("todos")).toBeInTheDocument();
+
+    const todos = screen.getByTestId("todos");
+    const todo1 = screen.getByTestId("todo:1");
+    const button = screen.getByText("Load more");
+
+    expect(todo1).toBeInTheDocument();
+
+    await act(() => user.click(button));
+
+    // startTransition will avoid rendering the suspense fallback for already
+    // revealed content if the state update inside the transition causes the
+    // component to suspend.
+    //
+    // Here we should not see the suspense fallback while the component suspends
+    // until the todo is finished loading. Seeing the suspense fallback is an
+    // indication that we are suspending the component too late in the process.
+    expect(screen.queryByText("Loading")).not.toBeInTheDocument();
+
+    // We can ensure this works with isPending from useTransition in the process
+    expect(todos).toHaveAttribute("aria-busy", "true");
+
+    // Ensure we are showing the stale UI until the new todo has loaded
+    expect(todo1).toHaveTextContent("Clean room");
+
+    // Eventually we should see the updated todos content once its done
+    // suspending.
+    await waitFor(() => {
+      expect(screen.getByTestId("todo:2")).toHaveTextContent(
+        "Take out trash (completed)"
+      );
+      expect(todo1).toHaveTextContent("Clean room");
+    });
+  });
+
+  describe.skip("type tests", () => {
+    it("returns unknown when TData cannot be inferred", () => {
+      const query = gql`
+        query {
+          hello
+        }
+      `;
+
+      const { data } = useSuspenseQuery(query);
+
+      expectTypeOf(data).toEqualTypeOf<unknown>();
+    });
+
+    it("disallows wider variables type than specified", () => {
+      const { query } = useVariablesQueryCase();
+
+      // @ts-expect-error should not allow wider TVariables type
+      useSuspenseQuery(query, { variables: { id: "1", foo: "bar" } });
+    });
+
+    it("returns TData in default case", () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query);
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData | undefined>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData | undefined>();
+    });
+
+    it('returns TData | undefined with errorPolicy: "ignore"', () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        errorPolicy: "ignore",
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        errorPolicy: "ignore",
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it('returns TData | undefined with errorPolicy: "all"', () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        errorPolicy: "all",
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        errorPolicy: "all",
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it('returns TData with errorPolicy: "none"', () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData | undefined>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData | undefined>();
+    });
+
+    it("returns DeepPartial<TData> with returnPartialData: true", () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        returnPartialData: true,
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: true,
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns TData with returnPartialData: false", () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        returnPartialData: false,
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: false,
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+    });
+
+    it("returns TData | undefined when skip is present", () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        skip: true,
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        skip: true,
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+
+      // TypeScript is too smart and using a `const` or `let` boolean variable
+      // for the `skip` option results in a false positive. Using an options
+      // object allows us to properly check for a dynamic case.
+      const options = {
+        skip: true,
+      };
+
+      const { data: dynamic } = useSuspenseQuery(query, {
+        skip: options.skip,
+      });
+
+      expectTypeOf(dynamic).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(dynamic).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns TData | undefined when using `skipToken` as options", () => {
+      const { query } = useVariablesQueryCase();
+      const options = {
+        skip: true,
+      };
+
+      const { data: inferred } = useSuspenseQuery(
+        query,
+        options.skip ? skipToken : { variables: { id: "1" } }
+      );
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, options.skip ? skipToken : { variables: { id: "1" } });
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns TData | undefined when using `skipToken` with undefined options", () => {
+      const { query } = useVariablesQueryCase();
+      const options = {
+        skip: true,
+      };
+
+      const { data: inferred } = useSuspenseQuery(
+        query,
+        options.skip ? skipToken : undefined
+      );
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, options.skip ? skipToken : undefined);
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData | undefined>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns DeepPartial<TData> | undefined when using `skipToken` as options with `returnPartialData`", () => {
+      const { query } = useVariablesQueryCase();
+      const options = {
+        skip: true,
+      };
+
+      const { data: inferred } = useSuspenseQuery(
+        query,
+        options.skip ? skipToken : { returnPartialData: true }
+      );
+
+      expectTypeOf(inferred).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, options.skip ? skipToken : { returnPartialData: true });
+
+      expectTypeOf(explicit).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns TData when passing an option that does not affect TData", () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        fetchPolicy: "no-cache",
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(inferred).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        fetchPolicy: "no-cache",
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<VariablesCaseData>();
+      expectTypeOf(explicit).not.toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+    });
+
+    it("handles combinations of options", () => {
+      // TypeScript is too smart and using a `const` or `let` boolean variable
+      // for the `skip` option results in a false positive. Using an options
+      // object allows us to properly check for a dynamic case which is the
+      // typical usage of this option.
+      const options = {
+        skip: true,
+      };
+
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferredPartialDataIgnore } = useSuspenseQuery(query, {
+        returnPartialData: true,
+        errorPolicy: "ignore",
+      });
+
+      expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(
+        inferredPartialDataIgnore
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicitPartialDataIgnore } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: true,
+        errorPolicy: "ignore",
+      });
+
+      expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(
+        explicitPartialDataIgnore
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: inferredPartialDataNone } = useSuspenseQuery(query, {
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(inferredPartialDataNone).toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+      expectTypeOf(
+        inferredPartialDataNone
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicitPartialDataNone } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(explicitPartialDataNone).toEqualTypeOf<
+        DeepPartial<VariablesCaseData>
+      >();
+      expectTypeOf(
+        explicitPartialDataNone
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: inferredSkipIgnore } = useSuspenseQuery(query, {
+        skip: options.skip,
+        errorPolicy: "ignore",
+      });
+
+      expectTypeOf(inferredSkipIgnore).toEqualTypeOf<
+        VariablesCaseData | undefined
+      >();
+      expectTypeOf(
+        inferredPartialDataIgnore
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicitSkipIgnore } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        skip: options.skip,
+        errorPolicy: "ignore",
+      });
+
+      expectTypeOf(explicitSkipIgnore).toEqualTypeOf<
+        VariablesCaseData | undefined
+      >();
+      expectTypeOf(explicitSkipIgnore).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: inferredSkipNone } = useSuspenseQuery(query, {
+        skip: options.skip,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(inferredSkipNone).toEqualTypeOf<
+        VariablesCaseData | undefined
+      >();
+      expectTypeOf(inferredSkipNone).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicitSkipNone } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        skip: options.skip,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(explicitSkipNone).toEqualTypeOf<
+        VariablesCaseData | undefined
+      >();
+      expectTypeOf(explicitSkipNone).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: inferredPartialDataNoneSkip } = useSuspenseQuery(query, {
+        skip: options.skip,
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(inferredPartialDataNoneSkip).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(
+        inferredPartialDataNoneSkip
+      ).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicitPartialDataNoneSkip } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        skip: options.skip,
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(explicitPartialDataNoneSkip).toEqualTypeOf<
+        DeepPartial<VariablesCaseData> | undefined
+      >();
+      expectTypeOf(
+        explicitPartialDataNoneSkip
+      ).not.toEqualTypeOf<VariablesCaseData>();
+    });
+
+    it("returns correct TData type when combined options that do not affect TData", () => {
+      const { query } = useVariablesQueryCase();
+
+      const { data: inferred } = useSuspenseQuery(query, {
+        fetchPolicy: "no-cache",
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(inferred).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(inferred).not.toEqualTypeOf<VariablesCaseData>();
+
+      const { data: explicit } = useSuspenseQuery<
+        VariablesCaseData,
+        VariablesCaseVariables
+      >(query, {
+        fetchPolicy: "no-cache",
+        returnPartialData: true,
+        errorPolicy: "none",
+      });
+
+      expectTypeOf(explicit).toEqualTypeOf<DeepPartial<VariablesCaseData>>();
+      expectTypeOf(explicit).not.toEqualTypeOf<VariablesCaseData>();
+    });
+  });
+});
diff --git a/src/react/hooks/internal/__tests__/useDeepMemo.test.ts b/src/react/hooks/internal/__tests__/useDeepMemo.test.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/internal/__tests__/useDeepMemo.test.ts
@@ -0,0 +1,47 @@
+import { renderHook } from "@testing-library/react";
+import { useDeepMemo } from "../useDeepMemo";
+
+describe("useDeepMemo", () => {
+  it("ensures the value is initialized", () => {
+    const { result } = renderHook(() =>
+      useDeepMemo(() => ({ test: true }), [])
+    );
+
+    expect(result.current).toEqual({ test: true });
+  });
+
+  it("returns memoized value when its dependencies are deeply equal", () => {
+    const { result, rerender } = renderHook(
+      ({ active, items, user }) => {
+        useDeepMemo(() => ({ active, items, user }), [items, name, active]);
+      },
+      {
+        initialProps: {
+          active: true,
+          items: [1, 2],
+          user: { name: "John Doe" },
+        },
+      }
+    );
+
+    const previousResult = result.current;
+
+    rerender({ active: true, items: [1, 2], user: { name: "John Doe" } });
+
+    expect(result.current).toBe(previousResult);
+  });
+
+  it("returns updated value if a dependency changes", () => {
+    const { result, rerender } = renderHook(
+      ({ items }) => useDeepMemo(() => ({ items }), [items]),
+      { initialProps: { items: [1] } }
+    );
+
+    const previousResult = result.current;
+
+    rerender({ items: [1, 2] });
+
+    expect(result.current).not.toBe(previousResult);
+    expect(result.current).toEqual({ items: [1, 2] });
+  });
+});
diff --git a/src/react/parser/__tests__/parser.test.ts b/src/react/parser/__tests__/parser.test.ts
--- a/src/react/parser/__tests__/parser.test.ts
+++ b/src/react/parser/__tests__/parser.test.ts
@@ -1,11 +1,11 @@
-import gql from 'graphql-tag';
+import gql from "graphql-tag";
 
-import { parser, DocumentType } from '..';
+import { parser, DocumentType } from "..";
 
 type OperationDefinition = any;
 
-describe('parser', () => {
-  it('should error if both a query and a mutation is present', () => {
+describe("parser", () => {
+  it("should error if both a query and a mutation is present", () => {
     const query = gql`
       query {
         user {
@@ -13,7 +13,7 @@ describe('parser', () => {
         }
       }
 
-      mutation($t: String) {
+      mutation ($t: String) {
         addT(t: $t) {
           user {
             name
@@ -25,7 +25,7 @@ describe('parser', () => {
     expect(parser.bind(null, query)).toThrowError(/react-apollo only supports/);
   });
 
-  it('should error if multiple operations are present', () => {
+  it("should error if multiple operations are present", () => {
     const query = gql`
       query One {
         user {
@@ -43,7 +43,7 @@ describe('parser', () => {
     expect(parser.bind(null, query)).toThrowError(/react-apollo only supports/);
   });
 
-  it('should error if not a DocumentNode', () => {
+  it("should error if not a DocumentNode", () => {
     const query = `
       query One { user { name } }
     `;
@@ -52,7 +52,7 @@ describe('parser', () => {
     );
   });
 
-  it('should return the name of the operation', () => {
+  it("should return the name of the operation", () => {
     const query = gql`
       query One {
         user {
@@ -60,7 +60,7 @@ describe('parser', () => {
         }
       }
     `;
-    expect(parser(query).name).toBe('One');
+    expect(parser(query).name).toBe("One");
 
     const mutation = gql`
       mutation One {
@@ -69,7 +69,7 @@ describe('parser', () => {
         }
       }
     `;
-    expect(parser(mutation).name).toBe('One');
+    expect(parser(mutation).name).toBe("One");
 
     const subscription = gql`
       subscription One {
@@ -78,10 +78,10 @@ describe('parser', () => {
         }
       }
     `;
-    expect(parser(subscription).name).toBe('One');
+    expect(parser(subscription).name).toBe("One");
   });
 
-  it('should return data as the name of the operation if not named', () => {
+  it("should return data as the name of the operation if not named", () => {
     const query = gql`
       query {
         user {
@@ -89,7 +89,7 @@ describe('parser', () => {
         }
       }
     `;
-    expect(parser(query).name).toBe('data');
+    expect(parser(query).name).toBe("data");
 
     const unnamedQuery = gql`
       {
@@ -98,7 +98,7 @@ describe('parser', () => {
         }
       }
     `;
-    expect(parser(unnamedQuery).name).toBe('data');
+    expect(parser(unnamedQuery).name).toBe("data");
 
     const mutation = gql`
       mutation {
@@ -107,7 +107,7 @@ describe('parser', () => {
         }
       }
     `;
-    expect(parser(mutation).name).toBe('data');
+    expect(parser(mutation).name).toBe("data");
 
     const subscription = gql`
       subscription {
@@ -116,10 +116,10 @@ describe('parser', () => {
         }
       }
     `;
-    expect(parser(subscription).name).toBe('data');
+    expect(parser(subscription).name).toBe("data");
   });
 
-  it('should return the type of operation', () => {
+  it("should return the type of operation", () => {
     const query = gql`
       query One {
         user {
@@ -157,7 +157,7 @@ describe('parser', () => {
     expect(parser(subscription).type).toBe(DocumentType.Subscription);
   });
 
-  it('should return the variable definitions of the operation', () => {
+  it("should return the variable definitions of the operation", () => {
     const query = gql`
       query One($t: String!) {
         user(t: $t) {
@@ -191,7 +191,7 @@ describe('parser', () => {
     );
   });
 
-  it('should not error if the operation has no variables', () => {
+  it("should not error if the operation has no variables", () => {
     const query = gql`
       query {
         user(t: $t) {
diff --git a/src/react/ssr/__tests__/useLazyQuery.test.tsx b/src/react/ssr/__tests__/useLazyQuery.test.tsx
--- a/src/react/ssr/__tests__/useLazyQuery.test.tsx
+++ b/src/react/ssr/__tests__/useLazyQuery.test.tsx
@@ -1,15 +1,15 @@
 /** @jest-environment node */
-import React from 'react';
-import { DocumentNode } from 'graphql';
-import gql from 'graphql-tag';
-import { mockSingleLink } from '../../../testing';
-import { ApolloClient } from '../../../core';
-import { InMemoryCache } from '../../../cache';
-import { ApolloProvider } from '../../context';
-import { useLazyQuery } from '../../hooks';
-import { renderToStringWithData } from '../../ssr';
+import React from "react";
+import { DocumentNode } from "graphql";
+import gql from "graphql-tag";
+import { mockSingleLink } from "../../../testing";
+import { ApolloClient } from "../../../core";
+import { InMemoryCache } from "../../../cache";
+import { ApolloProvider } from "../../context";
+import { useLazyQuery } from "../../hooks";
+import { renderToStringWithData } from "../../ssr";
 
-describe('useLazyQuery Hook SSR', () => {
+describe("useLazyQuery Hook SSR", () => {
   const CAR_QUERY: DocumentNode = gql`
     query {
       cars {
@@ -23,24 +23,24 @@ describe('useLazyQuery Hook SSR', () => {
   const CAR_RESULT_DATA = {
     cars: [
       {
-        make: 'Audi',
-        model: 'RS8',
-        vin: 'DOLLADOLLABILL',
-        __typename: 'Car'
-      }
-    ]
+        make: "Audi",
+        model: "RS8",
+        vin: "DOLLADOLLABILL",
+        __typename: "Car",
+      },
+    ],
   };
 
-  it('should run query only after calling the lazy mode execute function', () => {
+  it("should run query only after calling the lazy mode execute function", () => {
     const link = mockSingleLink({
       request: { query: CAR_QUERY },
-      result: { data: CAR_RESULT_DATA }
+      result: { data: CAR_RESULT_DATA },
     });
 
     const client = new ApolloClient({
       cache: new InMemoryCache(),
       link,
-      ssrMode: true
+      ssrMode: true,
     });
 
     const Component = () => {
@@ -66,7 +66,7 @@ describe('useLazyQuery Hook SSR', () => {
       </ApolloProvider>
     );
 
-    return renderToStringWithData(app).then(markup => {
+    return renderToStringWithData(app).then((markup) => {
       expect(markup).toMatch(/Audi/);
     });
   });
diff --git a/src/react/ssr/__tests__/useQuery.test.tsx b/src/react/ssr/__tests__/useQuery.test.tsx
--- a/src/react/ssr/__tests__/useQuery.test.tsx
+++ b/src/react/ssr/__tests__/useQuery.test.tsx
@@ -1,15 +1,15 @@
 /** @jest-environment node */
-import React from 'react';
-import { DocumentNode } from 'graphql';
-import gql from 'graphql-tag';
-import { MockedProvider, mockSingleLink } from '../../../testing';
-import { ApolloClient } from '../../../core';
-import { InMemoryCache } from '../../../cache';
-import { ApolloProvider } from '../../context';
-import { useApolloClient, useQuery } from '../../hooks';
-import { renderToStringWithData } from '..';
-
-describe('useQuery Hook SSR', () => {
+import React from "react";
+import { DocumentNode } from "graphql";
+import gql from "graphql-tag";
+import { MockedProvider, mockSingleLink } from "../../../testing";
+import { ApolloClient } from "../../../core";
+import { InMemoryCache } from "../../../cache";
+import { ApolloProvider } from "../../context";
+import { useApolloClient, useQuery } from "../../hooks";
+import { renderToStringWithData } from "..";
+
+describe("useQuery Hook SSR", () => {
   const CAR_QUERY: DocumentNode = gql`
     query {
       cars {
@@ -23,24 +23,24 @@ describe('useQuery Hook SSR', () => {
   const CAR_RESULT_DATA = {
     cars: [
       {
-        make: 'Audi',
-        model: 'RS8',
-        vin: 'DOLLADOLLABILL',
-        __typename: 'Car'
-      }
-    ]
+        make: "Audi",
+        model: "RS8",
+        vin: "DOLLADOLLABILL",
+        __typename: "Car",
+      },
+    ],
   };
 
   const CAR_MOCKS = [
     {
       request: {
-        query: CAR_QUERY
+        query: CAR_QUERY,
       },
-      result: { data: CAR_RESULT_DATA }
-    }
+      result: { data: CAR_RESULT_DATA },
+    },
   ];
 
-  it('should support SSR', () => {
+  it("should support SSR", () => {
     const Component = () => {
       const { loading, data } = useQuery(CAR_QUERY);
       if (!loading) {
@@ -61,12 +61,12 @@ describe('useQuery Hook SSR', () => {
       </MockedProvider>
     );
 
-    return renderToStringWithData(app).then(markup => {
+    return renderToStringWithData(app).then((markup) => {
       expect(markup).toMatch(/Audi/);
     });
   });
 
-  it('should initialize data as `undefined` when loading', () => {
+  it("should initialize data as `undefined` when loading", () => {
     const Component = () => {
       const { data, loading } = useQuery(CAR_QUERY);
       if (loading) {
@@ -84,7 +84,7 @@ describe('useQuery Hook SSR', () => {
     return renderToStringWithData(app);
   });
 
-  it('should skip SSR tree rendering and return a loading state if `ssr` option is `false`', async () => {
+  it("should skip SSR tree rendering and return a loading state if `ssr` option is `false`", async () => {
     let renderCount = 0;
     const Component = () => {
       const { data, loading } = useQuery(CAR_QUERY, { ssr: false });
@@ -105,13 +105,13 @@ describe('useQuery Hook SSR', () => {
       </MockedProvider>
     );
 
-    return renderToStringWithData(app).then(result => {
+    return renderToStringWithData(app).then((result) => {
       expect(renderCount).toBe(1);
-      expect(result).toEqual('');
+      expect(result).toEqual("");
     });
   });
 
-  it('should skip SSR tree rendering and not return a loading state loading if `ssr` option is `false` and `skip` is `true`', async () => {
+  it("should skip SSR tree rendering and not return a loading state loading if `ssr` option is `false` and `skip` is `true`", async () => {
     let renderCount = 0;
     const Component = () => {
       const { data, loading } = useQuery(CAR_QUERY, { ssr: false, skip: true });
@@ -129,67 +129,63 @@ describe('useQuery Hook SSR', () => {
       </MockedProvider>
     );
 
-    return renderToStringWithData(app).then(result => {
+    return renderToStringWithData(app).then((result) => {
       expect(renderCount).toBe(1);
-      expect(result).toEqual('');
+      expect(result).toEqual("");
     });
   });
 
-  it('should skip both SSR tree rendering and SSR component rendering if `ssr` option is `false` and `ssrMode` is `true`',
-    async () => {
-      const link = mockSingleLink({
-        request: { query: CAR_QUERY },
-        result: { data: CAR_RESULT_DATA }
-      });
+  it("should skip both SSR tree rendering and SSR component rendering if `ssr` option is `false` and `ssrMode` is `true`", async () => {
+    const link = mockSingleLink({
+      request: { query: CAR_QUERY },
+      result: { data: CAR_RESULT_DATA },
+    });
 
-      const client = new ApolloClient({
-        cache: new InMemoryCache(),
-        link,
-        ssrMode: true
-      });
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link,
+      ssrMode: true,
+    });
 
-      let renderCount = 0;
-      const Component = () => {
-        const { data, loading } = useQuery(CAR_QUERY, { ssr: false });
-
-        let content = null;
-        switch (renderCount) {
-          case 0:
-            expect(loading).toBeTruthy();
-            expect(data).toBeUndefined();
-            break;
-          case 1: // FAIL; should not render a second time
-          default:
-            throw new Error("Duplicate render");
-        }
+    let renderCount = 0;
+    const Component = () => {
+      const { data, loading } = useQuery(CAR_QUERY, { ssr: false });
 
-        renderCount += 1;
-        return content;
-      };
+      let content = null;
+      switch (renderCount) {
+        case 0:
+          expect(loading).toBeTruthy();
+          expect(data).toBeUndefined();
+          break;
+        case 1: // FAIL; should not render a second time
+        default:
+          throw new Error("Duplicate render");
+      }
 
-      const app = (
-        <ApolloProvider client={client}>
-          <Component />
-        </ApolloProvider>
-      );
+      renderCount += 1;
+      return content;
+    };
 
-      const view = await renderToStringWithData(app);
-      expect(renderCount).toBe(1);
-      expect(view).toEqual('');
-      await new Promise((resolve) => setTimeout(resolve, 20));
-      expect(renderCount).toBe(1);
-      expect(view).toEqual('');
-    }
-  );
+    const app = (
+      <ApolloProvider client={client}>
+        <Component />
+      </ApolloProvider>
+    );
 
-  it('should skip SSR tree rendering if `skip` option is `true`', async () => {
+    const view = await renderToStringWithData(app);
+    expect(renderCount).toBe(1);
+    expect(view).toEqual("");
+    await new Promise((resolve) => setTimeout(resolve, 20));
+    expect(renderCount).toBe(1);
+    expect(view).toEqual("");
+  });
+
+  it("should skip SSR tree rendering if `skip` option is `true`", async () => {
     let renderCount = 0;
     const Component = () => {
-      const {
-        loading,
-        networkStatus,
-        data,
-      } = useQuery(CAR_QUERY, { skip: true });
+      const { loading, networkStatus, data } = useQuery(CAR_QUERY, {
+        skip: true,
+      });
       renderCount += 1;
 
       expect(loading).toBeFalsy();
@@ -205,20 +201,17 @@ describe('useQuery Hook SSR', () => {
       </MockedProvider>
     );
 
-    return renderToStringWithData(app).then(result => {
+    return renderToStringWithData(app).then((result) => {
       expect(renderCount).toBe(1);
-      expect(result).toBe('');
+      expect(result).toBe("");
     });
   });
 
-  it('should render SSR tree rendering if `skip` option is `true` for only one instance of the query', async () => {
+  it("should render SSR tree rendering if `skip` option is `true` for only one instance of the query", async () => {
     let renderCount = 0;
 
     const AnotherComponent = () => {
-      const {
-        loading,
-        data,
-      } = useQuery(CAR_QUERY, { skip: false });
+      const { loading, data } = useQuery(CAR_QUERY, { skip: false });
 
       renderCount += 1;
 
@@ -236,10 +229,7 @@ describe('useQuery Hook SSR', () => {
     };
 
     const Component = () => {
-      const {
-        loading,
-        data,
-      } = useQuery(CAR_QUERY, { skip: true });
+      const { loading, data } = useQuery(CAR_QUERY, { skip: true });
       renderCount += 1;
 
       expect(loading).toBeFalsy();
@@ -254,14 +244,14 @@ describe('useQuery Hook SSR', () => {
       </MockedProvider>
     );
 
-    return renderToStringWithData(app).then(result => {
+    return renderToStringWithData(app).then((result) => {
       expect(renderCount).toBe(4);
       expect(result).toMatch(/Audi/);
       expect(result).toMatch(/RS8/);
     });
   });
 
-  it('should return data written previously to cache during SSR pass if using cache-only fetchPolicy', async () => {
+  it("should return data written previously to cache during SSR pass if using cache-only fetchPolicy", async () => {
     const cache = new InMemoryCache({
       typePolicies: {
         Order: {
@@ -290,11 +280,11 @@ describe('useQuery Hook SSR', () => {
 
     const initialData = {
       getSearchResults: {
-        __typename: 'SearchResults',
-        locale: 'en-US',
+        __typename: "SearchResults",
+        locale: "en-US",
         order: {
-          __typename: 'Order',
-          selection: 'RELEVANCE',
+          __typename: "Order",
+          selection: "RELEVANCE",
         },
         pagination: {
           pageLimit: 3,
@@ -310,10 +300,10 @@ describe('useQuery Hook SSR', () => {
     const spy = jest.fn();
 
     const Component = () => {
-      useApolloClient().writeQuery({ query, data: initialData });;
+      useApolloClient().writeQuery({ query, data: initialData });
 
       const { loading, data } = useQuery(query, {
-        fetchPolicy: 'cache-only',
+        fetchPolicy: "cache-only",
       });
 
       spy(loading);
@@ -323,30 +313,21 @@ describe('useQuery Hook SSR', () => {
 
         const {
           getSearchResults: {
-            pagination: {
-              pageLimit,
-            },
+            pagination: { pageLimit },
           },
         } = data;
-        return (
-          <div>
-            {pageLimit}
-          </div>
-        );
+        return <div>{pageLimit}</div>;
       }
       return null;
     };
 
     const app = (
-      <MockedProvider
-        addTypename
-        cache={cache}
-      >
+      <MockedProvider addTypename cache={cache}>
         <Component />
       </MockedProvider>
     );
 
-    return renderToStringWithData(app).then(markup => {
+    return renderToStringWithData(app).then((markup) => {
       expect(spy).toHaveBeenNthCalledWith(1, false);
       expect(markup).toMatch(/<div.*>3<\/div>/);
       expect(cache.extract()).toMatchSnapshot();
diff --git a/src/react/ssr/__tests__/useReactiveVar.test.tsx b/src/react/ssr/__tests__/useReactiveVar.test.tsx
--- a/src/react/ssr/__tests__/useReactiveVar.test.tsx
+++ b/src/react/ssr/__tests__/useReactiveVar.test.tsx
@@ -1,13 +1,13 @@
 /** @jest-environment node */
-import React from 'react';
-import { makeVar } from '../../../core';
-import { useReactiveVar } from '../../hooks';
+import React from "react";
+import { makeVar } from "../../../core";
+import { useReactiveVar } from "../../hooks";
 import { itAsync } from "../../../testing";
-import { renderToStringWithData } from '../';
+import { renderToStringWithData } from "../";
 
-describe('useReactiveVar Hook SSR', () => {
+describe("useReactiveVar Hook SSR", () => {
   itAsync("does not cause warnings", (resolve, reject) => {
-    const mock = jest.spyOn(console, 'error');
+    const mock = jest.spyOn(console, "error");
     const counterVar = makeVar(0);
     function Component() {
       const count = useReactiveVar(counterVar);
@@ -16,11 +16,14 @@ describe('useReactiveVar Hook SSR', () => {
       return <div>{count}</div>;
     }
 
-    renderToStringWithData(<Component />).then((value) => {
-      expect(value).toEqual('<div>0</div>');
-      expect(mock).toHaveBeenCalledTimes(0);
-    }).finally(() => {
-      mock.mockRestore();
-    }).then(resolve, reject);
+    renderToStringWithData(<Component />)
+      .then((value) => {
+        expect(value).toEqual("<div>0</div>");
+        expect(mock).toHaveBeenCalledTimes(0);
+      })
+      .finally(() => {
+        mock.mockRestore();
+      })
+      .then(resolve, reject);
   });
 });
diff --git a/src/testing/core/index.ts b/src/testing/core/index.ts
--- a/src/testing/core/index.ts
+++ b/src/testing/core/index.ts
@@ -1,16 +1,15 @@
-export {
-  MockLink,
-  mockSingleLink,
+export type {
   MockedResponse,
   MockLinkOptions,
-  ResultFunction
-} from './mocking/mockLink';
+  ResultFunction,
+} from "./mocking/mockLink.js";
+export { MockLink, mockSingleLink } from "./mocking/mockLink.js";
 export {
   MockSubscriptionLink,
-  mockObservableLink
-} from './mocking/mockSubscriptionLink';
-export { createMockClient } from './mocking/mockClient';
-export { default as subscribeAndCount } from './subscribeAndCount';
-export { itAsync } from './itAsync';
-export { wait, tick } from './wait'
-export * from './withConsoleSpy';
+  mockObservableLink,
+} from "./mocking/mockSubscriptionLink.js";
+export { createMockClient } from "./mocking/mockClient.js";
+export { default as subscribeAndCount } from "./subscribeAndCount.js";
+export { itAsync } from "./itAsync.js";
+export { wait, tick } from "./wait.js";
+export * from "./withConsoleSpy.js";
diff --git a/src/testing/core/mocking/mockClient.ts b/src/testing/core/mocking/mockClient.ts
--- a/src/testing/core/mocking/mockClient.ts
+++ b/src/testing/core/mocking/mockClient.ts
@@ -1,8 +1,9 @@
-import { DocumentNode } from 'graphql';
+import type { DocumentNode } from 'graphql';
 
-import { ApolloClient } from '../../../core';
-import { InMemoryCache, NormalizedCacheObject } from '../../../cache';
-import { mockSingleLink } from './mockLink';
+import { ApolloClient } from '../../../core/index.js';
+import type { NormalizedCacheObject } from '../../../cache/index.js';
+import { InMemoryCache } from '../../../cache/index.js';
+import { mockSingleLink } from './mockLink.js';
 
 export function createMockClient<TData>(
   data: TData,
diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts
--- a/src/testing/core/mocking/mockLink.ts
+++ b/src/testing/core/mocking/mockLink.ts
@@ -1,14 +1,14 @@
-import { invariant } from '../../../utilities/globals';
+import { invariant } from '../../../utilities/globals/index.js';
 
-import { print } from 'graphql';
 import { equal } from '@wry/equality';
 
-import {
-  ApolloLink,
+import type {
   Operation,
   GraphQLRequest,
-  FetchResult,
-} from '../../../link/core';
+  FetchResult} from '../../../link/core/index.js';
+import {
+  ApolloLink
+} from '../../../link/core/index.js';
 
 import {
   Observable,
@@ -17,7 +17,8 @@ import {
   removeConnectionDirectiveFromDocument,
   cloneDeep,
   stringifyForDisplay,
-} from '../../../utilities';
+  print
+} from '../../../utilities/index.js';
 
 export type ResultFunction<T> = () => T;
 
@@ -116,7 +117,7 @@ ${unmatchedVars.map(d => `  ${stringifyForDisplay(d)}`).join('\n')}
 
       if (this.showWarnings) {
         console.warn(
-          configError.message + 
+          configError.message +
             '\nThis typically indicates a configuration error in your mocks ' +
             'setup, usually due to a typo or mismatched variable.'
         );
diff --git a/src/testing/core/mocking/mockQueryManager.ts b/src/testing/core/mocking/mockQueryManager.ts
--- a/src/testing/core/mocking/mockQueryManager.ts
+++ b/src/testing/core/mocking/mockQueryManager.ts
@@ -1,6 +1,7 @@
-import { QueryManager } from '../../../core/QueryManager';
-import { mockSingleLink, MockedResponse } from './mockLink';
-import { InMemoryCache } from '../../../cache';
+import { QueryManager } from '../../../core/QueryManager.js';
+import type { MockedResponse } from './mockLink.js';
+import { mockSingleLink } from './mockLink.js';
+import { InMemoryCache } from '../../../cache/index.js';
 
 // Helper method for the tests that construct a query manager out of a
 // a list of mocked responses for a mocked network interface.
diff --git a/src/testing/core/mocking/mockSubscriptionLink.ts b/src/testing/core/mocking/mockSubscriptionLink.ts
--- a/src/testing/core/mocking/mockSubscriptionLink.ts
+++ b/src/testing/core/mocking/mockSubscriptionLink.ts
@@ -1,5 +1,6 @@
-import { Observable } from '../../../utilities';
-import { ApolloLink, FetchResult, Operation } from '../../../link/core';
+import { Observable } from '../../../utilities/index.js';
+import type { FetchResult, Operation } from '../../../link/core/index.js';
+import { ApolloLink } from '../../../link/core/index.js';
 
 export interface MockedSubscription {
   request: Operation;
diff --git a/src/testing/core/mocking/mockWatchQuery.ts b/src/testing/core/mocking/mockWatchQuery.ts
--- a/src/testing/core/mocking/mockWatchQuery.ts
+++ b/src/testing/core/mocking/mockWatchQuery.ts
@@ -1,6 +1,6 @@
-import { MockedResponse } from './mockLink';
-import mockQueryManager from './mockQueryManager';
-import { ObservableQuery } from '../../../core';
+import type { MockedResponse } from './mockLink.js';
+import mockQueryManager from './mockQueryManager.js';
+import type { ObservableQuery } from '../../../core/index.js';
 
 export default (...mockedResponses: MockedResponse[]): ObservableQuery<any> => {
   const queryManager = mockQueryManager(...mockedResponses);
diff --git a/src/testing/core/observableToPromise.ts b/src/testing/core/observableToPromise.ts
--- a/src/testing/core/observableToPromise.ts
+++ b/src/testing/core/observableToPromise.ts
@@ -1,5 +1,5 @@
-import { ObservableQuery, ApolloQueryResult } from '../../core';
-import { ObservableSubscription } from '../../utilities';
+import type { ObservableQuery, ApolloQueryResult } from '../../core/index.js';
+import type { ObservableSubscription } from '../../utilities/index.js';
 
 /**
  *
diff --git a/src/testing/core/subscribeAndCount.ts b/src/testing/core/subscribeAndCount.ts
--- a/src/testing/core/subscribeAndCount.ts
+++ b/src/testing/core/subscribeAndCount.ts
@@ -1,4 +1,5 @@
-import { ObservableSubscription, asyncMap, Observable } from '../../utilities';
+import type { ObservableSubscription, Observable } from '../../utilities/index.js';
+import { asyncMap } from '../../utilities/index.js';
 
 export default function subscribeAndCount<TResult>(
   reject: (reason: any) => any,
diff --git a/src/testing/index.ts b/src/testing/index.ts
--- a/src/testing/index.ts
+++ b/src/testing/index.ts
@@ -1,3 +1,4 @@
-import '../utilities/globals';
-export { MockedProvider, MockedProviderProps } from './react/MockedProvider';
-export * from './core';
+import '../utilities/globals/index.js';
+export type { MockedProviderProps } from './react/MockedProvider.js';
+export { MockedProvider } from './react/MockedProvider.js';
+export * from './core/index.js';
diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts
new file mode 100644
--- /dev/null
+++ b/src/testing/matchers/index.d.ts
@@ -0,0 +1,32 @@
+import type {
+  ApolloClient,
+  DocumentNode,
+  OperationVariables,
+} from "../../core/index.js";
+
+interface ApolloCustomMatchers<R = void, T = {}> {
+  /**
+   * Used to determine if two GraphQL query documents are equal to each other by
+   * comparing their printed values. The document must be parsed by `gql`.
+   */
+  toMatchDocument(document: DocumentNode): R;
+
+  /**
+   * Used to determine if the Suspense cache has a cache entry.
+   */
+  toHaveSuspenseCacheEntryUsing: T extends ApolloClient<any>
+    ? (
+        query: DocumentNode,
+        options?: {
+          variables?: OperationVariables;
+          queryKey?: string | number | any[];
+        }
+      ) => R
+    : { error: "matcher needs to be called on an ApolloClient instance" };
+}
+
+declare global {
+  namespace jest {
+    interface Matchers<R = void, T = {}> extends ApolloCustomMatchers<R, T> {}
+  }
+}
diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts
new file mode 100644
--- /dev/null
+++ b/src/testing/matchers/index.ts
@@ -0,0 +1,8 @@
+import { expect } from "@jest/globals";
+import { toMatchDocument } from "./toMatchDocument.js";
+import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js";
+
+expect.extend({
+  toHaveSuspenseCacheEntryUsing,
+  toMatchDocument,
+});
diff --git a/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts b/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts
new file mode 100644
--- /dev/null
+++ b/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts
@@ -0,0 +1,43 @@
+import type { MatcherFunction } from "expect";
+import type { DocumentNode } from "graphql";
+import type { OperationVariables } from "../../core/index.js";
+import { ApolloClient } from "../../core/index.js";
+import { canonicalStringify } from "../../cache/index.js";
+import { getSuspenseCache } from "../../react/cache/index.js";
+import type { CacheKey } from "../../react/cache/types.js";
+
+export const toHaveSuspenseCacheEntryUsing: MatcherFunction<
+  [
+    query: DocumentNode,
+    options: {
+      variables?: OperationVariables;
+      queryKey?: string | number | any[];
+    },
+  ]
+> = function (
+  client,
+  query,
+  { variables, queryKey = [] } = Object.create(null)
+) {
+  if (!(client instanceof ApolloClient)) {
+    throw new Error("Actual must be an instance of `ApolloClient`");
+  }
+
+  const suspenseCache = getSuspenseCache(client);
+
+  const cacheKey: CacheKey = [
+    query,
+    canonicalStringify(variables),
+    ...([] as any[]).concat(queryKey),
+  ];
+  const queryRef = suspenseCache["queryRefs"].lookupArray(cacheKey)?.current;
+
+  return {
+    pass: !!queryRef,
+    message: () => {
+      return `Expected suspense cache ${
+        queryRef ? "not " : ""
+      }to have cache entry using key`;
+    },
+  };
+};
diff --git a/src/testing/matchers/toMatchDocument.ts b/src/testing/matchers/toMatchDocument.ts
new file mode 100644
--- /dev/null
+++ b/src/testing/matchers/toMatchDocument.ts
@@ -0,0 +1,63 @@
+import { checkDocument, print } from "../../utilities/index.js";
+import type { DocumentNode } from "../../core/index.js";
+import type { MatcherFunction } from "expect";
+
+export const toMatchDocument: MatcherFunction<[document: DocumentNode]> =
+  function (actual, document) {
+    const hint = this.utils.matcherHint("toMatchDocument");
+    const actualDocument = print(
+      validateDocument(
+        actual,
+        hint +
+          `\n\n${this.utils.RECEIVED_COLOR(
+            "received"
+          )} document must be a parsed document.`
+      )
+    );
+    const expectedDocument = print(
+      validateDocument(
+        document,
+        hint +
+          `\n\n${this.utils.EXPECTED_COLOR(
+            "expected"
+          )} document must be a parsed document.`
+      )
+    );
+
+    const pass = actualDocument === expectedDocument;
+
+    return {
+      pass,
+      message: () => {
+        const hint = this.utils.matcherHint(
+          "toMatchDocument",
+          undefined,
+          undefined,
+          { isNot: this.isNot }
+        );
+
+        if (pass) {
+          return (
+            hint +
+            "\n\n" +
+            "Received:\n\n" +
+            this.utils.RECEIVED_COLOR(actualDocument)
+          );
+        }
+
+        return (
+          hint + "\n\n" + this.utils.diff(expectedDocument, actualDocument)
+        );
+      },
+    };
+  };
+
+function validateDocument(document: unknown, message: string) {
+  try {
+    checkDocument(document as DocumentNode);
+  } catch (e) {
+    throw new Error(message);
+  }
+
+  return document as DocumentNode;
+}
diff --git a/src/testing/react/MockedProvider.tsx b/src/testing/react/MockedProvider.tsx
--- a/src/testing/react/MockedProvider.tsx
+++ b/src/testing/react/MockedProvider.tsx
@@ -1,12 +1,14 @@
 import * as React from 'react';
 
-import { ApolloClient, DefaultOptions } from '../../core';
-import { InMemoryCache as Cache } from '../../cache';
-import { ApolloProvider } from '../../react/context';
-import { MockLink, MockedResponse } from '../core';
-import { ApolloLink } from '../../link/core';
-import { Resolvers } from '../../core';
-import { ApolloCache } from '../../cache';
+import type { DefaultOptions } from '../../core/index.js';
+import { ApolloClient } from '../../core/index.js';
+import { InMemoryCache as Cache } from '../../cache/index.js';
+import { ApolloProvider } from '../../react/context/index.js';
+import type { MockedResponse } from '../core/index.js';
+import { MockLink } from '../core/index.js';
+import type { ApolloLink } from '../../link/core/index.js';
+import type { Resolvers } from '../../core/index.js';
+import type { ApolloCache } from '../../cache/index.js';
 
 export interface MockedProviderProps<TSerializedCache = {}> {
   mocks?: ReadonlyArray<MockedResponse>;
@@ -55,13 +57,17 @@ export class MockedProvider extends React.Component<
       resolvers,
     });
 
-    this.state = { client };
+    this.state = {
+      client,
+    };
   }
 
   public render() {
     const { children, childProps } = this.props;
+    const { client } = this.state;
+
     return React.isValidElement(children) ? (
-      <ApolloProvider client={this.state.client}>
+      <ApolloProvider client={client}>
         {React.cloneElement(React.Children.only(children), { ...childProps })}
       </ApolloProvider>
     ) : null;
diff --git a/src/utilities/common/__tests__/omitDeep.ts b/src/utilities/common/__tests__/omitDeep.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/common/__tests__/omitDeep.ts
@@ -0,0 +1,137 @@
+import { omitDeep } from "../omitDeep";
+
+test("omits the key from a shallow object", () => {
+  expect(omitDeep({ omit: "a", keep: "b", other: "c" }, "omit")).toEqual({
+    keep: "b",
+    other: "c",
+  });
+});
+
+test("omits the key from arbitrarily nested object", () => {
+  expect(
+    omitDeep(
+      {
+        omit: "a",
+        keep: {
+          omit: "a",
+          keep: "b",
+          other: { omit: "a", keep: "b", other: "c" },
+        },
+      },
+      "omit"
+    )
+  ).toEqual({
+    keep: {
+      keep: "b",
+      other: { keep: "b", other: "c" },
+    },
+  });
+});
+
+test("omits the key from arrays", () => {
+  expect(
+    omitDeep(
+      [
+        { omit: "a", keep: "b", other: "c" },
+        { omit: "a", keep: "b", other: "c" },
+      ],
+      "omit"
+    )
+  ).toEqual([
+    { keep: "b", other: "c" },
+    { keep: "b", other: "c" },
+  ]);
+});
+
+test("omits the key from arbitrarily nested arrays", () => {
+  expect(
+    omitDeep(
+      [
+        [{ omit: "a", keep: "b", other: "c" }],
+        [
+          { omit: "a", keep: "b", other: "c" },
+          [{ omit: "a", keep: "b", other: "c" }],
+        ],
+      ],
+      "omit"
+    )
+  ).toEqual([
+    [{ keep: "b", other: "c" }],
+    [{ keep: "b", other: "c" }, [{ keep: "b", other: "c" }]],
+  ]);
+});
+
+test("returns primitives unchanged", () => {
+  expect(omitDeep("a", "ignored")).toBe("a");
+  expect(omitDeep(1, "ignored")).toBe(1);
+  expect(omitDeep(true, "ignored")).toBe(true);
+  expect(omitDeep(null, "ignored")).toBe(null);
+  expect(omitDeep(undefined, "ignored")).toBe(undefined);
+  expect(omitDeep(Symbol.for("foo"), "ignored")).toBe(Symbol.for("foo"));
+});
+
+test("handles circular references", () => {
+  let b: any;
+  const a = { omit: "foo", b };
+  b = { a, omit: "foo" };
+  a.b = b;
+
+  const variables = { a, b, omit: "foo" };
+
+  const result = omitDeep(variables, "omit");
+
+  expect(result).not.toHaveProperty("omit");
+  expect(result.a).not.toHaveProperty("omit");
+  expect(result.b).not.toHaveProperty("omit");
+});
+
+test("returns same object unmodified if key is not found", () => {
+  const obj = {
+    a: "a",
+    b: "b",
+    c: { d: "d", e: "e" },
+  };
+
+  const arr = [{ a: "a", b: "b", c: "c" }, { foo: "bar" }];
+
+  expect(omitDeep(obj, "omit")).toBe(obj);
+  expect(omitDeep(arr, "omit")).toBe(arr);
+});
+
+test("returns unmodified subtrees for subtrees that do not contain the key", () => {
+  const original = {
+    a: "a",
+    foo: { bar: "true" },
+    baz: [{ foo: "bar" }],
+    omitOne: [{ foo: "bar", omit: true }, { foo: "bar" }],
+  };
+
+  const result = omitDeep(original, "omit");
+
+  expect(result).not.toBe(original);
+  expect(result.foo).toBe(original.foo);
+  expect(result.baz).toBe(original.baz);
+  expect(result.omitOne).not.toBe(original.omitOne);
+  expect(result.omitOne[0]).not.toBe(original.omitOne[0]);
+  expect(result.omitOne[1]).toBe(original.omitOne[1]);
+});
+
+test("only considers plain objects and ignores class instances when omitting properties", () => {
+  class Thing {
+    foo = "bar";
+    omit = false;
+  }
+
+  const thing = new Thing();
+  const original = { thing };
+
+  const result = omitDeep(original, "omit");
+
+  expect(result.thing).toBe(thing);
+  expect(result.thing).toHaveProperty("omit", false);
+
+  const modifiedThing = omitDeep(thing, "omit");
+
+  expect(modifiedThing).toBe(thing);
+  expect(modifiedThing).toHaveProperty("omit", false);
+});
diff --git a/src/utilities/common/__tests__/stripTypename.ts b/src/utilities/common/__tests__/stripTypename.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/common/__tests__/stripTypename.ts
@@ -0,0 +1,60 @@
+import { stripTypename } from "../stripTypename";
+
+test("omits __typename from a shallow object", () => {
+  expect(
+    stripTypename({ __typename: "Person", firstName: "Foo", lastName: "Bar" })
+  ).toEqual({ firstName: "Foo", lastName: "Bar" });
+});
+
+test("omits __typename from arbitrarily nested object", () => {
+  expect(
+    stripTypename({
+      __typename: "Profile",
+      user: {
+        __typename: "User",
+        firstName: "Foo",
+        lastName: "Bar",
+        location: {
+          __typename: "Location",
+          city: "Denver",
+          country: "USA",
+        },
+      },
+    })
+  ).toEqual({
+    user: {
+      firstName: "Foo",
+      lastName: "Bar",
+      location: {
+        city: "Denver",
+        country: "USA",
+      },
+    },
+  });
+});
+
+test("omits the __typename from arrays", () => {
+  expect(
+    stripTypename([
+      { __typename: "Todo", name: "Take out trash" },
+      { __typename: "Todo", name: "Clean room" },
+    ])
+  ).toEqual([{ name: "Take out trash" }, { name: "Clean room" }]);
+});
+
+test("omits __typename from arbitrarily nested arrays", () => {
+  expect(
+    stripTypename([
+      [{ __typename: "Foo", foo: "foo" }],
+      [{ __typename: "Bar", bar: "bar" }, [{ __typename: "Baz", baz: "baz" }]],
+    ])
+  ).toEqual([[{ foo: "foo" }], [{ bar: "bar" }, [{ baz: "baz" }]]]);
+});
+
+test("returns primitives unchanged", () => {
+  expect(stripTypename("a")).toBe("a");
+  expect(stripTypename(1)).toBe(1);
+  expect(stripTypename(true)).toBe(true);
+  expect(stripTypename(null)).toBe(null);
+  expect(stripTypename(undefined)).toBe(undefined);
+});
diff --git a/src/utilities/graphql/__tests__/DocumentTransform.ts b/src/utilities/graphql/__tests__/DocumentTransform.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/graphql/__tests__/DocumentTransform.ts
@@ -0,0 +1,676 @@
+import { DocumentTransform } from "../DocumentTransform";
+import { isMutationOperation, isQueryOperation } from "../operations";
+import { removeDirectivesFromDocument } from "../transform";
+import { gql } from "graphql-tag";
+import { DocumentNode, visit, Kind } from "graphql";
+
+function stripDirective(directive: string) {
+  return (document: DocumentNode) => {
+    return removeDirectivesFromDocument([{ name: directive }], document)!;
+  };
+}
+
+function renameDirective(target: string, replacement: string) {
+  return (document: DocumentNode) => {
+    return visit(document, {
+      Directive(node) {
+        if (node.name.value === target) {
+          return {
+            ...node,
+            name: { kind: Kind.NAME, value: replacement },
+          };
+        }
+      },
+    });
+  };
+}
+
+function addClientDirectiveToField(fieldName: string) {
+  return (document: DocumentNode) => {
+    return visit(document, {
+      Field: {
+        leave: (node) => {
+          if (node.name.value === fieldName) {
+            return {
+              ...node,
+              directives: [
+                {
+                  kind: Kind.DIRECTIVE,
+                  name: { kind: Kind.NAME, value: "client" },
+                },
+              ],
+            };
+          }
+        },
+      },
+    });
+  };
+}
+
+test("can transform a document", () => {
+  const query = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const transform = new DocumentTransform(stripDirective("client"));
+
+  const result = transform.transformDocument(query);
+
+  expect(result).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `);
+});
+
+test("returns unmodified document if trying to transform an already computed result", () => {
+  const query = gql`
+    query TestQuery {
+      user {
+        name @custom
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const cachedTransform = new DocumentTransform(stripDirective("client"));
+  const uncachedTransform = new DocumentTransform(stripDirective("custom"), {
+    cache: false,
+  });
+
+  const withoutClient = cachedTransform.transformDocument(query);
+  const withoutCustom = uncachedTransform.transformDocument(query);
+
+  expect(cachedTransform.transformDocument(withoutClient)).toBe(withoutClient);
+
+  expect(uncachedTransform.transformDocument(withoutCustom)).toBe(
+    withoutCustom
+  );
+});
+
+test("caches the result of the transform by default", () => {
+  const query = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const expected = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `;
+
+  const transform = jest.fn(stripDirective("client"));
+  const documentTransform = new DocumentTransform(transform);
+
+  const result1 = documentTransform.transformDocument(query);
+
+  expect(result1).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(1);
+
+  const result2 = documentTransform.transformDocument(query);
+
+  expect(result2).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(1);
+});
+
+test("allows custom cache keys to be defined", () => {
+  const query = gql`
+    query TestQuery {
+      user @network {
+        name
+      }
+    }
+  `;
+
+  const onlineQuery = gql`
+    query TestQuery {
+      user {
+        name
+      }
+    }
+  `;
+
+  const offlineQuery = gql`
+    query TestQuery {
+      user @client {
+        name
+      }
+    }
+  `;
+
+  let online = true;
+
+  const onlineTransform = new DocumentTransform(stripDirective("network"));
+  const offlineTransform = new DocumentTransform(
+    renameDirective("network", "client")
+  );
+
+  const transform = jest.fn((document: DocumentNode) => {
+    return online
+      ? onlineTransform.transformDocument(document)
+      : offlineTransform.transformDocument(document);
+  });
+
+  const documentTransform = new DocumentTransform(transform, {
+    getCacheKey: (document) => [document, online],
+  });
+
+  const result1 = documentTransform.transformDocument(query);
+
+  expect(result1).toMatchDocument(onlineQuery);
+  expect(transform).toHaveBeenCalledTimes(1);
+
+  online = false;
+
+  const result2 = documentTransform.transformDocument(query);
+
+  expect(result2).toMatchDocument(offlineQuery);
+  expect(transform).toHaveBeenCalledTimes(2);
+
+  online = true;
+
+  const result3 = documentTransform.transformDocument(query);
+
+  expect(result3).toMatchDocument(onlineQuery);
+  expect(transform).toHaveBeenCalledTimes(2);
+
+  online = false;
+
+  const result4 = documentTransform.transformDocument(query);
+
+  expect(result4).toMatchDocument(offlineQuery);
+  expect(transform).toHaveBeenCalledTimes(2);
+});
+
+test("can disable caching the result output", () => {
+  const query = gql`
+    query {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const expected = gql`
+    query {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `;
+
+  const transform = jest.fn(stripDirective("client"));
+  const documentTransform = new DocumentTransform(transform, { cache: false });
+
+  const result1 = documentTransform.transformDocument(query);
+
+  expect(result1).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(1);
+
+  const result2 = documentTransform.transformDocument(query);
+
+  expect(result2).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(2);
+});
+
+test("can combine 2 transforms with `concat`", async () => {
+  const query = gql`
+    query TestQuery {
+      user @nonreactive {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const stripClient = new DocumentTransform(stripDirective("client"));
+  const stripNonReactive = new DocumentTransform(stripDirective("nonreactive"));
+  const documentTransform = stripClient.concat(stripNonReactive);
+
+  const result = documentTransform.transformDocument(query);
+
+  expect(result).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `);
+});
+
+test("runs concatenated transform after original transform", () => {
+  const query = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const expected = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `;
+
+  const addClientDirectiveToName = new DocumentTransform(
+    addClientDirectiveToField("name")
+  );
+
+  expect(addClientDirectiveToName.transformDocument(query)).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name @client
+        isLoggedIn @client
+      }
+    }
+  `);
+
+  const stripClient = new DocumentTransform(stripDirective("client"));
+  const documentTransform = addClientDirectiveToName.concat(stripClient);
+
+  const result = documentTransform.transformDocument(query);
+
+  expect(result).toMatchDocument(expected);
+});
+
+test("can combine multiple transforms with `concat`", async () => {
+  const query = gql`
+    query TestQuery {
+      user @nonreactive {
+        name @connection
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const stripClient = new DocumentTransform(stripDirective("client"));
+  const stripNonReactive = new DocumentTransform(stripDirective("nonreactive"));
+  const stripConnection = new DocumentTransform(stripDirective("connection"));
+  const documentTransform = stripClient
+    .concat(stripNonReactive)
+    .concat(stripConnection);
+
+  const result = documentTransform.transformDocument(query);
+
+  expect(result).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `);
+});
+
+test("caches the result output from a combined transform when both transforms are cached", async () => {
+  const query = gql`
+    query TestQuery {
+      user @nonreactive {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const expected = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `;
+
+  const stripClient = jest.fn(stripDirective("client"));
+  const stripNonReactive = jest.fn(stripDirective("nonreactive"));
+
+  const stripClientTransform = new DocumentTransform(stripClient);
+  const stripNonReactiveTransform = new DocumentTransform(stripNonReactive);
+  const documentTransform = stripClientTransform.concat(
+    stripNonReactiveTransform
+  );
+
+  const result = documentTransform.transformDocument(query);
+
+  expect(result).toMatchDocument(expected);
+  expect(stripClient).toHaveBeenCalledTimes(1);
+  expect(stripNonReactive).toHaveBeenCalledTimes(1);
+
+  const result2 = documentTransform.transformDocument(query);
+
+  expect(result2).toMatchDocument(expected);
+  expect(stripClient).toHaveBeenCalledTimes(1);
+  expect(stripNonReactive).toHaveBeenCalledTimes(1);
+});
+
+test("allows non cached transforms to be run when concatenated", async () => {
+  const query = gql`
+    query TestQuery {
+      user @nonreactive {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const expected = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `;
+
+  const stripClient = jest.fn(stripDirective("client"));
+  const stripNonReactive = jest.fn(stripDirective("nonreactive"));
+
+  const stripClientTransform = new DocumentTransform(stripClient, {
+    cache: true,
+  });
+  const stripNonReactiveTransform = new DocumentTransform(stripNonReactive, {
+    cache: false,
+  });
+
+  // Try ordering the transforms both ways to ensure the cached transform has
+  // no effect on whether the non-cached transform runs
+  const documentTransform =
+    stripNonReactiveTransform.concat(stripClientTransform);
+
+  const reversedTransform = stripClientTransform.concat(
+    stripNonReactiveTransform
+  );
+
+  const result = documentTransform.transformDocument(query);
+
+  expect(result).toMatchDocument(expected);
+  expect(stripClient).toHaveBeenCalledTimes(1);
+  expect(stripNonReactive).toHaveBeenCalledTimes(1);
+
+  const result2 = documentTransform.transformDocument(query);
+
+  expect(result2).toMatchDocument(expected);
+  // Even though stripClient is cached, it is called a second time because
+  // stripNonReactive returns a new document instance each time it runs.
+  expect(stripClient).toHaveBeenCalledTimes(2);
+  expect(stripNonReactive).toHaveBeenCalledTimes(2);
+
+  stripClient.mockClear();
+  stripNonReactive.mockClear();
+
+  const reversed = reversedTransform.transformDocument(query);
+
+  expect(reversed).toMatchDocument(expected);
+  expect(stripClient).toHaveBeenCalledTimes(1);
+  expect(stripNonReactive).toHaveBeenCalledTimes(1);
+
+  const reversed2 = reversedTransform.transformDocument(query);
+
+  expect(reversed2).toMatchDocument(expected);
+  // Now that the cached transform is first, we can make sure it doesn't run
+  // again. We verify the non-cached that is run after the cached transform does
+  // get a chance to execute.
+  expect(stripClient).toHaveBeenCalledTimes(1);
+  expect(stripNonReactive).toHaveBeenCalledTimes(2);
+});
+
+test("can conditionally run transforms using `DocumentTransform.split`", () => {
+  const mutation = gql`
+    mutation TestMutation {
+      incrementCounter @client {
+        count
+      }
+    }
+  `;
+
+  const query = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const documentTransform = DocumentTransform.split(
+    isQueryOperation,
+    new DocumentTransform(stripDirective("client"))
+  );
+
+  const queryResult = documentTransform.transformDocument(query);
+  const mutationResult = documentTransform.transformDocument(mutation);
+
+  expect(queryResult).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `);
+
+  expect(mutationResult).toMatchDocument(mutation);
+});
+
+test("properly caches the result of `filter` when the original transform is cached", () => {
+  const query = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const expected = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `;
+
+  const transform = jest.fn(stripDirective("client"));
+  const documentTransform = DocumentTransform.split(
+    isQueryOperation,
+    new DocumentTransform(transform, { cache: true })
+  );
+
+  const result = documentTransform.transformDocument(query);
+
+  expect(result).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(1);
+
+  const result2 = documentTransform.transformDocument(query);
+
+  expect(result2).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(1);
+});
+
+test("reruns transform returned from `DocumentTransform.split` when the original transform is not cached", () => {
+  const query = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const expected = gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `;
+
+  const transform = jest.fn(stripDirective("client"));
+  const documentTransform = DocumentTransform.split(
+    isQueryOperation,
+    new DocumentTransform(transform, { cache: false })
+  );
+
+  const result = documentTransform.transformDocument(query);
+
+  expect(result).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(1);
+
+  const result2 = documentTransform.transformDocument(query);
+
+  expect(result2).toMatchDocument(expected);
+  expect(transform).toHaveBeenCalledTimes(2);
+});
+
+test("properly handles combinations of `DocumentTransform.split` and `filter`", () => {
+  const mutation = gql`
+    mutation TestMutation {
+      incrementCounter @client {
+        count @nonreactive
+      }
+    }
+  `;
+
+  const query = gql`
+    query TestQuery {
+      user {
+        name @nonreactive
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const stripClient = new DocumentTransform(stripDirective("client"));
+  const stripNonReactive = new DocumentTransform(stripDirective("nonreactive"));
+
+  // Strip both @client and @nonreactive but only on query types
+  const queryOnlyTransform = DocumentTransform.split(
+    isQueryOperation,
+    stripClient.concat(stripNonReactive)
+  );
+
+  // Only strip @client from mutations but remove @nonreactive from all
+  const conditionalStrip = DocumentTransform.split(
+    isMutationOperation,
+    stripClient
+  ).concat(stripNonReactive);
+
+  expect(queryOnlyTransform.transformDocument(query)).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `);
+
+  expect(queryOnlyTransform.transformDocument(mutation)).toMatchDocument(
+    mutation
+  );
+
+  expect(conditionalStrip.transformDocument(query)).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `);
+
+  expect(conditionalStrip.transformDocument(mutation)).toMatchDocument(gql`
+    mutation TestMutation {
+      incrementCounter {
+        count
+      }
+    }
+  `);
+});
+
+test("executes other transform when using `DocumentTransform.split` when condition is false", () => {
+  const mutation = gql`
+    mutation TestMutation {
+      incrementCounter @client {
+        count @nonreactive
+      }
+    }
+  `;
+
+  const query = gql`
+    query TestQuery {
+      user {
+        name @nonreactive
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const stripClient = new DocumentTransform(stripDirective("client"));
+  const stripNonReactive = new DocumentTransform(stripDirective("nonreactive"));
+
+  // strip both directives for queries, but only @nonreactive for mutations
+  const documentTransform = DocumentTransform.split(
+    isQueryOperation,
+    stripClient.concat(stripNonReactive),
+    stripNonReactive
+  );
+
+  expect(documentTransform.transformDocument(query)).toMatchDocument(gql`
+    query TestQuery {
+      user {
+        name
+        isLoggedIn
+      }
+    }
+  `);
+
+  expect(documentTransform.transformDocument(mutation)).toMatchDocument(gql`
+    mutation TestMutation {
+      incrementCounter @client {
+        count
+      }
+    }
+  `);
+});
+
+test("errors when passing a document that has not been parsed with `gql`", () => {
+  const query = `
+    query TestQuery {
+      user {
+        name
+        isLoggedIn @client
+      }
+    }
+  `;
+
+  const documentTransform = new DocumentTransform((document) => document);
+
+  expect(() => {
+    documentTransform.transformDocument(query as unknown as DocumentNode);
+  }).toThrowError(/wrap the query string in a "gql" tag/);
+});