From 4ce6fab88d96ed9d77011b85d594abbc05743ccc Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Mon, 8 Jan 2024 02:43:51 +0900 Subject: [PATCH] fix(react): merge catchOn, throwOn as enabled (#570) # 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. --- .changeset/perfect-kangaroos-brake.md | 4 +- packages/react/src/ErrorBoundary.tsx | 119 ++++++------------ .../src/utility-types/ConstructorType.ts | 1 + packages/react/src/utility-types/index.ts | 1 + websites/visualization/src/app/layout.tsx | 3 +- .../app/react/ErrorBoundary/catchOn/page.tsx | 73 ----------- .../app/react/ErrorBoundary/enabled/page.tsx | 67 ++++++++++ .../app/react/ErrorBoundary/throwOn/page.tsx | 76 ----------- 8 files changed, 108 insertions(+), 236 deletions(-) create mode 100644 packages/react/src/utility-types/ConstructorType.ts delete mode 100644 websites/visualization/src/app/react/ErrorBoundary/catchOn/page.tsx create mode 100644 websites/visualization/src/app/react/ErrorBoundary/enabled/page.tsx delete mode 100644 websites/visualization/src/app/react/ErrorBoundary/throwOn/page.tsx diff --git a/.changeset/perfect-kangaroos-brake.md b/.changeset/perfect-kangaroos-brake.md index ca0568545..6406511c2 100644 --- a/.changeset/perfect-kangaroos-brake.md +++ b/.changeset/perfect-kangaroos-brake.md @@ -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 diff --git a/packages/react/src/ErrorBoundary.tsx b/packages/react/src/ErrorBoundary.tsx index 30fb14b57..27b14aefc 100644 --- a/packages/react/src/ErrorBoundary.tsx +++ b/packages/react/src/ErrorBoundary.tsx @@ -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, @@ -33,51 +33,36 @@ export interface ErrorBoundaryFallbackProps { reset: () => void } -type Condition = (error: Error) => boolean -type ConstructorType = new (...args: any[]) => TClass +type EnabledCallback = (error: Error) => boolean +type Enabled = ConstructorType | 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 - } & ( - | { - /** - * @experimental This is experimental feature. - */ - throwOn?: [ConstructorType | Condition, ...(ConstructorType | Condition)[]] - /** - * @experimental This is experimental feature. - */ - catchOn?: undefined - } - | { - /** - * @experimental This is experimental feature. - */ - throwOn?: undefined - /** - * @experimental This is experimental feature. - */ - catchOn?: [ConstructorType | Condition, ...(ConstructorType | 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 + /** + * @experimental This is experimental feature. + * @default true + */ + enabled?: Enabled | [Enabled, ...Enabled[]] | boolean + }>, ErrorBoundaryDevModeOptions > @@ -114,7 +99,7 @@ class BaseErrorBoundary extends Component 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 = } else { childrenOrFallback = fallback diff --git a/packages/react/src/utility-types/ConstructorType.ts b/packages/react/src/utility-types/ConstructorType.ts new file mode 100644 index 000000000..cffaa89c2 --- /dev/null +++ b/packages/react/src/utility-types/ConstructorType.ts @@ -0,0 +1 @@ +export type ConstructorType = new (...args: any[]) => TClass diff --git a/packages/react/src/utility-types/index.ts b/packages/react/src/utility-types/index.ts index 7870c8b39..23feb20b6 100644 --- a/packages/react/src/utility-types/index.ts +++ b/packages/react/src/utility-types/index.ts @@ -1,2 +1,3 @@ export type { PropsWithDevMode } from './PropsWithDevMode' export type { OmitKeyOf } from './OmitKeyOf' +export type { ConstructorType } from './ConstructorType' diff --git a/websites/visualization/src/app/layout.tsx b/websites/visualization/src/app/layout.tsx index eaca76d2c..00613c8ea 100644 --- a/websites/visualization/src/app/layout.tsx +++ b/websites/visualization/src/app/layout.tsx @@ -22,8 +22,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) 🔗 @suspensive/react - DevMode - 🔗 @suspensive/react - ErrorBoundary catchOn - 🔗 @suspensive/react - ErrorBoundary throwOn + 🔗 @suspensive/react - ErrorBoundary enabled 🔗 @suspensive/react-image
{children}
diff --git a/websites/visualization/src/app/react/ErrorBoundary/catchOn/page.tsx b/websites/visualization/src/app/react/ErrorBoundary/catchOn/page.tsx deleted file mode 100644 index 28ad750f5..000000000 --- a/websites/visualization/src/app/react/ErrorBoundary/catchOn/page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'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 ( - - ( - - Page: Global: unknown error: {props.error.message} - - - )} - > - ( - - Page: AxiosError: Network error: {props.error.message} - - - )} - > -
- - - - ) -} - -class CustomError extends Error {} - -const Section = () => { - const queryError = useQueryErrorResetBoundary() - - return ( - - ( - - Section: {props.error.message} - - - )} - > - - - - - - ) -} - -const AxiosErrorMaker = () => { - useSuspenseQuery({ - queryKey: ['ErrorBoundary', 'catchOn'], - queryFn: () => api.delay(1000, { percentage: 0 }), - }) - - return <>success -} diff --git a/websites/visualization/src/app/react/ErrorBoundary/enabled/page.tsx b/websites/visualization/src/app/react/ErrorBoundary/enabled/page.tsx new file mode 100644 index 000000000..23197827b --- /dev/null +++ b/websites/visualization/src/app/react/ErrorBoundary/enabled/page.tsx @@ -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 ( + ( + + Global: unknown error: {error.message} + + + )} + > + + ( + + Page: AxiosError: Network error: {error.message} + + + )} + > + + !isAxiosError(error)} // enabled if not AxiosError + onReset={queryError.reset} + fallback={({ error, reset }) => ( + + Section: {error.message} + + + )} + > + + + + + + + + + ) +} + +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 +} diff --git a/websites/visualization/src/app/react/ErrorBoundary/throwOn/page.tsx b/websites/visualization/src/app/react/ErrorBoundary/throwOn/page.tsx deleted file mode 100644 index 457bc53b0..000000000 --- a/websites/visualization/src/app/react/ErrorBoundary/throwOn/page.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client' - -import { ErrorBoundary, Suspense } from '@suspensive/react' -import { useSuspenseQuery } from '@suspensive/react-query' -import { useQueryErrorResetBoundary } from '@tanstack/react-query' -import { AxiosError } from 'axios' -import { Area, Box, Button } from '~/components/uis' -import { api } from '~/utils' - -class RootError extends Error {} - -export default function Page() { - const queryError = useQueryErrorResetBoundary() - - return ( - - ( - - Page: Global: unknown error: {props.error.message} - - - )} - > - ( - - Page: AxiosError: Network error: {props.error.message} - - - )} - > -
- - - - ) -} - -const Section = () => { - const queryError = useQueryErrorResetBoundary() - - return ( - - ( - - Section: {props.error.message} - - - )} - > - - - - - - ) -} - -const AxiosErrorMaker = () => { - useSuspenseQuery({ - queryKey: ['ErrorBoundary', 'throwOn'], - queryFn: () => - api.delay(1000, { percentage: 0 }).catch(() => { - throw new RootError('error should be catch in root') - }), - }) - - return <>success -}