Skip to content

Commit

Permalink
fix(react): merge catchOn, throwOn as enabled (#570)
Browse files Browse the repository at this point in the history
# Overview

I merge `catchOn`, `throwOn` as `enabled` to simplify them. Specially
thanks to @tooooo1, @sungh0lim, you make me make them as one

### Why name it as `enabled`
1. Positive word that mean condition
2. Friendly word (React Suspense user have used `enabled` option of
TanStack Query's useQuery before mostly)
3. It can make meaning ErrorBoundary catch all error in children as
default. others can not

## AS-IS (`catchOn`, `throwOn`)

![image](https://github.com/suspensive/react/assets/61593290/af2a70ec-34d1-4ae7-9b5a-dbc884420d97)

## TO-BE (`enabled`)

![image](https://github.com/suspensive/react/assets/61593290/5308d2c9-ce98-4022-a7fd-485d49349f21)

## PR Checklist

- [x] I did below actions if need

1. I read the [Contributing
Guide](https://github.com/suspensive/react/blob/main/CONTRIBUTING.md)
2. I added documents and tests.
  • Loading branch information
manudeli authored Jan 7, 2024
1 parent 8236bff commit 4ce6fab
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 236 deletions.
4 changes: 2 additions & 2 deletions .changeset/perfect-kangaroos-brake.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@suspensive/react": minor
'@suspensive/react': minor
---

feat(react): add catchOn throwOn prop on ErrorBoundary
feat(react): add enabled prop of ErrorBoundary
119 changes: 36 additions & 83 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { useDevModeObserve } from './contexts'
import { Delay } from './Delay'
import { ErrorBoundaryGroupContext } from './ErrorBoundaryGroup'
import type { PropsWithDevMode } from './utility-types'
import type { ConstructorType, PropsWithDevMode } from './utility-types'
import { assert, hasResetKeysChanged } from './utils'
import {
assertMessageUseErrorBoundaryFallbackPropsOnlyInFallbackOfErrorBoundary,
Expand All @@ -33,51 +33,36 @@ export interface ErrorBoundaryFallbackProps<TError extends Error = Error> {
reset: () => void
}

type Condition = (error: Error) => boolean
type ConstructorType<TClass> = new (...args: any[]) => TClass
type EnabledCallback = (error: Error) => boolean
type Enabled = ConstructorType<Error> | EnabledCallback
const shouldCatch = (error: Error, enabled: Enabled) =>
(enabled.prototype instanceof Error && error instanceof enabled) ||
(typeof enabled === 'function' && (enabled as EnabledCallback)(error))

export type ErrorBoundaryProps = PropsWithDevMode<
PropsWithChildren<
{
/**
* an array of elements for the ErrorBoundary to check each render. If any of those elements change between renders, then the ErrorBoundary will reset the state which will re-render the children
*/
resetKeys?: unknown[]
/**
* when ErrorBoundary is reset by resetKeys or fallback's props.reset, onReset will be triggered
*/
onReset?(): void
/**
* when ErrorBoundary catch error, onError will be triggered
*/
onError?(error: Error, info: ErrorInfo): void
/**
* when ErrorBoundary catch error, fallback will be render instead of children
*/
fallback: ReactNode | FunctionComponent<ErrorBoundaryFallbackProps>
} & (
| {
/**
* @experimental This is experimental feature.
*/
throwOn?: [ConstructorType<Error> | Condition, ...(ConstructorType<Error> | Condition)[]]
/**
* @experimental This is experimental feature.
*/
catchOn?: undefined
}
| {
/**
* @experimental This is experimental feature.
*/
throwOn?: undefined
/**
* @experimental This is experimental feature.
*/
catchOn?: [ConstructorType<Error> | Condition, ...(ConstructorType<Error> | Condition)[]]
}
)
>,
PropsWithChildren<{
/**
* an array of elements for the ErrorBoundary to check each render. If any of those elements change between renders, then the ErrorBoundary will reset the state which will re-render the children
*/
resetKeys?: unknown[]
/**
* when ErrorBoundary is reset by resetKeys or fallback's props.reset, onReset will be triggered
*/
onReset?(): void
/**
* when ErrorBoundary catch error, onError will be triggered
*/
onError?(error: Error, info: ErrorInfo): void
/**
* when ErrorBoundary catch error, fallback will be render instead of children
*/
fallback: ReactNode | FunctionComponent<ErrorBoundaryFallbackProps>
/**
* @experimental This is experimental feature.
* @default true
*/
enabled?: Enabled | [Enabled, ...Enabled[]] | boolean
}>,
ErrorBoundaryDevModeOptions
>

Expand Down Expand Up @@ -114,7 +99,7 @@ class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState
}

render() {
const { children, fallback, catchOn, throwOn } = this.props
const { children, fallback, enabled = true } = this.props

if (this.state.isError && typeof fallback === 'undefined') {
if (process.env.NODE_ENV !== 'production') {
Expand All @@ -124,50 +109,18 @@ class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState
}

let childrenOrFallback = children

if (this.state.isError) {
let isReThrow = false
if (throwOn) {
for (let i = 0; i < throwOn.length; i++) {
const error = throwOn[i],
condition = throwOn[i]
if (error.prototype instanceof Error) {
if (this.state.error instanceof error) {
isReThrow = true
break
}
} else if (typeof condition === 'function') {
if ((condition as Condition)(this.state.error)) {
isReThrow = true
break
}
}
}
}
if (catchOn) {
for (let i = 0; i < catchOn.length; i++) {
const error = catchOn[i],
condition = catchOn[i]
if (error.prototype instanceof Error) {
if (this.state.error instanceof error) {
isReThrow = false
break
}
} else if (typeof condition === 'function') {
if ((condition as Condition)(this.state.error)) {
isReThrow = false
break
}
}
}
}
if (isReThrow) {
const boundaryShouldCatch = Array.isArray(enabled)
? enabled.some((enabledItem) => this.state.isError && shouldCatch(this.state.error, enabledItem))
: typeof enabled === 'boolean'
? enabled
: shouldCatch(this.state.error, enabled)
if (!boundaryShouldCatch) {
throw this.state.error
}

if (typeof fallback === 'function') {
const FallbackComponent = fallback

childrenOrFallback = <FallbackComponent error={this.state.error} reset={this.reset} />
} else {
childrenOrFallback = fallback
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/utility-types/ConstructorType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ConstructorType<TClass> = new (...args: any[]) => TClass
1 change: 1 addition & 0 deletions packages/react/src/utility-types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export type { PropsWithDevMode } from './PropsWithDevMode'
export type { OmitKeyOf } from './OmitKeyOf'
export type { ConstructorType } from './ConstructorType'
3 changes: 1 addition & 2 deletions websites/visualization/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</Link>
</nav>
<Link href="/react/DevMode">🔗 @suspensive/react - DevMode</Link>
<Link href="/react/ErrorBoundary/catchOn">🔗 @suspensive/react - ErrorBoundary catchOn</Link>
<Link href="/react/ErrorBoundary/throwOn">🔗 @suspensive/react - ErrorBoundary throwOn</Link>
<Link href="/react/ErrorBoundary/enabled">🔗 @suspensive/react - ErrorBoundary enabled</Link>
<Link href="/react-image">🔗 @suspensive/react-image</Link>
<div className="flex flex-1 items-center justify-center">{children}</div>
</Providers>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'

import { ErrorBoundary, Suspense } from '@suspensive/react'
import { useSuspenseQuery } from '@suspensive/react-query'
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
import { AxiosError, isAxiosError } from 'axios'
import { Area, Box, Button } from '~/components/uis'
import { api } from '~/utils'

export default function Page() {
const queryError = useQueryErrorResetBoundary()

return (
<ErrorBoundary
onReset={queryError.reset}
fallback={({ error, reset }) => (
<Box.Error>
Global: unknown error: {error.message}
<Button onClick={reset}></Button>
</Box.Error>
)}
>
<Area title="Page" className="h-96">
<ErrorBoundary
enabled={AxiosError} // enabled only AxiosError
onReset={queryError.reset}
fallback={({ error, reset }) => (
<Box.Error>
Page: AxiosError: Network error: {error.message}
<Button onClick={reset}></Button>
</Box.Error>
)}
>
<Area title="Section">
<ErrorBoundary
enabled={(error) => !isAxiosError(error)} // enabled if not AxiosError
onReset={queryError.reset}
fallback={({ error, reset }) => (
<Box.Error>
Section: {error.message}
<Button onClick={reset}></Button>
</Box.Error>
)}
>
<Suspense clientOnly>
<AxiosErrorOrJustErrorMaker />
</Suspense>
</ErrorBoundary>
</Area>
</ErrorBoundary>
</Area>
</ErrorBoundary>
)
}

const AxiosErrorOrJustErrorMaker = () => {
if (Math.random() > 0.5) {
throw new Error('sometimes not AxiosError')
}

useSuspenseQuery({
queryKey: ['ErrorBoundary', 'enabled'],
queryFn: () => api.delay(1000, { percentage: 0 }),
})

return <>success</>
}

This file was deleted.

0 comments on commit 4ce6fab

Please sign in to comment.