Skip to content

Commit 954a1a0

Browse files
committed
feat: add reset error boundary component
1 parent 7730fee commit 954a1a0

File tree

5 files changed

+143
-14
lines changed

5 files changed

+143
-14
lines changed

docs/src/pages/docs/guides/suspense.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,36 @@ In addition to queries behaving differently in suspense mode, mutations also beh
4343

4444
## Resetting Error Boundaries
4545

46-
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.
46+
Whether you are using **suspense** or **useErrorBoundaries** in your queries, you will need to know how to use the `ReactQueryResetErrorBoundary` component to let queries within this boundary know that you want them to try again when you render them again.
47+
48+
There are two ways to trigger a reset:
49+
50+
1. By providing a function as a child of `ReactQueryResetErrorBoundary`. The first argument will be an object with a `reset` function.
51+
2. By using the `useResetErrorBoundary()` hook. The returned value will be an object with a `reset` function.
4752

4853
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:
4954

5055
```js
51-
import { queryCache } from "react-query";
52-
import { ErrorBoundary } from "react-error-boundary";
53-
54-
<ErrorBoundary
55-
onReset={() => queryCache.resetErrorBoundaries()}
56-
fallbackRender={({ error, resetErrorBoundary }) => (
57-
<div>
58-
There was an error!
59-
<Button onClick={() => resetErrorBoundary()}>Try again</Button>
60-
</div>
61-
)}
62-
>
56+
import { ReactQueryResetErrorBoundary } from 'react-query'
57+
import { ErrorBoundary } from 'react-error-boundary'
58+
59+
const App: React.FC = () => (
60+
<ReactQueryResetErrorBoundary>
61+
{({ reset }) => (
62+
<ErrorBoundary
63+
onReset={reset}
64+
fallbackRender={({ error, resetErrorBoundary }) => (
65+
<div>
66+
There was an error!
67+
<Button onClick={() => resetErrorBoundary()}>Try again</Button>
68+
</div>
69+
)}
70+
>
71+
<Page />
72+
</ErrorBoundary>
73+
)}
74+
</ReactQueryResetErrorBoundary>
75+
)
6376
```
6477

6578
## Fetch-on-render vs Render-as-you-fetch
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react'
2+
3+
interface ReactQueryResetErrorBoundaryValue {
4+
isReset: boolean
5+
reset: () => void
6+
}
7+
8+
export interface ReactQueryResetErrorBoundaryProps {
9+
children:
10+
| ((value: ReactQueryResetErrorBoundaryValue) => React.ReactNode)
11+
| React.ReactNode
12+
}
13+
14+
const context = React.createContext<ReactQueryResetErrorBoundaryValue>({
15+
isReset: false,
16+
reset: () => undefined,
17+
})
18+
19+
export const useResetErrorBoundary = () => React.useContext(context)
20+
21+
export const ReactQueryResetErrorBoundary: React.FC<ReactQueryResetErrorBoundaryProps> = ({
22+
children,
23+
}) => {
24+
const [isReset, setIsReset] = React.useState(false)
25+
const reset = React.useCallback(() => {
26+
setIsReset(true)
27+
}, [])
28+
const value = React.useMemo(() => ({ isReset, reset }), [isReset, reset])
29+
return (
30+
<context.Provider value={value}>
31+
{typeof children === 'function'
32+
? (children as Function)(value)
33+
: children}
34+
</context.Provider>
35+
)
36+
}

src/react/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ export {
33
useQueryCache,
44
} from './ReactQueryCacheProvider'
55
export { ReactQueryConfigProvider } from './ReactQueryConfigProvider'
6+
export {
7+
ReactQueryResetErrorBoundary,
8+
useResetErrorBoundary,
9+
} from './ReactQueryResetErrorBoundary'
610
export { useIsFetching } from './useIsFetching'
711
export { useMutation } from './useMutation'
812
export { useQuery } from './useQuery'
@@ -15,3 +19,4 @@ export type { UseInfiniteQueryObjectConfig } from './useInfiniteQuery'
1519
export type { UsePaginatedQueryObjectConfig } from './usePaginatedQuery'
1620
export type { ReactQueryCacheProviderProps } from './ReactQueryCacheProvider'
1721
export type { ReactQueryConfigProviderProps } from './ReactQueryConfigProvider'
22+
export type { ReactQueryResetErrorBoundaryProps } from './ReactQueryResetErrorBoundary'

src/react/tests/suspense.test.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as React from 'react'
55
import { sleep, queryKey, mockConsoleError } from './utils'
66
import { useQuery } from '..'
77
import { queryCache } from '../../core'
8+
import { ReactQueryResetErrorBoundary } from '../ReactQueryResetErrorBoundary'
89

910
describe("useQuery's in Suspense mode", () => {
1011
it('should not call the queryFn twice when used in Suspense mode', async () => {
@@ -192,6 +193,73 @@ describe("useQuery's in Suspense mode", () => {
192193
consoleMock.mockRestore()
193194
})
194195

196+
it('should retry fetch if the reset error boundary has been reset', async () => {
197+
const key = queryKey()
198+
199+
let succeed = false
200+
const consoleMock = mockConsoleError()
201+
202+
function Page() {
203+
useQuery(
204+
key,
205+
async () => {
206+
await sleep(10)
207+
208+
if (!succeed) {
209+
throw new Error('Suspense Error Bingo')
210+
} else {
211+
return 'data'
212+
}
213+
},
214+
{
215+
retryDelay: 10,
216+
suspense: true,
217+
}
218+
)
219+
220+
return <div>rendered</div>
221+
}
222+
223+
const rendered = render(
224+
<ReactQueryResetErrorBoundary>
225+
{({ reset }) => (
226+
<ErrorBoundary
227+
onReset={reset}
228+
fallbackRender={({ resetErrorBoundary }) => (
229+
<div>
230+
<div>error boundary</div>
231+
<button
232+
onClick={() => {
233+
succeed = true
234+
resetErrorBoundary()
235+
}}
236+
>
237+
retry
238+
</button>
239+
</div>
240+
)}
241+
>
242+
<React.Suspense fallback={'Loading...'}>
243+
<Page />
244+
</React.Suspense>
245+
</ErrorBoundary>
246+
)}
247+
</ReactQueryResetErrorBoundary>
248+
)
249+
250+
await waitFor(() => rendered.getByText('Loading...'))
251+
252+
await waitFor(() => rendered.getByText('error boundary'))
253+
254+
await waitFor(() => rendered.getByText('retry'))
255+
256+
fireEvent.click(rendered.getByText('retry'))
257+
258+
await waitFor(() => rendered.getByText('rendered'))
259+
260+
consoleMock.mockRestore()
261+
})
262+
195263
it('should not call the queryFn when not enabled', async () => {
196264
const key = queryKey()
197265

src/react/useBaseQuery.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import { useRerenderer } from './utils'
44
import { QueryObserver } from '../core/queryObserver'
55
import { QueryResultBase, QueryObserverConfig } from '../core/types'
66
import { useDefaultedQueryConfig } from './useDefaultedQueryConfig'
7+
import { useResetErrorBoundary } from './ReactQueryResetErrorBoundary'
78

89
export function useBaseQuery<TResult, TError>(
910
config: QueryObserverConfig<TResult, TError> = {}
1011
): QueryResultBase<TResult, TError> {
1112
config = useDefaultedQueryConfig(config)
1213

14+
const resetBoundary = useResetErrorBoundary()
15+
1316
// Make a rerender function
1417
const rerender = useRerenderer()
1518

@@ -39,7 +42,11 @@ export function useBaseQuery<TResult, TError>(
3942
if (config.suspense || config.useErrorBoundary) {
4043
const query = observer.getCurrentQuery()
4144

42-
if (result.isError && query.state.throwInErrorBoundary) {
45+
if (
46+
result.isError &&
47+
!resetBoundary.isReset &&
48+
query.state.throwInErrorBoundary
49+
) {
4350
throw result.error
4451
}
4552

0 commit comments

Comments
 (0)