queryCache.resetErrorBoundaries()}
- fallbackRender={({ error, resetErrorBoundary }) => (
-
- There was an error!
- resetErrorBoundary()}>Try again
-
- )}
->
+import { useErrorResetBoundary } from 'react-query'
+import { ErrorBoundary } from 'react-error-boundary'
+
+const App: React.FC = () => {
+ const { reset } = useErrorResetBoundary()
+ return (
+ (
+
+ There was an error!
+ resetErrorBoundary()}>Try again
+
+ )}
+ >
+
+
+ )
+}
```
## Fetch-on-render vs Render-as-you-fetch
diff --git a/src/react/ReactQueryErrorResetBoundary.tsx b/src/react/ReactQueryErrorResetBoundary.tsx
new file mode 100644
index 0000000000..ac6a8adf92
--- /dev/null
+++ b/src/react/ReactQueryErrorResetBoundary.tsx
@@ -0,0 +1,51 @@
+import React from 'react'
+
+// CONTEXT
+
+interface ReactQueryErrorResetBoundaryValue {
+ clearReset: () => void
+ isReset: () => boolean
+ reset: () => void
+}
+
+function createValue(): ReactQueryErrorResetBoundaryValue {
+ let isReset = true
+ return {
+ clearReset: () => {
+ isReset = false
+ },
+ reset: () => {
+ isReset = true
+ },
+ isReset: () => {
+ return isReset
+ },
+ }
+}
+
+const context = React.createContext(createValue())
+
+// HOOK
+
+export const useErrorResetBoundary = () => React.useContext(context)
+
+// COMPONENT
+
+export interface ReactQueryErrorResetBoundaryProps {
+ children:
+ | ((value: ReactQueryErrorResetBoundaryValue) => React.ReactNode)
+ | React.ReactNode
+}
+
+export const ReactQueryErrorResetBoundary: React.FC = ({
+ children,
+}) => {
+ const value = React.useMemo(() => createValue(), [])
+ return (
+
+ {typeof children === 'function'
+ ? (children as Function)(value)
+ : children}
+
+ )
+}
diff --git a/src/react/index.ts b/src/react/index.ts
index 4a645f15bd..93a54f7e44 100644
--- a/src/react/index.ts
+++ b/src/react/index.ts
@@ -3,6 +3,10 @@ export {
useQueryCache,
} from './ReactQueryCacheProvider'
export { ReactQueryConfigProvider } from './ReactQueryConfigProvider'
+export {
+ ReactQueryErrorResetBoundary,
+ useErrorResetBoundary,
+} from './ReactQueryErrorResetBoundary'
export { useIsFetching } from './useIsFetching'
export { useMutation } from './useMutation'
export { useQuery } from './useQuery'
@@ -15,3 +19,4 @@ export type { UseInfiniteQueryObjectConfig } from './useInfiniteQuery'
export type { UsePaginatedQueryObjectConfig } from './usePaginatedQuery'
export type { ReactQueryCacheProviderProps } from './ReactQueryCacheProvider'
export type { ReactQueryConfigProviderProps } from './ReactQueryConfigProvider'
+export type { ReactQueryErrorResetBoundaryProps } from './ReactQueryErrorResetBoundary'
diff --git a/src/react/tests/suspense.test.tsx b/src/react/tests/suspense.test.tsx
index 0dd3cd0845..387fce907c 100644
--- a/src/react/tests/suspense.test.tsx
+++ b/src/react/tests/suspense.test.tsx
@@ -5,6 +5,10 @@ import * as React from 'react'
import { sleep, queryKey, mockConsoleError } from './utils'
import { useQuery } from '..'
import { queryCache } from '../../core'
+import {
+ ReactQueryErrorResetBoundary,
+ useErrorResetBoundary,
+} from '../ReactQueryErrorResetBoundary'
describe("useQuery's in Suspense mode", () => {
it('should not call the queryFn twice when used in Suspense mode', async () => {
@@ -192,6 +196,135 @@ describe("useQuery's in Suspense mode", () => {
consoleMock.mockRestore()
})
+ it('should retry fetch if the reset error boundary has been reset', async () => {
+ const key = queryKey()
+
+ let succeed = false
+ const consoleMock = mockConsoleError()
+
+ function Page() {
+ useQuery(
+ key,
+ async () => {
+ await sleep(10)
+ if (!succeed) {
+ throw new Error('Suspense Error Bingo')
+ } else {
+ return 'data'
+ }
+ },
+ {
+ retry: false,
+ suspense: true,
+ }
+ )
+ return rendered
+ }
+
+ const rendered = render(
+
+ {({ reset }) => (
+ (
+
+
error boundary
+
{
+ resetErrorBoundary()
+ }}
+ >
+ retry
+
+
+ )}
+ >
+
+
+
+
+ )}
+
+ )
+
+ await waitFor(() => rendered.getByText('Loading...'))
+ await waitFor(() => rendered.getByText('error boundary'))
+ await waitFor(() => rendered.getByText('retry'))
+ fireEvent.click(rendered.getByText('retry'))
+ await waitFor(() => rendered.getByText('error boundary'))
+ await waitFor(() => rendered.getByText('retry'))
+ succeed = true
+ fireEvent.click(rendered.getByText('retry'))
+ await waitFor(() => rendered.getByText('rendered'))
+
+ consoleMock.mockRestore()
+ })
+
+ it('should retry fetch if the reset error boundary has been reset with global hook', async () => {
+ const key = queryKey()
+
+ let succeed = false
+ const consoleMock = mockConsoleError()
+
+ function Page() {
+ useQuery(
+ key,
+ async () => {
+ await sleep(10)
+ if (!succeed) {
+ throw new Error('Suspense Error Bingo')
+ } else {
+ return 'data'
+ }
+ },
+ {
+ retry: false,
+ suspense: true,
+ }
+ )
+ return rendered
+ }
+
+ function App() {
+ const { reset } = useErrorResetBoundary()
+ return (
+ (
+
+
error boundary
+
{
+ resetErrorBoundary()
+ }}
+ >
+ retry
+
+
+ )}
+ >
+
+
+
+
+ )
+ }
+
+ const rendered = render( )
+
+ await waitFor(() => rendered.getByText('Loading...'))
+ await waitFor(() => rendered.getByText('error boundary'))
+ await waitFor(() => rendered.getByText('retry'))
+ fireEvent.click(rendered.getByText('retry'))
+ await waitFor(() => rendered.getByText('error boundary'))
+ await waitFor(() => rendered.getByText('retry'))
+ succeed = true
+ fireEvent.click(rendered.getByText('retry'))
+ await waitFor(() => rendered.getByText('rendered'))
+
+ consoleMock.mockRestore()
+ })
+
it('should not call the queryFn when not enabled', async () => {
const key = queryKey()
diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts
index 6509f8efd0..cce86a709d 100644
--- a/src/react/useBaseQuery.ts
+++ b/src/react/useBaseQuery.ts
@@ -3,17 +3,19 @@ import React from 'react'
import { useRerenderer } from './utils'
import { getResolvedQueryConfig } from '../core/config'
import { QueryObserver } from '../core/queryObserver'
-import { QueryResultBase, QueryConfig, QueryKey } from '../core/types'
-import { useQueryCache } from './ReactQueryCacheProvider'
+import { QueryResultBase, QueryKey, QueryConfig } from '../core/types'
+import { useErrorResetBoundary } from './ReactQueryErrorResetBoundary'
+import { useQueryCache } from '.'
import { useContextConfig } from './ReactQueryConfigProvider'
export function useBaseQuery(
queryKey: QueryKey,
config?: QueryConfig
): QueryResultBase {
- const rerender = useRerenderer()
const cache = useQueryCache()
+ const rerender = useRerenderer()
const contextConfig = useContextConfig()
+ const errorResetBoundary = useErrorResetBoundary()
// Get resolved config
const resolvedConfig = getResolvedQueryConfig(
@@ -49,7 +51,11 @@ export function useBaseQuery(
if (resolvedConfig.suspense || resolvedConfig.useErrorBoundary) {
const query = observer.getCurrentQuery()
- if (result.isError && query.state.throwInErrorBoundary) {
+ if (
+ result.isError &&
+ !errorResetBoundary.isReset() &&
+ query.state.throwInErrorBoundary
+ ) {
throw result.error
}
@@ -58,6 +64,7 @@ export function useBaseQuery(
resolvedConfig.suspense &&
!result.isSuccess
) {
+ errorResetBoundary.clearReset()
const unsubscribe = observer.subscribe()
throw observer.fetch().finally(unsubscribe)
}