From 4c741f74994a3d8f87f229e4fb313736616600a0 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 24 Jun 2020 11:45:13 -0600 Subject: [PATCH] feat: added queryCache.resetErrorBoundaries Info can be found in the docs under the `Resetting Error Boundaries` section --- README.md | 24 ++++++- examples/suspense/package.json | 3 +- .../suspense/src/components/ErrorBounderay.js | 24 ------- examples/suspense/src/index.js | 24 +++++-- examples/suspense/src/queries.js | 10 ++- examples/suspense/yarn.lock | 65 ++++++++++--------- src/queryCache.js | 7 ++ src/utils.js | 19 ++---- 8 files changed, 98 insertions(+), 78 deletions(-) delete mode 100644 examples/suspense/src/components/ErrorBounderay.js diff --git a/README.md b/README.md index 5373bd2b50..c2f864e5f2 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,6 @@ This library is being built and maintained by me, @tannerlinsley and I am always - - [Installation](#installation) - [Defaults to keep in mind](#defaults-to-keep-in-mind) - [Queries](#queries) @@ -990,10 +989,31 @@ import { useQuery } from 'react-query' useQuery(queryKey, queryFn, { suspense: true }) ``` -When using suspense mode, `status` states and `error` objects are not needed and are then replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please see the [Suspense Example](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/suspense) for more information on how to set up suspense mode. +When using suspense mode, `status` states and `error` objects are not needed and are then replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please read the [Resetting Error Boundaries](#resetting-error-boundaries) and look at the [Suspense Example](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/master/examples/suspense) for more information on how to set up suspense mode. In addition to queries behaving differently in suspense mode, mutations also behave a bit differently. By default, instead of supplying the `error` variable when a mutation fails, it will be thrown during the next render of the component it's used in and propagate to the nearest error boundary, similar to query errors. If you wish to disable this, you can set the `useErrorBoundary` option to `false`. If you wish that errors are not thrown at all, you can set the `throwOnError` option to `false` as well! +## Resetting Error Boundaries + +Whether you are using **suspense** or **useErrorBoundaries** in your queries, you will need to know how to use the `queryCache.resetErrorBoundaries` function to let queries know that you want them to try again when you render them again. + +How you trigger this function is up to you, but the most common use case is to do it in something like `react-error-boundary`'s `onReset` callback: + +```js +import { queryCache } from "react-query"; +import { ErrorBoundary } from "react-error-boundary"; + + queryCache.resetErrorBoundaries()} + fallbackRender={({ error, resetErrorBoundary }) => ( +
+ There was an error! + +
+ )} +> +``` + ## Fetch-on-render vs Fetch-as-you-render Out of the box, React Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. However, if you want to take it to the next level and implement a `Fetch-as-you-render` model, we recommend implementing [Prefetching](#prefetching) on routing and/or user interactions events to initialize queries before they are needed. diff --git a/examples/suspense/package.json b/examples/suspense/package.json index 2ffbbe1c44..22117693e0 100755 --- a/examples/suspense/package.json +++ b/examples/suspense/package.json @@ -8,7 +8,8 @@ "axios": "0.19.2", "react": "0.0.0-experimental-5faf377df", "react-dom": "0.0.0-experimental-5faf377df", - "react-query": "latest", + "react-error-boundary": "^2.2.3", + "react-query": "^2.0.4", "react-scripts": "3.0.1" }, "devDependencies": { diff --git a/examples/suspense/src/components/ErrorBounderay.js b/examples/suspense/src/components/ErrorBounderay.js deleted file mode 100644 index 6dc36fc99c..0000000000 --- a/examples/suspense/src/components/ErrorBounderay.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -import Button from "./Button"; - -export default class ErrorBoundary extends React.Component { - state = { error: null }; - static getDerivedStateFromError(error) { - return { error }; - } - componentDidCatch() { - // log the error to the server - } - tryAgain = () => this.setState({ error: null }); - render() { - return this.state.error ? ( -
- There was an error. -
{this.state.error.message}
-
- ) : ( - this.props.children - ); - } -} diff --git a/examples/suspense/src/index.js b/examples/suspense/src/index.js index 7c1731e74b..4aba59dd13 100755 --- a/examples/suspense/src/index.js +++ b/examples/suspense/src/index.js @@ -1,12 +1,12 @@ import React, { lazy } from "react"; import ReactDOM from "react-dom"; import { ReactQueryConfigProvider, queryCache } from "react-query"; +import { ErrorBoundary } from "react-error-boundary"; import "./styles.css"; import { fetchProjects } from "./queries"; -import ErrorBounderay from "./components/ErrorBounderay"; import Button from "./components/Button"; const Projects = lazy(() => import("./components/Projects")); @@ -14,8 +14,11 @@ const Project = lazy(() => import("./components/Project")); const queryConfig = { shared: { - suspense: true - } + suspense: true, + }, + queries: { + retry: 0, + }, }; function App() { @@ -26,7 +29,7 @@ function App() { +
{error.message}
+ + )} + onReset={() => queryCache.resetErrorBoundaries()} + > Loading projects...}> {showProjects ? ( activeProject ? ( @@ -52,7 +64,7 @@ function App() { ) ) : null} - +
); } diff --git a/examples/suspense/src/queries.js b/examples/suspense/src/queries.js index 1afc8fdc15..3ac1669882 100644 --- a/examples/suspense/src/queries.js +++ b/examples/suspense/src/queries.js @@ -1,11 +1,17 @@ import axios from "axios"; +let count = 0; + export async function fetchProjects(key) { console.info("fetch projects"); + if (count < 4) { + count++; + throw new Error("testing"); + } let { data } = await axios.get( `https://api.github.com/users/tannerlinsley/repos?sort=updated` ); - await new Promise(r => setTimeout(r, 1000)); + await new Promise((r) => setTimeout(r, 1000)); return data; } @@ -14,6 +20,6 @@ export async function fetchProject(key, { id }) { let { data } = await axios.get( `https://api.github.com/repos/tannerlinsley/${id}` ); - await new Promise(r => setTimeout(r, 1000)); + await new Promise((r) => setTimeout(r, 1000)); return data; } diff --git a/examples/suspense/yarn.lock b/examples/suspense/yarn.lock index 93818fa65e..73919d8929 100644 --- a/examples/suspense/yarn.lock +++ b/examples/suspense/yarn.lock @@ -891,6 +891,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.9.6": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" @@ -1165,6 +1172,11 @@ dependencies: ramda "^0.26.0" +"@scarf/scarf@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.0.6.tgz#52011dfb19187b53b75b7b6eac20da0810ddd88f" + integrity sha512-y4+DuXrAd1W5UIY3zTcsosi/1GyYT8k5jGnZ/wG7UUHVrU+MHlH4Mp87KK2/lvMW4+H7HVcdB+aJhqywgXksjA== + "@svgr/babel-plugin-add-jsx-attribute@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1" @@ -1321,31 +1333,11 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/prop-types@*": - version "15.7.3" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" - integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== - "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== -"@types/react-query@^0.3.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@types/react-query/-/react-query-0.3.4.tgz#6a225b8f90e162deedaa434011c99d730d069046" - integrity sha512-lYBiEkfp+q/gSZ8XMm4YsnGvhMqBZSIUJxIQK9vMmAOCpjw7HbRDeMPC6WV0FuIhbXhTlamD3EepMOzujeChfg== - dependencies: - "@types/react" "*" - -"@types/react@*": - version "16.9.16" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.16.tgz#4f12515707148b1f53a8eaa4341dae5dfefb066d" - integrity sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw== - dependencies: - "@types/prop-types" "*" - csstype "^2.2.0" - "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -3078,11 +3070,6 @@ cssstyle@^1.0.0, cssstyle@^1.1.1: dependencies: cssom "0.3.x" -csstype@^2.2.0: - version "2.6.7" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" - integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== - cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -8074,6 +8061,13 @@ react-dom@0.0.0-experimental-5faf377df: prop-types "^15.6.2" scheduler "0.0.0-experimental-5faf377df" +react-error-boundary@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-2.2.3.tgz#34c8238012d3b4148cec47a1b3cec669d5206578" + integrity sha512-Jiaiu6CJ4ho3sMCVI7gg+O/JB5vlFFZGwlnpFBTCOSyheYRTzz+FhBMo7tfnCTB/ZR0LaMzAPGbZGrEzAOd0eg== + dependencies: + "@babel/runtime" "^7.9.6" + react-error-overlay@^5.1.4: version "5.1.6" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.6.tgz#0cd73407c5d141f9638ae1e0c63e7b2bf7e9929d" @@ -8089,12 +8083,13 @@ react-is@^16.8.1, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== -react-query@latest: - version "0.3.22" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-0.3.22.tgz#042df656c1571df128a818964122d90e4af55edb" - integrity sha512-x05TEfUAT69Qve7090IFBnqzYsl49s8+vx6sEpFAR2C0xdtUplr6ZAeSfya0SRft6ko4vJgktAJCTOV30AihPg== +react-query@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-2.0.4.tgz#dcae7518efa4bdd31410e92ccd4f396d7398dfdc" + integrity sha512-6+hYuRWvPQlFrzoJgT5ZmlCoSxv3LqpiDXaSzbcAtwsEtcUFWz5DKL6L8EpYoreJS2INAQONEYHtXCt1tT4HlQ== dependencies: - "@types/react-query" "^0.3.0" + "@scarf/scarf" "^1.0.6" + ts-toolbelt "^6.9.4" react-scripts@3.0.1: version "3.0.1" @@ -8266,6 +8261,11 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -9452,6 +9452,11 @@ ts-pnp@^1.0.0: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.4.tgz#ae27126960ebaefb874c6d7fa4729729ab200d90" integrity sha512-1J/vefLC+BWSo+qe8OnJQfWTYRS6ingxjwqmHMqaMxXMj7kFtKLgAaYW3JeX3mktjgUL+etlU8/B4VUAUI9QGw== +ts-toolbelt@^6.9.4: + version "6.9.9" + resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.9.9.tgz#e6cfd8ec7d425d2a06bda3b4fe9577ceaf2abda8" + integrity sha512-5a8k6qfbrL54N4Dw+i7M6kldrbjgDWb5Vit8DnT+gwThhvqMg8KtxLE5Vmnft+geIgaSOfNJyAcnmmlflS+Vdg== + tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" diff --git a/src/queryCache.js b/src/queryCache.js index f9631d0ee7..eb89f79942 100644 --- a/src/queryCache.js +++ b/src/queryCache.js @@ -152,6 +152,12 @@ export function makeQueryCache({ frozen = isServer, defaultConfig } = {}) { } } + queryCache.resetErrorBoundaries = () => { + queryCache.getQueries(true).forEach(query => { + query.state.throwInErrorBoundary = false + }) + } + queryCache.buildQuery = (userQueryKey, queryFn, config = {}) => { config = { ...configRef.current.shared, @@ -665,6 +671,7 @@ function switchActions(state, action) { ...(!action.cancelled && { status: statusError, error: action.error, + throwInErrorBoundary: true, }), } case actionSetState: diff --git a/src/utils.js b/src/utils.js index 16d74b4c4d..e4793c39e1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -128,21 +128,14 @@ export function handleSuspense(queryInfo) { queryInfo.query.config.suspense || queryInfo.query.config.useErrorBoundary ) { - if (queryInfo.query.state.status === statusError) { - if (!queryInfo.query.suspenseErrorHandled) { - queryInfo.query.suspenseErrorHandled = true - - setTimeout(() => { - queryInfo.query.state.status = statusLoading - }, 0) - - throw queryInfo.error - } + if ( + queryInfo.query.state.status === statusError && + queryInfo.query.state.throwInErrorBoundary + ) { + throw queryInfo.error } - queryInfo.query.suspenseErrorHandled = false - - if (queryInfo.query.config.suspense && queryInfo.status === statusLoading) { + if (queryInfo.query.config.suspense && queryInfo.status !== statusSuccess) { queryInfo.query.wasSuspended = true throw queryInfo.query.fetch() }