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