diff --git a/CHANGELOG.md b/CHANGELOG.md
index e72d79890ca..7dea902be0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,19 @@
- Add `GraphQLWsLink` in `@apollo/client/ws/subscriptions`. This link is similar to the existing `WebSocketLink` in `@apollo/client/link/ws`, but uses the newer [`graphql-ws`](https://www.npmjs.com/package/graphql-ws) package and protocol instead of the older `subscriptions-transport-ws` implementation.
[@glasser](https://github.com/glasser) in [#9369](https://github.com/apollographql/apollo-client/pull/9369)
+## Apollo Client 3.5.8 (2022-01-24)
+
+### Bug Fixes
+
+- Fix the type of the `called` property returned by `useQuery()` and `useLazyQuery()`.
+ [@sztadii](https://github.com/sztadii) in [#9304](https://github.com/apollographql/apollo-client/pull/9304)
+
+### Bug Fixes (by [@brainkim](https://github.com/brainkim) in [#9328](https://github.com/apollographql/apollo-client/pull/9328))
+
+- Fix `refetch()` not being called when `skip` is true.
+- Fix the promise returned from the `useLazyQuery()` execution function having stale variables.
+- Fix the promise returned from the `useLazyQuery()` execution function not rejecting when a query errors.
+
## Apollo Client 3.5.7 (2022-01-10)
### Bug Fixes
diff --git a/docs/package.json b/docs/package.json
index 8b3f54b3f1b..624e8ee318a 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -7,13 +7,13 @@
"types": "cd .. && typedoc --json ./docs/src/gatsby-theme-apollo-docs/docs.json ./src/index.ts"
},
"dependencies": {
- "gatsby": "3.0.0",
- "gatsby-theme-apollo-docs": "5.3.12",
+ "gatsby": "3.14.6",
+ "gatsby-theme-apollo-docs": "6.0.0",
"react": "16.14.0",
"react-dom": "16.14.0"
},
"devDependencies": {
- "typedoc": "0.22.10",
+ "typedoc": "0.22.11",
"typescript": "4.4.4"
}
}
diff --git a/docs/source/caching/cache-field-behavior.md b/docs/source/caching/cache-field-behavior.md
index de0cb3dd0ca..f3f0d998d68 100644
--- a/docs/source/caching/cache-field-behavior.md
+++ b/docs/source/caching/cache-field-behavior.md
@@ -322,7 +322,7 @@ query BookWithAuthorLanguages {
}
```
-In this case, the `favoriteBook.authors` field is no longer just a single object, but an array of authors, so it's even more imporant to define a custom `merge` function to prevent loss of data by replacement:
+In this case, the `favoriteBook.authors` field is no longer just a single object, but an array of authors, so it's even more important to define a custom `merge` function to prevent loss of data by replacement:
```ts
const cache = new InMemoryCache({
diff --git a/package-lock.json b/package-lock.json
index 27dd8da28a5..8b517ee294c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
"zen-observable-ts": "^1.2.0"
},
"devDependencies": {
- "@babel/parser": "7.16.7",
+ "@babel/parser": "7.16.12",
"@graphql-tools/schema": "8.3.1",
"@rollup/plugin-node-resolve": "11.2.1",
"@testing-library/react": "12.1.2",
@@ -34,13 +34,12 @@
"@types/hoist-non-react-statics": "3.3.1",
"@types/jest": "27.4.0",
"@types/lodash": "4.14.178",
- "@types/node": "16.11.19",
+ "@types/node": "16.11.21",
"@types/react": "17.0.34",
"@types/react-dom": "17.0.2",
- "@types/use-sync-external-store": "^0.0.3",
"acorn": "8.6.0",
"bundlesize": "0.18.1",
- "cross-fetch": "3.1.4",
+ "cross-fetch": "3.1.5",
"crypto-hash": "1.3.0",
"fetch-mock": "9.11.0",
"glob": "7.2.0",
@@ -53,17 +52,16 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"recast": "0.21.0",
- "resolve": "1.21.0",
+ "resolve": "1.22.0",
"rimraf": "3.0.2",
- "rollup": "2.63.0",
+ "rollup": "2.66.1",
"rollup-plugin-terser": "7.0.2",
"rxjs": "6.6.7",
"subscriptions-transport-ws": "0.11.0",
"terser": "5.10.0",
- "ts-jest": "27.1.2",
+ "ts-jest": "27.1.3",
"ts-node": "10.4.0",
"typescript": "4.5.2",
- "use-sync-external-store": "1.0.0-rc.0",
"wait-for-observables": "1.0.3",
"whatwg-fetch": "3.6.2"
},
@@ -73,9 +71,8 @@
"peerDependencies": {
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
"graphql-ws": "^5.5.5",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0-beta",
- "subscriptions-transport-ws": "^0.9.0 || ^0.11.0",
- "use-sync-external-store": "^1.0.0 || ^1.0.0-rc || ^1.0.0-beta"
+ "react": "^16.8.0 || ^17.0.0",
+ "subscriptions-transport-ws": "^0.9.0 || ^0.11.0"
},
"peerDependenciesMeta": {
"graphql-ws": {
@@ -86,9 +83,6 @@
},
"subscriptions-transport-ws": {
"optional": true
- },
- "use-sync-external-store": {
- "optional": true
}
}
},
@@ -407,9 +401,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz",
- "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==",
+ "version": "7.16.12",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
+ "integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -1304,9 +1298,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "16.11.19",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz",
- "integrity": "sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng==",
+ "version": "16.11.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
+ "integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==",
"dev": true
},
"node_modules/@types/prettier": {
@@ -1371,12 +1365,6 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
- "node_modules/@types/use-sync-external-store": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
- "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
- "dev": true
- },
"node_modules/@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@@ -2223,12 +2211,12 @@
"dev": true
},
"node_modules/cross-fetch": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
- "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
+ "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dev": true,
"dependencies": {
- "node-fetch": "2.6.1"
+ "node-fetch": "2.6.7"
}
},
"node_modules/cross-spawn": {
@@ -4473,12 +4461,45 @@
}
},
"node_modules/node-fetch": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
- "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
"engines": {
"node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
+ "dev": true
+ },
+ "node_modules/node-fetch/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
+ "dev": true
+ },
+ "node_modules/node-fetch/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+ "dev": true,
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
}
},
"node_modules/node-int64": {
@@ -5049,12 +5070,12 @@
}
},
"node_modules/resolve": {
- "version": "1.21.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz",
- "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==",
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
"dev": true,
"dependencies": {
- "is-core-module": "^2.8.0",
+ "is-core-module": "^2.8.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -5120,9 +5141,9 @@
}
},
"node_modules/rollup": {
- "version": "2.63.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz",
- "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==",
+ "version": "2.66.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.66.1.tgz",
+ "integrity": "sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@@ -5704,9 +5725,9 @@
}
},
"node_modules/ts-jest": {
- "version": "27.1.2",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.2.tgz",
- "integrity": "sha512-eSOiJOWq6Hhs6Khzk5wKC5sgWIXgXqOCiIl1+3lfnearu58Hj4QpE5tUhQcA3xtZrELbcvAGCsd6HB8OsaVaTA==",
+ "version": "27.1.3",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.3.tgz",
+ "integrity": "sha512-6Nlura7s6uM9BVUAoqLH7JHyMXjz8gluryjpPXxr3IxZdAXnU6FhjvVLHFtfd1vsE1p8zD1OJfskkc0jhTSnkA==",
"dev": true,
"dependencies": {
"bs-logger": "0.x",
@@ -5893,15 +5914,6 @@
"node": ">= 4.0.0"
}
},
- "node_modules/use-sync-external-store": {
- "version": "1.0.0-rc.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.0.0-rc.0.tgz",
- "integrity": "sha512-0U9Xlc2QDFzSGMB0DvcJQL0+DIdxDPJC7mnZlYFbl7wrSrPMcs89X5TVkNB6Dzg618m8lZop+U+J6ow3vq9RAQ==",
- "dev": true,
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0-rc"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -6555,9 +6567,9 @@
}
},
"@babel/parser": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz",
- "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==",
+ "version": "7.16.12",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
+ "integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==",
"dev": true
},
"@babel/plugin-syntax-async-generators": {
@@ -7277,9 +7289,9 @@
"dev": true
},
"@types/node": {
- "version": "16.11.19",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.19.tgz",
- "integrity": "sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng==",
+ "version": "16.11.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
+ "integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==",
"dev": true
},
"@types/prettier": {
@@ -7344,12 +7356,6 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
- "@types/use-sync-external-store": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
- "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
- "dev": true
- },
"@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@@ -8016,12 +8022,12 @@
"dev": true
},
"cross-fetch": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
- "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
+ "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dev": true,
"requires": {
- "node-fetch": "2.6.1"
+ "node-fetch": "2.6.7"
}
},
"cross-spawn": {
@@ -9750,10 +9756,37 @@
}
},
"node-fetch": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
- "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
- "dev": true
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "dev": true,
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ },
+ "dependencies": {
+ "tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
+ "dev": true
+ },
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+ "dev": true,
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ }
+ }
},
"node-int64": {
"version": "0.4.0",
@@ -10208,12 +10241,12 @@
"dev": true
},
"resolve": {
- "version": "1.21.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz",
- "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==",
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
"dev": true,
"requires": {
- "is-core-module": "^2.8.0",
+ "is-core-module": "^2.8.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
@@ -10257,9 +10290,9 @@
}
},
"rollup": {
- "version": "2.63.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz",
- "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==",
+ "version": "2.66.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.66.1.tgz",
+ "integrity": "sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
@@ -10715,9 +10748,9 @@
}
},
"ts-jest": {
- "version": "27.1.2",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.2.tgz",
- "integrity": "sha512-eSOiJOWq6Hhs6Khzk5wKC5sgWIXgXqOCiIl1+3lfnearu58Hj4QpE5tUhQcA3xtZrELbcvAGCsd6HB8OsaVaTA==",
+ "version": "27.1.3",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.3.tgz",
+ "integrity": "sha512-6Nlura7s6uM9BVUAoqLH7JHyMXjz8gluryjpPXxr3IxZdAXnU6FhjvVLHFtfd1vsE1p8zD1OJfskkc0jhTSnkA==",
"dev": true,
"requires": {
"bs-logger": "0.x",
@@ -10825,13 +10858,6 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
- "use-sync-external-store": {
- "version": "1.0.0-rc.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.0.0-rc.0.tgz",
- "integrity": "sha512-0U9Xlc2QDFzSGMB0DvcJQL0+DIdxDPJC7mnZlYFbl7wrSrPMcs89X5TVkNB6Dzg618m8lZop+U+J6ow3vq9RAQ==",
- "dev": true,
- "requires": {}
- },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index cdabb5f90c1..ab6cfcfa16a 100644
--- a/package.json
+++ b/package.json
@@ -65,9 +65,8 @@
"peerDependencies": {
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
"graphql-ws": "^5.5.5",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0-beta",
- "subscriptions-transport-ws": "^0.9.0 || ^0.11.0",
- "use-sync-external-store": "^1.0.0 || ^1.0.0-rc || ^1.0.0-beta"
+ "react": "^16.8.0 || ^17.0.0",
+ "subscriptions-transport-ws": "^0.9.0 || ^0.11.0"
},
"peerDependenciesMeta": {
"graphql-ws": {
@@ -78,9 +77,6 @@
},
"subscriptions-transport-ws": {
"optional": true
- },
- "use-sync-external-store": {
- "optional": true
}
},
"dependencies": {
@@ -98,7 +94,7 @@
"zen-observable-ts": "^1.2.0"
},
"devDependencies": {
- "@babel/parser": "7.16.7",
+ "@babel/parser": "7.16.12",
"@graphql-tools/schema": "8.3.1",
"@rollup/plugin-node-resolve": "11.2.1",
"@testing-library/react": "12.1.2",
@@ -109,13 +105,12 @@
"@types/hoist-non-react-statics": "3.3.1",
"@types/jest": "27.4.0",
"@types/lodash": "4.14.178",
- "@types/node": "16.11.19",
+ "@types/node": "16.11.21",
"@types/react": "17.0.34",
"@types/react-dom": "17.0.2",
- "@types/use-sync-external-store": "^0.0.3",
"acorn": "8.6.0",
"bundlesize": "0.18.1",
- "cross-fetch": "3.1.4",
+ "cross-fetch": "3.1.5",
"crypto-hash": "1.3.0",
"fetch-mock": "9.11.0",
"glob": "7.2.0",
@@ -128,17 +123,16 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"recast": "0.21.0",
- "resolve": "1.21.0",
+ "resolve": "1.22.0",
"rimraf": "3.0.2",
- "rollup": "2.63.0",
+ "rollup": "2.66.1",
"rollup-plugin-terser": "7.0.2",
"rxjs": "6.6.7",
"subscriptions-transport-ws": "0.11.0",
"terser": "5.10.0",
- "ts-jest": "27.1.2",
+ "ts-jest": "27.1.3",
"ts-node": "10.4.0",
"typescript": "4.5.2",
- "use-sync-external-store": "1.0.0-rc.0",
"wait-for-observables": "1.0.3",
"whatwg-fetch": "3.6.2"
},
diff --git a/scripts/codemods/ac2-to-ac3/package.json b/scripts/codemods/ac2-to-ac3/package.json
index d9d5b23ab05..1ea6e4b3411 100644
--- a/scripts/codemods/ac2-to-ac3/package.json
+++ b/scripts/codemods/ac2-to-ac3/package.json
@@ -1,6 +1,6 @@
{
"private": true,
"devDependencies": {
- "jscodeshift": "0.13.0"
+ "jscodeshift": "0.13.1"
}
}
diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts
index 5f79a8ecfba..010eba89b09 100644
--- a/src/core/ObservableQuery.ts
+++ b/src/core/ObservableQuery.ts
@@ -285,9 +285,11 @@ export class ObservableQuery<
variablesMustMatch?: boolean,
) {
const last = this.last;
- if (last &&
- last[key] &&
- (!variablesMustMatch || equal(last!.variables, this.variables))) {
+ if (
+ last &&
+ last[key] &&
+ (!variablesMustMatch || equal(last.variables, this.variables))
+ ) {
return last[key];
}
}
@@ -326,7 +328,7 @@ export class ObservableQuery<
// (no-cache, network-only, or cache-and-network), override it with
// network-only to force the refetch for this fetchQuery call.
const { fetchPolicy } = this.options;
- if (fetchPolicy === 'standby' || fetchPolicy === 'cache-and-network') {
+ if (fetchPolicy === 'cache-and-network') {
reobserveOptions.fetchPolicy = fetchPolicy;
} else if (fetchPolicy === 'no-cache') {
reobserveOptions.fetchPolicy = 'no-cache';
@@ -761,8 +763,12 @@ once, rather than every time you call fetchMore.`);
result: ApolloQueryResult,
variables: TVariables | undefined,
) {
- if (this.getLastError() || this.isDifferentFromLastResult(result)) {
- this.updateLastResult(result, variables);
+ const lastError = this.getLastError();
+ if (lastError || this.isDifferentFromLastResult(result)) {
+ if (lastError || !result.partial || this.options.returnPartialData) {
+ this.updateLastResult(result, variables);
+ }
+
iterateObserversSafely(this.observers, 'next', result);
}
}
diff --git a/src/link/persisted-queries/__tests__/react.tsx b/src/link/persisted-queries/__tests__/react.tsx
index 5c22c0e0983..a9595a1b1ac 100644
--- a/src/link/persisted-queries/__tests__/react.tsx
+++ b/src/link/persisted-queries/__tests__/react.tsx
@@ -1,4 +1,3 @@
-/** @jest-environment node */
import * as React from 'react';
import * as ReactDOM from 'react-dom/server';
import gql from 'graphql-tag';
diff --git a/src/react/components/__tests__/client/Mutation.test.tsx b/src/react/components/__tests__/client/Mutation.test.tsx
index 19c80a76285..64c3f3fb931 100644
--- a/src/react/components/__tests__/client/Mutation.test.tsx
+++ b/src/react/components/__tests__/client/Mutation.test.tsx
@@ -1021,11 +1021,7 @@ describe('General Mutation testing', () => {
return (
{(createTodo: any, resultMutation: any) => (
-
+
{(resultQuery: any) => {
try {
if (count === 0) {
@@ -1051,16 +1047,13 @@ describe('General Mutation testing', () => {
// mutation loading
expect(resultMutation.loading).toBe(true);
} else if (count === 6) {
- // mutation still loading???
- expect(resultMutation.loading).toBe(true);
- } else if (count === 7) {
- expect(resultQuery.loading).toBe(true);
+ // mutation loaded
expect(resultMutation.loading).toBe(false);
- } else if (count === 8) {
+ } else if (count === 7) {
// query refetched
- expect(resultQuery.data).toEqual(peopleData3);
expect(resultQuery.loading).toBe(false);
expect(resultMutation.loading).toBe(false);
+ expect(resultQuery.data).toEqual(peopleData3);
}
count++;
} catch (err) {
@@ -1081,7 +1074,7 @@ describe('General Mutation testing', () => {
);
waitFor(() => {
- expect(count).toBe(9);
+ expect(count).toEqual(8);
}).then(resolve, reject);
}));
diff --git a/src/react/components/__tests__/client/Query.test.tsx b/src/react/components/__tests__/client/Query.test.tsx
index 736a730014a..4261e93d5ac 100644
--- a/src/react/components/__tests__/client/Query.test.tsx
+++ b/src/react/components/__tests__/client/Query.test.tsx
@@ -1229,11 +1229,7 @@ describe('Query component', () => {
const { variables } = this.state;
return (
-
+
{(result: any) => {
try {
switch (count) {
@@ -1721,40 +1717,35 @@ describe('Query component', () => {
{({ loading, data }: any) => {
- try {
- switch (renderCount) {
- case 0:
- expect(loading).toBe(true);
- break;
- case 1:
- case 2:
- expect(loading).toBe(false);
- expect(data).toEqual(data1);
- break;
- case 3:
- expect(loading).toBe(true);
- break;
- case 4:
- expect(loading).toBe(false);
- expect(data).toEqual(data2);
- setTimeout(() => {
- this.setState({ variables: { first: 1 } });
- });
- case 5:
- expect(loading).toBe(false);
- expect(data).toEqual(data2);
- break;
- case 6:
- expect(loading).toBe(false);
- expect(data).toEqual(data1);
- break;
- }
- } catch (err) {
- reject(err);
+ switch (renderCount) {
+ case 0:
+ expect(loading).toBe(true);
+ break;
+ case 1:
+ case 2:
+ expect(loading).toBe(false);
+ expect(data).toEqual(data1);
+ break;
+ case 3:
+ expect(loading).toBe(true);
+ break;
+ case 4:
+ expect(loading).toBe(false);
+ expect(data).toEqual(data2);
+ setTimeout(() => {
+ this.setState({ variables: { first: 1 } });
+ });
+ case 5:
+ expect(loading).toBe(false);
+ expect(data).toEqual(data2);
+ break;
+ case 6:
+ expect(loading).toBe(false);
+ expect(data).toEqual(data1);
+ break;
}
renderCount += 1;
return null;
@@ -1771,7 +1762,6 @@ describe('Query component', () => {
);
waitFor(() => {
- expect(renderCount).toBe(7);
expect(onCompletedCallCount).toBe(3);
}).then(resolve, reject);
});
diff --git a/src/react/components/__tests__/ssr/getDataFromTree.test.tsx b/src/react/components/__tests__/ssr/getDataFromTree.test.tsx
index 526264bcebf..696135a3faa 100644
--- a/src/react/components/__tests__/ssr/getDataFromTree.test.tsx
+++ b/src/react/components/__tests__/ssr/getDataFromTree.test.tsx
@@ -1,4 +1,3 @@
-/** @jest-environment node */
import React from 'react';
import gql from 'graphql-tag';
import { DocumentNode } from 'graphql';
diff --git a/src/react/components/__tests__/ssr/server.test.tsx b/src/react/components/__tests__/ssr/server.test.tsx
index ef4d3756d62..890f8c78188 100644
--- a/src/react/components/__tests__/ssr/server.test.tsx
+++ b/src/react/components/__tests__/ssr/server.test.tsx
@@ -1,4 +1,3 @@
-/** @jest-environment node */
import React from 'react';
import {
print,
diff --git a/src/react/hoc/__tests__/queries/errors.test.tsx b/src/react/hoc/__tests__/queries/errors.test.tsx
index db974ffe2ff..c8b466f76fa 100644
--- a/src/react/hoc/__tests__/queries/errors.test.tsx
+++ b/src/react/hoc/__tests__/queries/errors.test.tsx
@@ -216,10 +216,7 @@ describe('[queries] errors', () => {
let iteration = 0;
let done = false;
const ErrorContainer = withState('var', 'setVar', 1)(
- graphql(
- query,
- { options: { notifyOnNetworkStatusChange: true }},
- )(
+ graphql(query)(
class extends React.Component> {
componentDidUpdate() {
const { props } = this;
@@ -237,7 +234,7 @@ describe('[queries] errors', () => {
);
} else if (iteration === 3) {
// variables have changed, wee are loading again but also have data
- expect(props.data!.loading).toBe(true);
+ expect(props.data!.loading).toBeTruthy();
} else if (iteration === 4) {
// the second request had an error!
expect(props.data!.error).toBeTruthy();
@@ -259,8 +256,8 @@ describe('[queries] errors', () => {
render() {
return null;
}
- },
- ),
+ }
+ )
);
render(
@@ -473,11 +470,9 @@ describe('[queries] errors', () => {
});
break;
case 3:
- // Second render was added by useSyncExternalStore changes...
- case 4:
expect(props.data!.loading).toBeTruthy();
break;
- case 5:
+ case 4:
expect(props.data!.loading).toBeFalsy();
expect(props.data!.error).toBeFalsy();
expect(props.data!.allPeople).toEqual(
@@ -504,7 +499,7 @@ describe('[queries] errors', () => {
);
- waitFor(() => expect(count).toBe(6)).then(resolve, reject);
+ waitFor(() => 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) => {
diff --git a/src/react/hoc/__tests__/queries/lifecycle.test.tsx b/src/react/hoc/__tests__/queries/lifecycle.test.tsx
index 2846ceea4d1..63c0c691079 100644
--- a/src/react/hoc/__tests__/queries/lifecycle.test.tsx
+++ b/src/react/hoc/__tests__/queries/lifecycle.test.tsx
@@ -46,8 +46,7 @@ describe('[queries] lifecycle', () => {
const Container = graphql(query, {
options: props => ({
variables: props,
- fetchPolicy: count === 0 ? 'cache-and-network' : 'cache-first',
- notifyOnNetworkStatusChange: true,
+ fetchPolicy: count === 0 ? 'cache-and-network' : 'cache-first'
})
})(
class extends React.Component> {
@@ -210,10 +209,7 @@ describe('[queries] lifecycle', () => {
});
const Container = graphql(query, {
- options: props => ({
- variables: props,
- notifyOnNetworkStatusChange: true,
- }),
+ options: props => ({ variables: props })
})(
class extends React.Component> {
componentDidUpdate(prevProps: ChildProps) {
@@ -311,10 +307,7 @@ describe('[queries] lifecycle', () => {
cache: new Cache({ addTypename: false })
});
- const Container = graphql(
- query,
- { options: { notifyOnNetworkStatusChange: true } },
- )(
+ const Container = graphql(query)(
class extends React.Component> {
componentDidUpdate(prevProps: ChildProps) {
try {
@@ -801,15 +794,11 @@ describe('[queries] lifecycle', () => {
}
render() {
- try {
- count++;
- const user = this.props.data!.user;
- const name = user ? user.name : '';
- if (count === 3) {
- expect(name).toBe('Luke Skywalker');
- }
- } catch (err) {
- reject(err);
+ count++;
+ const user = this.props.data!.user;
+ const name = user ? user.name : '';
+ if (count === 2) {
+ expect(name).toBe('Luke Skywalker');
}
return null;
}
@@ -884,30 +873,26 @@ describe('[queries] lifecycle', () => {
{({ loading, data, refetch }: any) => {
- try {
- if (!loading) {
- if (!refetched) {
- 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
- //we update the data To get around this issue, we would need
- //to start the subscription before we render to the page. In
- //practice, this seems like an uncommon use case, since the
- //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');
- done = true;
- });
+ if (!loading) {
+ if (!refetched) {
+ 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
+ //we update the data To get around this issue, we would need
+ //to start the subscription before we render to the page. In
+ //practice, this seems like an uncommon use case, since the
+ //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');
+ done = true;
});
- refetched = true;
- } else {
- expect(data.books[0].name).toEqual('first');
- }
+ });
+ refetched = true;
+ } else {
+ expect(data.books[0].name).toEqual('first');
}
- } catch (err) {
- reject(err);
}
return stub
;
}}
diff --git a/src/react/hoc/__tests__/queries/loading.test.tsx b/src/react/hoc/__tests__/queries/loading.test.tsx
index 176b79dc63a..da240757568 100644
--- a/src/react/hoc/__tests__/queries/loading.test.tsx
+++ b/src/react/hoc/__tests__/queries/loading.test.tsx
@@ -635,10 +635,7 @@ describe('[queries] loading', () => {
const Container = connect(
graphql(query, {
- options: ({ first }) => ({
- variables: { first },
- notifyOnNetworkStatusChange: true,
- })
+ options: ({ first }) => ({ variables: { first } })
})(
class extends React.Component> {
render() {
@@ -757,8 +754,7 @@ describe('[queries] loading', () => {
graphql(query, {
options: ({ first }) => ({
variables: { first },
- fetchPolicy: 'network-only',
- notifyOnNetworkStatusChange: true,
+ fetchPolicy: 'network-only'
})
})(
class extends React.Component> {
@@ -778,6 +774,7 @@ describe('[queries] loading', () => {
expect(props.data!.loading).toBeFalsy(); // has initial data
expect(props.data!.allPeople).toEqual(data.allPeople);
break;
+
case 3:
expect(props.data!.loading).toBeTruthy(); // on variables change
break;
diff --git a/src/react/hoc/__tests__/queries/observableQuery.test.tsx b/src/react/hoc/__tests__/queries/observableQuery.test.tsx
index f8dbda220c8..3ed9572593f 100644
--- a/src/react/hoc/__tests__/queries/observableQuery.test.tsx
+++ b/src/react/hoc/__tests__/queries/observableQuery.test.tsx
@@ -446,9 +446,7 @@ describe('[queries] observableQuery', () => {
if (count === 2) {
expect(loading).toBe(false);
expect(allPeople).toEqual(dataOne.allPeople);
- setTimeout(() => {
- refetch();
- });
+ refetch();
}
if (count === 3) {
expect(loading).toBe(false);
diff --git a/src/react/hoc/__tests__/queries/skip.test.tsx b/src/react/hoc/__tests__/queries/skip.test.tsx
index 6be679ce960..03e1fe1837c 100644
--- a/src/react/hoc/__tests__/queries/skip.test.tsx
+++ b/src/react/hoc/__tests__/queries/skip.test.tsx
@@ -166,8 +166,7 @@ describe('[queries] skip', () => {
options: ({ person }) => ({
variables: {
id: person!.id
- },
- notifyOnNetworkStatusChange: true,
+ }
})
})(
class extends React.Component> {
@@ -176,13 +175,13 @@ describe('[queries] skip', () => {
const { props } = this;
switch (count) {
case 0:
- expect(props.data!.loading).toBe(true);
- break;
case 1:
- expect(props.data!.loading).toBe(false);
- expect(props.data!.allPeople).toEqual(data.allPeople);
+ expect(props.data!.loading).toBeTruthy();
break;
case 2:
+ expect(props.data!.allPeople).toEqual(data.allPeople);
+ break;
+ case 3:
expect(renderCount).toBe(3);
break;
}
@@ -219,7 +218,7 @@ describe('[queries] skip', () => {
);
- waitFor(() => expect(count).toBe(2)).then(resolve, reject);
+ waitFor(() => expect(count).toBe(3)).then(resolve, reject);
});
itAsync("doesn't run options or props when skipped, including option.client", (resolve, reject) => {
@@ -664,21 +663,28 @@ describe('[queries] skip', () => {
case 3:
// This render is triggered after setting skip to true. Now
// let's set skip to false to re-trigger the query.
- expect(this.props.skip).toBe(true);
- expect(this.props.data).toBeUndefined();
- expect(ranQuery).toBe(1);
setTimeout(() => {
this.props.setSkip(false);
}, 10);
- break;
+ // fallthrough
case 4:
+ expect(this.props.skip).toBe(true);
+ expect(this.props.data).toBeUndefined();
+ expect(ranQuery).toBe(1);
+ break;
+ case 5:
expect(this.props.skip).toBe(false);
expect(this.props.data!.loading).toBe(true);
expect(this.props.data.allPeople).toEqual(data.allPeople);
expect(ranQuery).toBe(1);
break;
- case 5:
+ case 6:
expect(this.props.skip).toBe(false);
+ expect(this.props.data!.loading).toBe(true);
+ expect(this.props.data.allPeople).toEqual(data.allPeople);
+ expect(ranQuery).toBe(2);
+ break;
+ case 7:
expect(this.props.data!.loading).toBe(false);
expect(this.props.data.allPeople).toEqual(nextData.allPeople);
expect(ranQuery).toBe(2);
@@ -690,13 +696,13 @@ describe('[queries] skip', () => {
this.props.data.refetch();
}, 10);
break;
- case 6:
+ case 8:
expect(this.props.skip).toBe(false);
expect(this.props.data!.loading).toBe(true);
expect(this.props.data.allPeople).toEqual(nextData.allPeople);
expect(ranQuery).toBe(3);
break;
- case 7:
+ case 9:
// The next batch of data has loaded.
expect(this.props.skip).toBe(false);
expect(this.props.data!.loading).toBe(false);
@@ -733,7 +739,7 @@ describe('[queries] skip', () => {
);
- waitFor(() => expect(count).toEqual(7)).then(resolve, reject);
+ waitFor(() => expect(count).toEqual(9)).then(resolve, reject);
}));
it('removes the injected props if skip becomes true', async () => {
diff --git a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx
index 7d24cce84dc..02cccfd7cb7 100644
--- a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx
+++ b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx
@@ -1,4 +1,3 @@
-/** @jest-environment node */
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom/server';
diff --git a/src/react/hoc/__tests__/ssr/server.test.tsx b/src/react/hoc/__tests__/ssr/server.test.tsx
index a1322817ca3..fc001c11283 100644
--- a/src/react/hoc/__tests__/ssr/server.test.tsx
+++ b/src/react/hoc/__tests__/ssr/server.test.tsx
@@ -1,4 +1,3 @@
-/** @jest-environment node */
import React from 'react';
import {
print,
diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx
index 7e9ff7692b9..7e4c1843e7f 100644
--- a/src/react/hooks/__tests__/useLazyQuery.test.tsx
+++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { GraphQLError } from 'graphql';
import gql from 'graphql-tag';
import { renderHook } from '@testing-library/react-hooks';
@@ -261,8 +262,9 @@ describe('useLazyQuery Hook', () => {
expect(result.current[1].data).toEqual({ hello: 'world 1' });
setTimeout(() => execute());
+
await waitForNextUpdate();
- expect(result.current[1].loading).toBe(true);
+ expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toEqual({ hello: 'world 1' });
await waitForNextUpdate();
@@ -449,6 +451,7 @@ describe('useLazyQuery Hook', () => {
expect(result.current[1].previousData).toBe(undefined);
setTimeout(() => execute({ variables: { id: 2 }}));
+
await waitForNextUpdate();
expect(result.current[1].loading).toBe(true);
expect(result.current[1].data).toBe(undefined);
@@ -529,8 +532,10 @@ describe('useLazyQuery Hook', () => {
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toBe(undefined);
const execute = result.current[0];
- const mock = jest.fn();
- setTimeout(() => mock(execute()));
+ let executeResult: any;
+ setTimeout(() => {
+ executeResult = execute();
+ });
await waitForNextUpdate();
expect(result.current[1].loading).toBe(true);
@@ -538,9 +543,221 @@ describe('useLazyQuery Hook', () => {
await waitForNextUpdate();
expect(result.current[1].loading).toBe(false);
expect(result.current[1].data).toEqual({ hello: 'world' });
+ await expect(executeResult).resolves.toEqual(result.current[1]);
+ });
+
+ it('should have matching results from execution function and hook', async () => {
+ const query = gql`
+ query GetCountries($filter: String) {
+ countries(filter: $filter) {
+ code
+ name
+ }
+ }
+ `;
+
+ const mocks = [
+ {
+ request: {
+ query,
+ variables: {
+ filter: "PA",
+ },
+ },
+ result: {
+ data: {
+ countries: {
+ code: "PA",
+ name: "Panama",
+ },
+ },
+ },
+ delay: 20,
+ },
+ {
+ request: {
+ query,
+ variables: {
+ filter: "BA",
+ },
+ },
+ result: {
+ data: {
+ countries: {
+ code: "BA",
+ name: "Bahamas",
+ },
+ },
+ },
+ delay: 20,
+ },
+ ];
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useLazyQuery(query),
+ {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toBe(undefined);
+ const execute = result.current[0];
+ let executeResult: any;
+ setTimeout(() => {
+ executeResult = execute({ variables: { filter: "PA" } });
+ });
+
+ await waitForNextUpdate();
+ expect(result.current[1].loading).toBe(true);
+
+ await waitForNextUpdate();
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toEqual({
+ countries: {
+ code: "PA",
+ name: "Panama",
+ },
+ });
+
+ expect(executeResult).toBeInstanceOf(Promise);
+ expect((await executeResult).data).toEqual({
+ countries: {
+ code: "PA",
+ name: "Panama",
+ },
+ });
+
+ setTimeout(() => {
+ executeResult = execute({ variables: { filter: "BA" } });
+ });
+
+ await waitForNextUpdate();
+ // TODO: Get rid of this render.
+
+ await waitForNextUpdate();
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toEqual({
+ countries: {
+ code: "BA",
+ name: "Bahamas",
+ },
+ });
+
+ expect(executeResult).toBeInstanceOf(Promise);
+ expect((await executeResult).data).toEqual({
+ countries: {
+ code: "BA",
+ name: "Bahamas",
+ },
+ });
+ });
+
+ it('the promise should reject with errors the “way useMutation does”', async () => {
+ const query = gql`{ hello }`;
+ const mocks = [
+ {
+ request: { query },
+ result: {
+ errors: [new GraphQLError('error 1')],
+ },
+ delay: 20,
+ },
+ {
+ request: { query },
+ result: {
+ errors: [new GraphQLError('error 2')],
+ },
+ delay: 20,
+ },
+ ];
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useLazyQuery(query),
+ {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ const execute = result.current[0];
+ let executeResult: any;
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toBe(undefined);
+ setTimeout(() => {
+ executeResult = execute();
+ executeResult.catch(() => {});
+ });
+
+ await waitForNextUpdate();
+ expect(result.current[1].loading).toBe(true);
+ expect(result.current[1].data).toBe(undefined);
+ expect(result.current[1].error).toBe(undefined);
+
+ await waitForNextUpdate();
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toBe(undefined);
+ expect(result.current[1].error).toEqual(new Error('error 1'));
+
+ await expect(executeResult).rejects.toEqual(new Error('error 1'));
+
+ setTimeout(() => {
+ executeResult = execute();
+ executeResult.catch(() => {});
+ });
+
+ await waitForNextUpdate();
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toBe(undefined);
+ expect(result.current[1].error).toEqual(new Error('error 1'));
+
+ await waitForNextUpdate();
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toBe(undefined);
+ expect(result.current[1].error).toEqual(new Error('error 2'));
+
+ await expect(executeResult).rejects.toEqual(new Error('error 2'));
+ });
+
+ it('the promise should not cause an unhandled rejection', async () => {
+ const query = gql`{ hello }`;
+ const mocks = [
+ {
+ request: { query },
+ result: {
+ errors: [new GraphQLError('error 1')],
+ },
+ },
+ ];
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useLazyQuery(query),
+ {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ const execute = result.current[0];
+ expect(result.current[1].loading).toBe(false);
+ expect(result.current[1].data).toBe(undefined);
+ setTimeout(() => {
+ execute();
+ });
+
+ await waitForNextUpdate();
- expect(mock).toHaveBeenCalledTimes(1);
- expect(mock.mock.calls[0][0]).toBeInstanceOf(Promise);
- expect(await mock.mock.calls[0][0]).toEqual(result.current[1]);
+ // Making sure the rejection triggers a test failure.
+ await new Promise((resolve) => setTimeout(resolve, 50));
});
});
diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx
index b361caaa592..df4849da185 100644
--- a/src/react/hooks/__tests__/useMutation.test.tsx
+++ b/src/react/hooks/__tests__/useMutation.test.tsx
@@ -272,6 +272,49 @@ 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 () => {
+ const variables = {
+ description: 'Get milk!'
+ };
+
+ const mocks = [
+ {
+ request: {
+ query: CREATE_TODO_MUTATION,
+ variables,
+ },
+ result: {
+ errors: [new GraphQLError(CREATE_TODO_ERROR)],
+ },
+ }
+ ];
+
+ const { result } = renderHook(
+ () => useMutation(CREATE_TODO_MUTATION),
+ { wrapper: ({ children }) => (
+
+ {children}
+
+ )},
+ );
+
+ const createTodo = result.current[0];
+ let fetchError: any;
+ await act(async () => {
+ // need to call createTodo this way to get “act” warnings to go away.
+ try {
+ await createTodo({ variables });
+ } catch (err) {
+ fetchError = err;
+ return;
+ }
+
+ throw new Error("function did not error");
+ });
+
+ expect(fetchError).toEqual(new GraphQLError(CREATE_TODO_ERROR));
+ });
+
it(`should reject when errorPolicy is 'none'`, async () => {
const variables = {
description: 'Get milk!'
@@ -341,7 +384,47 @@ describe('useMutation Hook', () => {
expect(fetchResult.data).toEqual(CREATE_TODO_RESULT);
expect(fetchResult.errors[0].message).toEqual(CREATE_TODO_ERROR);
- })
+ });
+
+ it(`should ignore errors when errorPolicy is 'ignore'`, async () => {
+ const errorMock = jest.spyOn(console, "error")
+ .mockImplementation(() => {});
+ const variables = {
+ description: 'Get milk!'
+ };
+
+ const mocks = [
+ {
+ request: {
+ query: CREATE_TODO_MUTATION,
+ variables,
+ },
+ result: {
+ errors: [new GraphQLError(CREATE_TODO_ERROR)],
+ },
+ }
+ ];
+
+ const { result } = renderHook(
+ () => useMutation(CREATE_TODO_MUTATION, { errorPolicy: "ignore" }),
+ { wrapper: ({ children }) => (
+
+ {children}
+
+ )},
+ );
+
+ const createTodo = result.current[0];
+ let fetchResult: any;
+ await act(async () => {
+ fetchResult = await createTodo({ variables });
+ });
+
+ expect(fetchResult).toEqual({});
+ expect(errorMock).toHaveBeenCalledTimes(1);
+ expect(errorMock.mock.calls[0][0]).toMatch("Missing field");
+ errorMock.mockRestore();
+ });
});
it('should return the current client instance in the result object', async () => {
@@ -1295,6 +1378,7 @@ describe('useMutation Hook', () => {
optimisticResponse,
update(cache) {
const result = cache.readQuery({ query: countQuery });
+
cache.writeQuery({
query: countQuery,
data: {
@@ -1328,7 +1412,13 @@ describe('useMutation Hook', () => {
});
});
+ expect(result.current.query.loading).toBe(false);
+ expect(result.current.query.data).toEqual({ todoCount: 0 });
+ expect(result.current.mutation[1].loading).toBe(true);
+ expect(result.current.mutation[1].data).toBe(undefined);
expect(finishedReobserving).toBe(false);
+
+ await waitForNextUpdate();
expect(result.current.query.loading).toBe(false);
expect(result.current.query.data).toEqual({ todoCount: 1 });
expect(result.current.mutation[1].loading).toBe(true);
@@ -1479,7 +1569,7 @@ describe('useMutation Hook', () => {
expect(result.current.query.data).toEqual(mocks[0].result.data);
await waitForNextUpdate();
- expect(result.current.query.loading).toBe(true);
+ expect(result.current.query.loading).toBe(false);
expect(result.current.query.data).toEqual(mocks[0].result.data);
await waitForNextUpdate();
diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx
index b3cf69b4101..556c5e10952 100644
--- a/src/react/hooks/__tests__/useQuery.test.tsx
+++ b/src/react/hooks/__tests__/useQuery.test.tsx
@@ -158,7 +158,7 @@ describe('useQuery Hook', () => {
);
const { result, rerender, waitForNextUpdate } = renderHook(
- ({ id }) => useQuery(query, { variables: { id } }),
+ ({ id }) => useQuery(query, { variables: { id }}),
{ wrapper, initialProps: { id: 1 } },
);
expect(result.current.loading).toBe(true);
@@ -211,6 +211,7 @@ describe('useQuery Hook', () => {
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual({ hello: "world 1" });
+
rerender({ id: 2 });
expect(result.current.loading).toBe(true);
expect(result.current.data).toBe(undefined);
@@ -1584,7 +1585,6 @@ describe('useQuery Hook', () => {
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(data1);
expect(onCompleted).toHaveBeenLastCalledWith(data1);
- expect(onCompleted).toHaveBeenCalledTimes(1);
rerender({ variables: { first: 2 } });
expect(result.current.loading).toBe(true);
@@ -1593,13 +1593,12 @@ describe('useQuery Hook', () => {
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(data2);
expect(onCompleted).toHaveBeenLastCalledWith(data2);
- expect(onCompleted).toHaveBeenCalledTimes(2);
rerender({ variables: { first: 1 } });
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(data1);
-
expect(onCompleted).toHaveBeenLastCalledWith(data1);
+
expect(onCompleted).toHaveBeenCalledTimes(3);
});
});
@@ -3072,7 +3071,8 @@ describe('useQuery Hook', () => {
expect(result.current.data).toEqual({ hello: 'world' });
});
- it('should not refetch when skip is true', async () => {
+ // 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' },
@@ -3099,13 +3099,18 @@ describe('useQuery Hook', () => {
expect(result.current.data).toBe(undefined);
await expect(waitForNextUpdate({ timeout: 20 }))
.rejects.toThrow('Timed out');
- result.current.refetch();
- await expect(waitForNextUpdate({ timeout: 20 }))
- .rejects.toThrow('Timed out');
+ const promise = result.current.refetch();
+ // TODO: Not really sure about who is causing this render.
+ await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.data).toBe(undefined);
- expect(requestSpy).toHaveBeenCalledTimes(0);
+ expect(requestSpy).toHaveBeenCalledTimes(1);
requestSpy.mockRestore();
+ expect(promise).resolves.toEqual({
+ data: {hello: "world"},
+ loading: false,
+ networkStatus: 7,
+ });
});
});
@@ -3322,6 +3327,99 @@ describe('useQuery Hook', () => {
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 () => {
+ const cache = new InMemoryCache();
+ const client = new ApolloClient({
+ cache,
+ link: ApolloLink.empty(),
+ });
+
+ const query = gql`
+ query MyCar($id: ID) {
+ car (id: $id) {
+ id
+ make
+ }
+ }
+ `;
+
+ const partialQuery = gql`
+ query MyCar($id: ID) {
+ car (id: $id) {
+ id
+ make
+ model
+ }
+ }
+ `;
+
+ cache.writeQuery({
+ query,
+ variables: { id: 1 },
+ data: {
+ car: {
+ __typename: 'Car',
+ id: 1,
+ make: 'Ford',
+ model: 'Pinto',
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: partialQuery,
+ variables: { id: 2 },
+ data: {
+ car: {
+ __typename: 'Car',
+ id: 2,
+ make: 'Ford',
+ model: 'Pinto',
+ },
+ },
+ });
+
+
+ let setId: any;
+ const { result, waitForNextUpdate } = renderHook(
+ () => {
+ const [id, setId1] = React.useState(2);
+ setId = setId1;
+ return useQuery(partialQuery, {
+ variables: { id },
+ returnPartialData: false,
+ notifyOnNetworkStatusChange: true,
+ });
+ },
+ {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ expect(result.current.loading).toBe(false);
+ expect(result.current.data).toEqual({
+ car: {
+ __typename: 'Car',
+ id: 2,
+ make: 'Ford',
+ model: 'Pinto',
+ },
+ });
+
+ setTimeout(() => {
+ setId(1);
+ });
+
+ await waitForNextUpdate();
+
+ expect(result.current.loading).toBe(true);
+ expect(result.current.data).toBe(undefined);
+ });
});
describe('Previous data', () => {
diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx
index 3ad179278cb..caeb9949f06 100644
--- a/src/react/hooks/__tests__/useSubscription.test.tsx
+++ b/src/react/hooks/__tests__/useSubscription.test.tsx
@@ -127,19 +127,27 @@ describe('useSubscription Hook', () => {
}
`;
+ const onSetup = jest.fn();
const link = new MockSubscriptionLink();
+ link.onSetup(onSetup);
const client = new ApolloClient({
link,
cache: new Cache({ addTypename: false })
});
const onSubscriptionData = jest.fn();
- const { result, unmount, waitForNextUpdate } = renderHook(
- () => useSubscription(subscription, {
- onSubscriptionData,
+ const { result, unmount, waitForNextUpdate, rerender } = renderHook(
+ ({ variables }) => useSubscription(subscription, {
+ variables,
skip: true,
+ onSubscriptionData,
}),
{
+ initialProps: {
+ variables: {
+ foo: 'bar'
+ }
+ },
wrapper: ({ children }) => (
{children}
@@ -151,11 +159,14 @@ describe('useSubscription Hook', () => {
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(undefined);
expect(result.current.data).toBe(undefined);
+
+ rerender({ variables: { foo: 'bar2' }});
await expect(waitForNextUpdate({ timeout: 20 }))
.rejects.toThrow('Timed out');
- unmount();
+ expect(onSetup).toHaveBeenCalledTimes(0);
expect(onSubscriptionData).toHaveBeenCalledTimes(0);
+ unmount();
});
it('should create a subscription after skip has changed from true to a falsy value', async () => {
diff --git a/src/react/hooks/useApolloClient.ts b/src/react/hooks/useApolloClient.ts
index e3556811bd6..02c403be2fe 100644
--- a/src/react/hooks/useApolloClient.ts
+++ b/src/react/hooks/useApolloClient.ts
@@ -11,8 +11,8 @@ export function useApolloClient(
invariant(
!!client,
'Could not find "client" in the context or passed in as an option. ' +
- 'Wrap the root component in an , or pass an ApolloClient' +
- 'ApolloClient instance in via options.',
+ 'Wrap the root component in an , or pass an ApolloClient ' +
+ 'instance in via options.',
);
return client;
diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts
index 861b0cfa367..c9ed4643320 100644
--- a/src/react/hooks/useLazyQuery.ts
+++ b/src/react/hooks/useLazyQuery.ts
@@ -1,10 +1,9 @@
import { DocumentNode } from 'graphql';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import {
LazyQueryHookOptions,
- LazyQueryResult,
QueryLazyOptions,
QueryTuple,
} from '../types/types';
@@ -25,54 +24,21 @@ export function useLazyQuery(
query: DocumentNode | TypedDocumentNode,
options?: LazyQueryHookOptions
): QueryTuple {
- const [execution, setExecution] = useState<
- {
- called: boolean,
- options?: QueryLazyOptions,
- resolves: Array<(result: LazyQueryResult) => void>,
- }
- >({
+ const [execution, setExecution] = useState<{
+ called: boolean,
+ options?: QueryLazyOptions,
+ }>({
called: false,
- resolves: [],
});
- const execute = useCallback<
- QueryTuple[0]
- >((executeOptions?: QueryLazyOptions) => {
- let resolve!: (result: LazyQueryResult) => void;
- const promise = new Promise>(
- (resolve1) => (resolve = resolve1),
- );
- setExecution((execution) => {
- if (execution.called) {
- result && result.refetch(executeOptions?.variables);
- }
-
- return {
- called: true,
- resolves: [...execution.resolves, resolve],
- options: executeOptions,
- };
- });
-
- return promise;
- }, []);
-
let result = useQuery(query, {
...options,
...execution.options,
- // We don’t set skip to execution.called, because we need useQuery to call
- // addQueryPromise, so that ssr calls waits for execute to be called.
+ // We don’t set skip to execution.called, because some useQuery SSR code
+ // checks skip for some reason.
fetchPolicy: execution.called ? options?.fetchPolicy : 'standby',
skip: undefined,
});
- useEffect(() => {
- const { resolves } = execution;
- if (!result.loading && resolves.length) {
- setExecution((execution) => ({ ...execution, resolves: [] }));
- resolves.forEach((resolve) => resolve(result));
- }
- }, [result, execution]);
if (!execution.called) {
result = {
@@ -80,19 +46,50 @@ export function useLazyQuery(
loading: false,
data: void 0 as unknown as TData,
error: void 0,
- // TODO: fix the type of result
- called: false as any,
+ called: false,
};
+ }
+ // We use useMemo here to make sure the eager methods have a stable identity.
+ const eagerMethods = useMemo(() => {
+ const eagerMethods: Record = {};
for (const key of EAGER_METHODS) {
const method = result[key];
- result[key] = (...args: any) => {
+ eagerMethods[key] = (...args: any) => {
setExecution((execution) => ({ ...execution, called: true }));
return (method as any)(...args);
};
}
- }
- // TODO: fix the type of result
- return [execute, result as LazyQueryResult];
+ return eagerMethods;
+ }, []);
+
+ result.error = result.error || void 0;
+ Object.assign(result, eagerMethods);
+
+ const execute = useCallback<
+ QueryTuple[0]
+ >((executeOptions?: QueryLazyOptions) => {
+ setExecution({ called: true, options: executeOptions });
+ const promise = result.refetch(executeOptions?.variables).then((result1) => {
+ const result2 = {
+ ...result,
+ data: result1.data,
+ error: result1.error,
+ called: true,
+ loading: false,
+ };
+
+ Object.assign(result2, eagerMethods);
+ return result2;
+ });
+
+ // 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/useQuery.ts b/src/react/hooks/useQuery.ts
index fd629f51368..363ca3ba3a3 100644
--- a/src/react/hooks/useQuery.ts
+++ b/src/react/hooks/useQuery.ts
@@ -1,5 +1,4 @@
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
-import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { equal } from '@wry/equality';
import { OperationVariables, mergeOptions } from '../../core';
import { getApolloContext } from '../context';
@@ -31,14 +30,6 @@ export function useQuery<
const client = useApolloClient(options?.client);
const defaultWatchQueryOptions = client.defaultOptions.watchQuery;
verifyDocumentType(query, DocumentType.Query);
-
- const ref = useRef({
- client,
- query,
- options,
- watchQueryOptions: createWatchQueryOptions(query, options, defaultWatchQueryOptions),
- });
-
const [obsQuery, setObsQuery] = useState(() => {
const watchQueryOptions = createWatchQueryOptions(query, options, defaultWatchQueryOptions);
// See if there is an existing observable that was used to fetch the same
@@ -85,7 +76,6 @@ export function useQuery<
sub.unsubscribe();
},
complete() {
- // TODO: Does this branch ever get called before next() and error()?
resolve();
},
});
@@ -99,127 +89,142 @@ export function useQuery<
return obsQuery;
});
+ let [result, setResult] = useState(() => {
+ const result = obsQuery.getCurrentResult();
+ if (!result.loading && options) {
+ if (result.error) {
+ options.onError?.(result.error);
+ } else if (result.data) {
+ options.onCompleted?.(result.data);
+ }
+ }
+
+ return result;
+ });
+
+ const ref = useRef({
+ client,
+ query,
+ options,
+ result,
+ previousData: void 0 as TData | undefined,
+ watchQueryOptions: createWatchQueryOptions(query, options, defaultWatchQueryOptions),
+ });
+
// An effect to recreate the obsQuery whenever the client or query changes.
- // This effect is also responsible for updating the obsQuery options whenever
- // they change.
+ // This effect is also responsible for checking and updating the obsQuery
+ // options whenever they change.
useEffect(() => {
const watchQueryOptions = createWatchQueryOptions(query, options, defaultWatchQueryOptions);
-
+ let nextResult: ApolloQueryResult | undefined;
if (ref.current.client !== client || !equal(ref.current.query, query)) {
const obsQuery = client.watchQuery(watchQueryOptions);
setObsQuery(obsQuery);
+ nextResult = obsQuery.getCurrentResult();
} else if (!equal(ref.current.watchQueryOptions, watchQueryOptions)) {
- obsQuery.setOptions(watchQueryOptions);
- // We call setObsQuery to rerender the hook.
- setObsQuery(obsQuery);
+ obsQuery.setOptions(watchQueryOptions).catch(() => {});
+ nextResult = obsQuery.getCurrentResult();
+ ref.current.watchQueryOptions = watchQueryOptions;
}
- Object.assign(ref.current, {
- client,
- query,
- options,
- watchQueryOptions,
- });
- }, [obsQuery, client, query, options]);
+ if (nextResult) {
+ const previousResult = ref.current.result;
+ if (previousResult.data) {
+ ref.current.previousData = previousResult.data;
+ }
- const [subscribe, getSnapshot] = useMemo(() => {
- let previousResult: ApolloQueryResult | undefined;
- const subscribe = (forceUpdate: () => void) => {
- let subscription = obsQuery.subscribe(forceUpdate, onError);
- function onError(error: Error) {
- forceUpdate();
- subscription.unsubscribe();
- const last = obsQuery["last"];
- obsQuery.resetLastResults();
- obsQuery.subscribe(forceUpdate, onError);
- obsQuery["last"] = last;
- if (!error.hasOwnProperty('graphQLErrors')) {
- // The error is not a GraphQL error
- throw error;
+ setResult(ref.current.result = nextResult);
+ if (!nextResult.loading && options) {
+ if (nextResult.error) {
+ options.onError?.(nextResult.error);
+ } else if (nextResult.data) {
+ options.onCompleted?.(nextResult.data);
}
}
+ }
- return () => {
- subscription.unsubscribe();
- };
- };
+ Object.assign(ref.current, { client, query });
+ }, [obsQuery, client, query, options]);
- const getSnapshot = () => {
- let result = obsQuery.getCurrentResult();
- if (result.errors && result.errors.length) {
- // Until a set naming convention for networkError and graphQLErrors is
- // decided upon, we map errors (graphQLErrors) to the error options.
- // TODO: Is it possible for both result.error and result.errors to be
- // defined here?
- result = {
- ...result,
- error:
- result.error || new ApolloError({ graphQLErrors: result.errors }),
- };
- }
+ // An effect to subscribe to the current observable query
+ useEffect(() => {
+ if (context.renderPromises) {
+ return;
+ }
+ let subscription = obsQuery.subscribe(onNext, onError);
+ // We use `getCurrentResult()` instead of the callback argument because
+ // the values differ slightly. Specifically, loading results will have
+ // an empty object for data instead of `undefined` for some reason.
+ function onNext() {
+ const previousResult = ref.current.result;
+ 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) ||
- !equal(previousResult.error, result.error)
+ previousResult &&
+ previousResult.loading === result.loading &&
+ previousResult.networkStatus === result.networkStatus &&
+ equal(previousResult.data, result.data)
) {
- if (previousResult) {
- result = {
- ...result,
- previousData: previousResult.data || (previousResult as any).previousData,
- } as ApolloQueryResult;
- }
+ return;
+ }
- previousResult = result;
+ if (previousResult.data) {
+ ref.current.previousData = previousResult.data;
+ }
- if (!result.loading) {
- if (result.data) {
- ref.current.options?.onCompleted?.(result.data);
- } else if (result.error) {
- ref.current.options?.onError?.(result.error);
- }
- }
+ setResult(ref.current.result = result);
+ if (!result.loading) {
+ ref.current.options?.onCompleted?.(result.data);
}
+ }
- return previousResult;
- };
+ function 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;
+ }
- return [subscribe, getSnapshot];
- }, [obsQuery]);
+ if (!error.hasOwnProperty('graphQLErrors')) {
+ // The error is not a GraphQL error
+ throw error;
+ }
- const obsQueryMethods = useMemo(() => ({
- refetch: obsQuery.refetch.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 previousResult = ref.current.result;
+ if (
+ (previousResult && previousResult.loading) ||
+ !equal(error, previousResult.error)
+ ) {
+ setResult(ref.current.result = {
+ data: previousResult.data,
+ error: error as ApolloError,
+ loading: false,
+ networkStatus: NetworkStatus.error,
+ });
+ ref.current.options?.onError?.(error as ApolloError);
+ }
+ }
+
+ return () => subscription.unsubscribe();
+ }, [obsQuery, context.renderPromises, client.disableNetworkFetches]);
- let result = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
let partial: boolean | undefined;
({ partial, ...result } = result);
- if (options?.skip || options?.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
- // set to `false` (since we aren't loading anything).
- //
- // NOTE: We no longer think this is the correct behavior. Skipping should
- // not automatically set `data` to `undefined`, but instead leave the
- // previous data in place. In other words, skipping should not mandate that
- // previously received data is all of a sudden removed. Unfortunately,
- // changing this is breaking, so we'll have to wait until Apollo Client 4.0
- // to address this.
- result = {
- loading: false,
- data: void 0 as unknown as TData,
- error: void 0,
- networkStatus: NetworkStatus.ready,
- };
- } else {
+
+ {
// BAD BOY CODE BLOCK WHERE WE PUT SIDE-EFFECTS IN THE RENDER FUNCTION
+ //
// TODO: This code should be removed when the partialRefetch option is
// removed. I was unable to get this hook to behave reasonably in certain
// edge cases when this block was put in an effect.
@@ -247,22 +252,71 @@ export function useQuery<
!options?.skip &&
result.loading
) {
- obsQuery.setOptions(
- createWatchQueryOptions(query, options, defaultWatchQueryOptions)
- ).catch(() => {});
+ obsQuery.setOptions(createWatchQueryOptions(query, options, defaultWatchQueryOptions)).catch(() => {});
}
// We assign options during rendering as a guard to make sure that
// callbacks like onCompleted and onError are not stale.
- // TODO
Object.assign(ref.current, { options });
}
+ if (
+ (context.renderPromises || client.disableNetworkFetches) &&
+ options?.ssr === false
+ ) {
+ // If SSR has been explicitly disabled, and this function has been called
+ // on the server side, return the default loading state.
+ result = ref.current.result = {
+ loading: true,
+ data: void 0 as unknown as TData,
+ error: void 0,
+ networkStatus: NetworkStatus.loading,
+ };
+ } else if (options?.skip || options?.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
+ // set to `false` (since we aren't loading anything).
+ //
+ // NOTE: We no longer think this is the correct behavior. Skipping should
+ // not automatically set `data` to `undefined`, but instead leave the
+ // previous data in place. In other words, skipping should not mandate that
+ // previously received data is all of a sudden removed. Unfortunately,
+ // changing this is breaking, so we'll have to wait until Apollo Client 4.0
+ // to address this.
+ result = {
+ loading: false,
+ data: void 0 as unknown as TData,
+ error: void 0,
+ networkStatus: NetworkStatus.ready,
+ };
+ }
+
+ if (result.errors && result.errors.length) {
+ // Until a set naming convention for networkError and graphQLErrors is
+ // decided upon, we map errors (graphQLErrors) to the error options.
+ // TODO: Is it possible for both result.error and result.errors to be
+ // defined here?
+ result = {
+ ...result,
+ error: result.error || new ApolloError({ graphQLErrors: result.errors }),
+ };
+ }
+
+ const obsQueryFields = useMemo(() => ({
+ refetch: obsQuery.refetch.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]);
+
return {
- ...obsQueryMethods,
+ ...obsQueryFields,
variables: createWatchQueryOptions(query, options, defaultWatchQueryOptions).variables,
client,
called: true,
+ previousData: ref.current.previousData,
...result,
};
}
@@ -275,7 +329,9 @@ function createWatchQueryOptions(
options: QueryHookOptions = {},
defaultOptions?: Partial>
): WatchQueryOptions {
- // Using destructuring to remove hook specific options.
+ // TODO: For some reason, we pass context, which is the React Apollo Context,
+ // into observable queries, and test for that.
+ // removing hook specific options
const {
skip,
ssr,
@@ -284,8 +340,6 @@ function createWatchQueryOptions(
displayName,
...otherOptions
} = options;
- // TODO: For some reason, we pass context, which is the React Apollo Context,
- // into observable queries, and test for that.
let watchQueryOptions = { query, ...otherOptions };
if (defaultOptions) {
diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts
index be264a5185e..f1335048b53 100644
--- a/src/react/hooks/useSubscription.ts
+++ b/src/react/hooks/useSubscription.ts
@@ -45,14 +45,16 @@ export function useSubscription(
shouldResubscribe = !!shouldResubscribe(options!);
}
- if (options?.skip && !options?.skip !== !ref.current.options?.skip) {
- setResult({
- loading: false,
- data: void 0,
- error: void 0,
- variables: options?.variables,
- });
- setObservable(null);
+ if (options?.skip) {
+ if (!options?.skip !== !ref.current.options?.skip) {
+ setResult({
+ loading: false,
+ data: void 0,
+ error: void 0,
+ variables: options?.variables,
+ });
+ setObservable(null);
+ }
} else if (
shouldResubscribe !== false && (
client !== ref.current.client ||
diff --git a/src/react/ssr/RenderPromises.ts b/src/react/ssr/RenderPromises.ts
index ad65067b618..fb74c87c304 100644
--- a/src/react/ssr/RenderPromises.ts
+++ b/src/react/ssr/RenderPromises.ts
@@ -44,22 +44,21 @@ export class RenderPromises {
// Registers the server side rendered observable.
public registerSSRObservable(
observable: ObservableQuery,
- options: QueryDataOptions
+ props: QueryDataOptions
) {
if (this.stopped) return;
- this.lookupQueryInfo(options).observable = observable;
+ this.lookupQueryInfo(props).observable = observable;
}
// Get's the cached observable that matches the SSR Query instances query and variables.
public getSSRObservable(
- options: QueryDataOptions
+ props: QueryDataOptions
): ObservableQuery | null {
- return this.lookupQueryInfo(options).observable;
+ return this.lookupQueryInfo(props).observable;
}
public addQueryPromise(
queryInstance: QueryData,
- // TODO: This callback is a noop on the useQuery side.
finish: () => React.ReactNode
): React.ReactNode {
if (!this.stopped) {
@@ -67,14 +66,15 @@ export class RenderPromises {
if (!info.seen) {
this.queryPromises.set(
queryInstance.getOptions(),
- new Promise(resolve => resolve(queryInstance.fetchData()))
+ new Promise(resolve => {
+ resolve(queryInstance.fetchData());
+ })
);
// Render null to abandon this subtree for this rendering, so that we
// can wait for the data to arrive.
return null;
}
}
-
return finish();
}
diff --git a/src/react/ssr/__tests__/useLazyQuery.test.tsx b/src/react/ssr/__tests__/useLazyQuery.test.tsx
index 811c3b2e458..963f9726777 100644
--- a/src/react/ssr/__tests__/useLazyQuery.test.tsx
+++ b/src/react/ssr/__tests__/useLazyQuery.test.tsx
@@ -40,7 +40,7 @@ describe('useLazyQuery Hook SSR', () => {
const client = new ApolloClient({
cache: new InMemoryCache(),
link,
- ssrMode: true,
+ ssrMode: true
});
const Component = () => {
diff --git a/src/react/ssr/__tests__/useQuery.test.tsx b/src/react/ssr/__tests__/useQuery.test.tsx
index 62584b130c5..0e5aacb584b 100644
--- a/src/react/ssr/__tests__/useQuery.test.tsx
+++ b/src/react/ssr/__tests__/useQuery.test.tsx
@@ -1,4 +1,3 @@
-/** @jest-environment node */
import React from 'react';
import { DocumentNode } from 'graphql';
import gql from 'graphql-tag';
diff --git a/src/react/types/types.ts b/src/react/types/types.ts
index 4f3bc908aac..f74817d8117 100644
--- a/src/react/types/types.ts
+++ b/src/react/types/types.ts
@@ -78,7 +78,7 @@ export interface QueryResult
error?: ApolloError;
loading: boolean;
networkStatus: NetworkStatus;
- called: true;
+ called: boolean;
}
export interface QueryDataOptions