Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/src/pages/reference/useMutation.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ mutate(variables, {
- This function receives a `retryAttempt` integer and the actual Error and returns the delay to apply before the next attempt in milliseconds.
- A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff.
- A function like `attempt => attempt * 1000` applies linear backoff.
- `useErrorBoundary`
- Defaults to the global query config's `useErrorBoundary` value, which is `false`
- Set this to true if you want mutation errors to be thrown in the render phase and propagate to the nearest error boundary
- `useErrorBoundary: undefined | boolean | (error: TError) => boolean`
- Defaults to the global query config's `useErrorBoundary` value, which is `undefined`
- Set this to `true` if you want mutation errors to be thrown in the render phase and propagate to the nearest error boundary
- Set this to `false` to disable the behaviour of throwing errors to the error boundary.
- If set to a function, it will be passed the error and should return a boolean indicating whether to show the error in an error boundary (`true`) or return the error as state (`false`)

**Returns**

Expand Down
8 changes: 5 additions & 3 deletions docs/src/pages/reference/useQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,11 @@ const result = useQuery({
- Optional
- Defaults to `true`
- If set to `false`, structural sharing between query results will be disabled.
- `useErrorBoundary: boolean`
- Defaults to the global query config's `useErrorBoundary` value, which is false
- Set this to true if you want errors to be thrown in the render phase and propagated to the nearest error boundary
- `useErrorBoundary: undefined | boolean | (error: TError) => boolean`
- Defaults to the global query config's `useErrorBoundary` value, which is `undefined`
- Set this to `true` if you want errors to be thrown in the render phase and propagate to the nearest error boundary
- Set this to `false` to disable `suspense`'s default behaviour of throwing errors to the error boundary.
- If set to a function, it will be passed the error and should return a boolean indicating whether to show the error in an error boundary (`true`) or return the error as state (`false`)

**Returns**

Expand Down
7 changes: 5 additions & 2 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,12 @@ export interface QueryObserverOptions<
onSettled?: (data: TData | undefined, error: TError | null) => void
/**
* Whether errors should be thrown instead of setting the `error` property.
* If set to `true` or `suspense` is `true`, all errors will be thrown to the error boundary.
* If set to `false` and `suspense` is `false`, errors are returned as state.
* If set to a function, it will be passed the error and should return a boolean indicating whether to show the error in an error boundary (`true`) or return the error as state (`false`).
* Defaults to `false`.
*/
useErrorBoundary?: boolean
useErrorBoundary?: boolean | ((error: TError) => boolean)
/**
* This option can be used to transform or select a part of the data returned by the query function.
*/
Expand Down Expand Up @@ -527,7 +530,7 @@ export interface MutationObserverOptions<
TVariables = void,
TContext = unknown
> extends MutationOptions<TData, TError, TVariables, TContext> {
useErrorBoundary?: boolean
useErrorBoundary?: boolean | ((error: TError) => boolean)
}

export interface MutateOptions<
Expand Down
179 changes: 179 additions & 0 deletions src/react/tests/suspense.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,185 @@ describe("useQuery's in Suspense mode", () => {
consoleMock.mockRestore()
})

it('should throw errors to the error boundary by default', async () => {
const key = queryKey()

const consoleMock = mockConsoleError()

function Page() {
useQuery(
key,
async () => {
await sleep(10)
throw new Error('Suspense Error a1x')
},
{
retry: false,
suspense: true,
}
)
return <div>rendered</div>
}

function App() {
return (
<ErrorBoundary
fallbackRender={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}

const rendered = renderWithClient(queryClient, <App />)

await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('error boundary'))

consoleMock.mockRestore()
})

it('should not throw errors to the error boundary when useErrorBoundary: false', async () => {
const key = queryKey()

const consoleMock = mockConsoleError()

function Page() {
useQuery(
key,
async () => {
await sleep(10)
throw new Error('Suspense Error a2x')
},
{
retry: false,
suspense: true,
useErrorBoundary: false,
}
)
return <div>rendered</div>
}

function App() {
return (
<ErrorBoundary
fallbackRender={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}

const rendered = renderWithClient(queryClient, <App />)

await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('rendered'))

consoleMock.mockRestore()
})

it('should not throw errors to the error boundary when a useErrorBoundary function returns true', async () => {
const key = queryKey()

const consoleMock = mockConsoleError()

function Page() {
useQuery(
key,
async () => {
await sleep(10)
return Promise.reject('Remote Error')
},
{
retry: false,
suspense: true,
useErrorBoundary: err => err !== 'Local Error',
}
)
return <div>rendered</div>
}

function App() {
return (
<ErrorBoundary
fallbackRender={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}

const rendered = renderWithClient(queryClient, <App />)

await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('error boundary'))

consoleMock.mockRestore()
})

it('should not throw errors to the error boundary when a useErrorBoundary function returns false', async () => {
const key = queryKey()

const consoleMock = mockConsoleError()

function Page() {
useQuery(
key,
async () => {
await sleep(10)
return Promise.reject('Local Error')
},
{
retry: false,
suspense: true,
useErrorBoundary: err => err !== 'Local Error',
}
)
return <div>rendered</div>
}

function App() {
return (
<ErrorBoundary
fallbackRender={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}

const rendered = renderWithClient(queryClient, <App />)

await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('rendered'))

consoleMock.mockRestore()
})

it('should not call the queryFn when not enabled', async () => {
const key = queryKey()

Expand Down
109 changes: 109 additions & 0 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
QueryFunction,
QueryFunctionContext,
} from '../..'
import { ErrorBoundary } from 'react-error-boundary'

describe('useQuery', () => {
const queryCache = new QueryCache()
Expand Down Expand Up @@ -2552,6 +2553,114 @@ describe('useQuery', () => {
consoleMock.mockRestore()
})

it('should throw error if queryFn throws and useErrorBoundary is in use', async () => {
const key = queryKey()
const consoleMock = mockConsoleError()

function Page() {
const { status, error } = useQuery<undefined, string>(
key,
() => Promise.reject('Error test jaylen'),
{ retry: false, useErrorBoundary: true }
)

return (
<div>
<h1>{status}</h1>
<h2>{error}</h2>
</div>
)
}

const rendered = renderWithClient(
queryClient,
<ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
<Page />
</ErrorBoundary>
)

await waitFor(() => rendered.getByText('error boundary'))

consoleMock.mockRestore()
})

it('should set status to error instead of throwing when error should not be thrown', async () => {
const key = queryKey()
const consoleMock = mockConsoleError()

function Page() {
const { status, error } = useQuery<undefined, string>(
key,
() => Promise.reject('Local Error'),
{
retry: false,
useErrorBoundary: err => err !== 'Local Error',
}
)

return (
<div>
<h1>{status}</h1>
<h2>{error}</h2>
</div>
)
}

const rendered = renderWithClient(
queryClient,
<ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
<Page />
</ErrorBoundary>
)

await waitFor(() => rendered.getByText('error'))
await waitFor(() => rendered.getByText('Local Error'))

consoleMock.mockRestore()
})

it('should throw error instead of setting status when error should be thrown', async () => {
const key = queryKey()
const consoleMock = mockConsoleError()

function Page() {
const { status, error } = useQuery<undefined, string>(
key,
() => Promise.reject('Remote Error'),
{
retry: false,
useErrorBoundary: err => err !== 'Local Error',
}
)

return (
<div>
<h1>{status}</h1>
<h2>{error}</h2>
</div>
)
}

const rendered = renderWithClient(
queryClient,
<ErrorBoundary
fallbackRender={({ error }) => (
<div>
<div>error boundary</div>
<div>{error}</div>
</div>
)}
>
<Page />
</ErrorBoundary>
)

await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('Remote Error'))

consoleMock.mockRestore()
})

it('should always fetch if refetchOnMount is set to always', async () => {
const key = queryKey()
const states: UseQueryResult<string>[] = []
Expand Down
2 changes: 1 addition & 1 deletion src/react/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export interface UseMutationOptions<
) => Promise<unknown> | void
retry?: RetryValue<TError>
retryDelay?: RetryDelayValue<TError>
useErrorBoundary?: boolean
useErrorBoundary?: boolean | ((error: TError) => boolean)
}

export type UseMutateFunction<
Expand Down
9 changes: 7 additions & 2 deletions src/react/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { QueryObserver } from '../core/queryObserver'
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
import { useQueryClient } from './QueryClientProvider'
import { UseBaseQueryOptions } from './types'
import { shouldThrowError } from './utils'

export function useBaseQuery<
TQueryFnData,
Expand Down Expand Up @@ -123,9 +124,13 @@ export function useBaseQuery<

// Handle error boundary
if (
(defaultedOptions.suspense || defaultedOptions.useErrorBoundary) &&
result.isError &&
!result.isFetching
!result.isFetching &&
shouldThrowError(
defaultedOptions.suspense,
defaultedOptions.useErrorBoundary,
result.error
)
) {
throw result.error
}
Expand Down
Loading