diff --git a/.changeset/two-weeks-serve.md b/.changeset/two-weeks-serve.md
new file mode 100644
index 000000000..12eb91b48
--- /dev/null
+++ b/.changeset/two-weeks-serve.md
@@ -0,0 +1,5 @@
+---
+"@suspensive/react": minor
+---
+
+feat(react): add useErrorBoundaryFallbackProps
diff --git a/packages/react/src/ErrorBoundary.en.mdx b/packages/react/src/ErrorBoundary.en.mdx
index e11dc75aa..b8ec2c519 100644
--- a/packages/react/src/ErrorBoundary.en.mdx
+++ b/packages/react/src/ErrorBoundary.en.mdx
@@ -48,6 +48,68 @@ const ErrorAfter4s = () => {

+:::tip
+
+## Define component as ``'s fallback
+
+### ErrorBoundaryFallbackProps
+
+If you want to deliver a declared component as ``'s fallback, you can use the `ErrorBoundaryFallbackProps` type to declare the component easily.
+
+```tsx
+import type { ErrorBoundaryFallbackProps } from '@suspensive/react'
+
+const ErrorBoundaryFallback = ({ reset, error }: ErrorBoundaryFallbackProps) => (
+ <>
+
+ {error.message}
+ >
+)
+
+const Example = () => (
+
+
+
+)
+```
+
+:::
+
+:::tip
+
+## Use `` fallback props without prop drilling
+
+### useErrorBoundaryFallbackProps
+
+If component using `reset` method and `error` object is nested, prop drilling cannot be avoided.
+The `useErrorBoundaryFallbackProps` allows you to access the `reset` method and `error` objects without prop drilling.
+
+```tsx
+import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react'
+
+const Nested = () => {
+ const { reset, error } = useErrorBoundaryFallbackProps()
+
+ return (
+ <>
+
+ {error.message}
+ >
+ )
+}
+
+// There's no need to pass fallback Prop here!
+const ErrorBoundaryFallback = () =>
+
+const Example = () => (
+
+
+
+)
+```
+
+:::
+
### props.resetKeys
If you want to reset `` by component where is outside of ``'s fallback. Inject any resetKey in resetKeys. resetKeys work only when at least one element of array is changed. you don't need to worry about provide new array as resetKeys like how useEffect's dependency array work.
@@ -123,25 +185,9 @@ useErrorBoundary is experimental feature, this interfaces could be changed
:::
-We can reset `` without props using useErrorBoundary.reset in ErrorBoundaryFallback.
-
-```tsx
-import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
-
-const ErrorBoundaryFallback = () => {
- const errorBoundary = useErrorBoundary()
-
- return
-}
-
-const Example = () => (
-
-
-
-)
-```
+### useErrorBoundary().setError
-In children of ``, we can use useErrorBoundary.setError to make `` aware of the Error without throw.
+In children of ``, we can use useErrorBoundary().setError to make `` aware of the Error without throw.
```tsx
import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
diff --git a/packages/react/src/ErrorBoundary.ko.mdx b/packages/react/src/ErrorBoundary.ko.mdx
index 976be5c01..80e4b7d85 100644
--- a/packages/react/src/ErrorBoundary.ko.mdx
+++ b/packages/react/src/ErrorBoundary.ko.mdx
@@ -48,9 +48,71 @@ const ErrorAfter4s = () => {

+:::tip
+
+## ``'의 fallback로 전달할 컴포넌트 정의하기
+
+### ErrorBoundaryFallbackProps
+
+``'의 fallback으로 컴포넌트를 전달하고 싶다면 `ErrorBoundaryFallbackProps` 타입을 활용해 쉽게 컴포넌트를 선언 할 수 있습니다.
+
+```tsx
+import type { ErrorBoundaryFallbackProps } from '@suspensive/react'
+
+const ErrorBoundaryFallback = ({ reset, error }: ErrorBoundaryFallbackProps) => (
+ <>
+
+ {error.message}
+ >
+)
+
+const Example = () => (
+
+
+
+)
+```
+
+:::
+
+:::tip
+
+## `` fallback props을 prop drilling 없이 사용하기
+
+### useErrorBoundaryFallbackProps
+
+`error` 객체와 `reset` 메소드을 사용하려는 컴포넌트가 중첩되면 prop drilling을 피할 수 없습니다.
+이 때, `useErrorBoundaryFallbackProps`을 통해, prop drilling 없이 `reset` 메소드와 `error` 객체에 접근할 수 있습니다.
+
+```tsx
+import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react'
+
+const Nested = () => {
+ const { reset, error } = useErrorBoundaryFallbackProps()
+
+ return (
+ <>
+
+ {error.message}
+ >
+ )
+}
+
+// 여기서 fallbackProp 을 전달할 필요가 없어집니다!
+const ErrorBoundaryFallback = () =>
+
+const Example = () => (
+
+
+
+)
+```
+
+:::
+
### props.resetKeys
-``의 fallback 외부에 있는 컴포넌트가 ``를 reset하려면 resetKeys배열에 resetKey를 할당하면 됩니다. resetKeys는 배열의 하나 이상의 요소가 변경된 경우에만 작동합니다. useEffect의 종속성 배열이 작동하는 방식과 같이 resetKeys로 매 렌더링마다 새 배열을 주입하는 것을 걱정할 필요도 없습니다.
+``의 fallback 외부에 있는 컴포넌트가 ``를 reset하려면 resetKeys 배열에 resetKey를 할당하면 됩니다. resetKeys는 배열의 하나 이상의 요소가 변경된 경우에만 작동합니다. useEffect의 종속성 배열이 작동하는 방식과 같이 resetKeys로 매 렌더링마다 새 배열을 주입하는 것을 걱정할 필요도 없습니다.
```tsx
import { ErrorBoundary } from '@suspensive/react'
@@ -123,25 +185,9 @@ useErrorBoundary는 실험 기능이므로 이 인터페이스는 변경될 수
:::
-ErrorBoundaryFallback에서 useErrorBoundary.reset을 사용해 props 없이도 ``을 reset할 수 있습니다.
-
-```tsx
-import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
-
-const ErrorBoundaryFallback = () => {
- const errorBoundary = useErrorBoundary()
-
- return
-}
-
-const Example = () => (
-
-
-
-)
-```
+### useErrorBoundary().setError
-``의 children에서 useErrorBoundary.setError을 사용해 throw 없이도 ``에서 Error를 알도록 할 수 있습니다.
+``의 children에서 useErrorBoundary().setError을 사용해 throw 없이도 ``에서 Error를 알게 할 수 있습니다.
```tsx
import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
diff --git a/packages/react/src/ErrorBoundary.spec.tsx b/packages/react/src/ErrorBoundary.spec.tsx
index 3b051879f..657ee46c8 100644
--- a/packages/react/src/ErrorBoundary.spec.tsx
+++ b/packages/react/src/ErrorBoundary.spec.tsx
@@ -1,12 +1,12 @@
import { act, render } from '@testing-library/react'
import type { ComponentProps, ComponentRef } from 'react'
-import { createElement, createRef, useEffect } from 'react'
+import { createElement, createRef } from 'react'
import { createRoot } from 'react-dom/client'
import { vi } from 'vitest'
import { useSetTimeout } from './hooks'
import { assert } from './utils'
import { ERROR_MESSAGE, FALLBACK, MS_100, TEXT, ThrowError, ThrowNull } from './utils/toTest'
-import { ErrorBoundary, useErrorBoundary, withErrorBoundary } from '.'
+import { ErrorBoundary, useErrorBoundary, useErrorBoundaryFallbackProps, withErrorBoundary } from '.'
let container = document.createElement('div')
let root = createRoot(container)
@@ -250,22 +250,79 @@ describe('useErrorBoundary', () => {
)
)
- it('should supply reset function to reset in fallback of ErrorBoundary', () => {
- const onReset = vi.fn()
+ it('should supply setError to set Error of ErrorBoundary manually', () => {
+ const onError = vi.fn()
vi.useFakeTimers()
renderErrorBoundary({
- onReset,
- fallback: function ErrorBoundaryFallback({ error }) {
+ onError,
+ fallback: function ErrorBoundaryFallback() {
+ const props = useErrorBoundaryFallbackProps()
+ useSetTimeout(props.reset, MS_100)
+ return <>{props.error.message}>
+ },
+ children: createElement(() => {
const errorBoundary = useErrorBoundary()
+ useSetTimeout(() => errorBoundary.setError(new Error(ERROR_MESSAGE)), MS_100)
+ return <>{TEXT}>
+ }),
+ })
+ expect(container.textContent).toBe(TEXT)
+ expect(container.textContent).not.toBe(ERROR_MESSAGE)
+ expect(onError).toHaveBeenCalledTimes(0)
+ act(() => vi.advanceTimersByTime(MS_100))
+ expect(container.textContent).toBe(ERROR_MESSAGE)
+ expect(container.textContent).not.toBe(TEXT)
+ expect(onError).toHaveBeenCalledTimes(1)
+ })
- useEffect(() => {
- const timeoutId = setTimeout(() => {
- errorBoundary.reset()
- }, MS_100)
- return () => clearTimeout(timeoutId)
- })
+ it('should guarantee hook calling position is in children of ErrorBoundary', () => {
+ expect(
+ render(
+
+ {createElement(() => {
+ useErrorBoundary()
+ return <>{TEXT}>
+ })}
+
+ ).getByText(TEXT)
+ ).toBeInTheDocument()
- return <>{error.message}>
+ expect(() =>
+ render(
+ {
+ useErrorBoundary()
+ return <>>
+ }}
+ >
+
+
+ )
+ ).toThrow(assert.message.useErrorBoundary.onlyInChildrenOfErrorBoundary)
+ })
+})
+
+describe('useErrorBoundaryFallbackProps', () => {
+ beforeEach(() => {
+ container = document.createElement('div')
+ root = createRoot(container)
+ ThrowError.reset()
+ })
+ const renderErrorBoundary = (props: Partial>) =>
+ act(() =>
+ root.render( <>{props.error.message}>} {...props} />)
+ )
+
+ it('should supply reset function and error to reset in fallback of ErrorBoundary', () => {
+ const onReset = vi.fn()
+ vi.useFakeTimers()
+ renderErrorBoundary({
+ onReset,
+ fallback: function ErrorBoundaryFallback() {
+ const props = useErrorBoundaryFallbackProps()
+ useSetTimeout(props.reset, MS_100)
+
+ return <>{props.error.message}>
},
children: (
@@ -286,49 +343,40 @@ describe('useErrorBoundary', () => {
expect(onReset).toHaveBeenCalledTimes(1)
})
- it('should supply setError to set Error of ErrorBoundary manually', () => {
- const onError = vi.fn()
- vi.useFakeTimers()
- renderErrorBoundary({
- onError,
- fallback: function ErrorBoundaryFallback({ error }) {
- const errorBoundary = useErrorBoundary()
- useSetTimeout(errorBoundary.reset, MS_100)
- return <>{error.message}>
- },
- children: createElement(() => {
- const errorBoundary = useErrorBoundary()
- useSetTimeout(() => errorBoundary.setError(new Error(ERROR_MESSAGE)), MS_100)
- return <>{TEXT}>
- }),
- })
- expect(container.textContent).toBe(TEXT)
- expect(container.textContent).not.toBe(ERROR_MESSAGE)
- expect(onError).toHaveBeenCalledTimes(0)
- act(() => vi.advanceTimersByTime(MS_100))
- expect(container.textContent).toBe(ERROR_MESSAGE)
- expect(container.textContent).not.toBe(TEXT)
- expect(onError).toHaveBeenCalledTimes(1)
+ it('should guarantee hook calling position is in fallback of ErrorBoundary', () => {
+ expect(
+ render(
+ <>{props.error.message}>}>
+ {createElement(() => {
+ useErrorBoundaryFallbackProps()
+ return <>{TEXT}>
+ })}
+
+ ).getByText(assert.message.useErrorBoundaryFallbackProps.onlyInFallbackOfErrorBoundary)
+ ).toBeInTheDocument()
})
- it('should guarantee ErrorBoundary in parent', () => {
- const rendered = render(
-
- {createElement(() => {
- useErrorBoundary()
- return <>{TEXT}>
- })}
-
- )
- expect(rendered.getByText(TEXT)).toBeInTheDocument()
-
+ it('should be prevented to be called outside fallback of ErrorBoundary', () => {
expect(() =>
render(
createElement(() => {
- useErrorBoundary()
+ useErrorBoundaryFallbackProps()
return <>{TEXT}>
})
)
- ).toThrow(assert.message.useErrorBoundary.onlyInChildrenOfErrorBoundary)
+ ).toThrow(assert.message.useErrorBoundaryFallbackProps.onlyInFallbackOfErrorBoundary)
+ })
+
+ it('should be prevented to be called in children of ErrorBoundary', () => {
+ expect(
+ render(
+ <>{props.error.message}>}>
+ {createElement(() => {
+ useErrorBoundaryFallbackProps()
+ return <>{TEXT}>
+ })}
+
+ ).getByText(assert.message.useErrorBoundaryFallbackProps.onlyInFallbackOfErrorBoundary)
+ ).toBeInTheDocument()
})
})
diff --git a/packages/react/src/ErrorBoundary.tsx b/packages/react/src/ErrorBoundary.tsx
index 24b5e5f58..26a5aff8f 100644
--- a/packages/react/src/ErrorBoundary.tsx
+++ b/packages/react/src/ErrorBoundary.tsx
@@ -14,11 +14,11 @@ import { ErrorBoundaryGroupContext } from './ErrorBoundaryGroup'
import type { PropsWithoutChildren } from './types'
import { assert, hasResetKeysChanged } from './utils'
-export type ErrorBoundaryFallbackProps = {
+export type ErrorBoundaryFallbackProps = {
/**
* when ErrorBoundary catch error, you can use this error
*/
- error: Error
+ error: TError
/**
* when you want to reset caught error, you can use this reset
*/
@@ -54,7 +54,7 @@ type ErrorBoundaryState =
error: null
}
-const initialState: ErrorBoundaryState = {
+const initialErrorBoundaryState: ErrorBoundaryState = {
isError: false,
error: null,
}
@@ -63,7 +63,7 @@ class BaseErrorBoundary extends Component {
this.props.onReset?.()
- this.setState(initialState)
+ this.setState(initialErrorBoundaryState)
}
render() {
@@ -89,14 +89,14 @@ class BaseErrorBoundary extends Component
{this.state.isError
? typeof fallback === 'function'
? createElement(fallback, {
error: this.state.error,
- reset: this.reset.bind(this),
+ reset: this.reset,
})
: fallback
: children}
@@ -124,7 +124,6 @@ if (process.env.NODE_ENV !== 'production') {
ErrorBoundary.displayName = 'ErrorBoundary'
}
-// HOC
export const withErrorBoundary = = Record>(
Component: ComponentType,
errorBoundaryProps: PropsWithoutChildren
@@ -143,11 +142,7 @@ export const withErrorBoundary = =
return Wrapped
}
-type ErrorBoundaryContextType = {
- reset: () => void
-} & ErrorBoundaryState
-
-const ErrorBoundaryContext = createContext(null)
+const ErrorBoundaryContext = createContext<({ reset: () => void } & ErrorBoundaryState) | null>(null)
/**
* @experimental This is experimental feature.
@@ -157,19 +152,33 @@ export const useErrorBoundary = () => {
isError: false,
error: null,
})
-
if (state.isError) {
throw state.error
}
- const errorBoundaryContext = useContext(ErrorBoundaryContext)
- assert(errorBoundaryContext != null, assert.message.useErrorBoundary.onlyInChildrenOfErrorBoundary)
+ const errorBoundary = useContext(ErrorBoundaryContext)
+ assert(errorBoundary != null && !errorBoundary.isError, assert.message.useErrorBoundary.onlyInChildrenOfErrorBoundary)
return useMemo(
() => ({
- ...errorBoundaryContext,
setError: (error: TError) => setState({ isError: true, error }),
}),
- [errorBoundaryContext]
+ []
+ )
+}
+
+export const useErrorBoundaryFallbackProps = (): ErrorBoundaryFallbackProps => {
+ const errorBoundary = useContext(ErrorBoundaryContext)
+ assert(
+ errorBoundary != null && errorBoundary.isError,
+ assert.message.useErrorBoundaryFallbackProps.onlyInFallbackOfErrorBoundary
+ )
+
+ return useMemo(
+ () => ({
+ error: errorBoundary.error as TError,
+ reset: errorBoundary.reset,
+ }),
+ [errorBoundary.error, errorBoundary.reset]
)
}
diff --git a/packages/react/src/ErrorBoundaryGroup.spec.tsx b/packages/react/src/ErrorBoundaryGroup.spec.tsx
index 20632238f..18e0e63b6 100644
--- a/packages/react/src/ErrorBoundaryGroup.spec.tsx
+++ b/packages/react/src/ErrorBoundaryGroup.spec.tsx
@@ -68,13 +68,13 @@ describe('', () => {
})
})
-const UsingUseErrorBoundary = () => {
+const UsingUseErrorBoundaryGroup = () => {
useErrorBoundaryGroup()
return <>{TEXT}>
}
describe('useErrorBoundaryGroup', () => {
it('should throw error without ErrorBoundaryGroup in parent', () => {
- expect(() => render()).toThrow(
+ expect(() => render()).toThrow(
assert.message.useErrorBoundaryGroup.onlyInChildrenOfErrorBoundaryGroup
)
})
@@ -82,7 +82,7 @@ describe('useErrorBoundaryGroup', () => {
describe('withErrorBoundaryGroup', () => {
it('should wrap component. we can check by useErrorBoundaryGroup', () => {
- const rendered = render(createElement(withErrorBoundaryGroup(UsingUseErrorBoundary)))
+ const rendered = render(createElement(withErrorBoundaryGroup(UsingUseErrorBoundaryGroup)))
expect(rendered.queryByText(TEXT)).toBeInTheDocument()
})
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 80e14467f..15b11505d 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -1,6 +1,6 @@
export { SuspensiveProvider, Suspensive } from './SuspensiveProvider'
export { Suspense, withSuspense } from './Suspense'
-export { ErrorBoundary, withErrorBoundary, useErrorBoundary } from './ErrorBoundary'
+export { ErrorBoundary, withErrorBoundary, useErrorBoundary, useErrorBoundaryFallbackProps } from './ErrorBoundary'
export { ErrorBoundaryGroup, withErrorBoundaryGroup, useErrorBoundaryGroup } from './ErrorBoundaryGroup'
export { AsyncBoundary, withAsyncBoundary } from './AsyncBoundary'
export { Delay, withDelay } from './Delay'
diff --git a/packages/react/src/utils/assert.spec.ts b/packages/react/src/utils/assert.spec.ts
index 350f02b57..f3e0c3fbc 100644
--- a/packages/react/src/utils/assert.spec.ts
+++ b/packages/react/src/utils/assert.spec.ts
@@ -29,7 +29,8 @@ describe('assert', () => {
})
it('should throw error if given condition is not met', () => {
+ const assertMessage = "value should be 'paz'"
const value = 'baz' as string
- expect(() => assert(value === 'paz', "value should be 'paz'")).toThrow("value should be 'paz'")
+ expect(() => assert(value === 'paz', assertMessage)).toThrow(assertMessage)
})
})
diff --git a/packages/react/src/utils/assert.ts b/packages/react/src/utils/assert.ts
index 823f639b2..ffeecf47e 100644
--- a/packages/react/src/utils/assert.ts
+++ b/packages/react/src/utils/assert.ts
@@ -8,6 +8,10 @@ assert.message = {
useErrorBoundary: {
onlyInChildrenOfErrorBoundary: 'useErrorBoundary: this hook should be called in ErrorBoundary.props.children',
},
+ useErrorBoundaryFallbackProps: {
+ onlyInFallbackOfErrorBoundary:
+ 'useErrorBoundaryFallbackProps: this hook should be called in ErrorBoundary.props.fallback',
+ },
useErrorBoundaryGroup: {
onlyInChildrenOfErrorBoundaryGroup:
'useErrorBoundaryGroup: this hook should be called in ErrorBoundaryGroup.props.children',
diff --git a/websites/visualization/src/app/react/experimental/useErrorBoundary/page.tsx b/websites/visualization/src/app/react/experimental/useErrorBoundary/page.tsx
index cd8e71b61..25b13f3fb 100644
--- a/websites/visualization/src/app/react/experimental/useErrorBoundary/page.tsx
+++ b/websites/visualization/src/app/react/experimental/useErrorBoundary/page.tsx
@@ -1,6 +1,6 @@
'use client'
-import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
+import { ErrorBoundary, useErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react'
import type { PropsWithChildren } from 'react'
import { createElement, useEffect, useState } from 'react'
@@ -8,9 +8,9 @@ export default function Page() {
return (
errorBoundary.reset()}>reset: {error.message}
+ return
}}
>
{createElement(function ErrorComponent() {