Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): add useErrorBoundaryFallbackProps #216

Merged
merged 16 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions .changeset/two-weeks-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react": minor
---

feat(react): add useErrorBoundaryFallbackProps
52 changes: 34 additions & 18 deletions packages/react/src/ErrorBoundary.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,38 @@ const ErrorAfter4s = () => {

![Example banner](/gif/errorboundary-example.gif)

:::tip

## Use `<ErrorBoundary/>` fallback props without prop drilling

### useErrorBoundaryFallbackProps
manudeli marked this conversation as resolved.
Show resolved Hide resolved

In fallback of `<ErrorBoundary/>`, by calling useErrorBoundaryFallbackProps().reset/error, we can use error of `<ErrorBoundary/>` and reset it without using prop directly

```tsx
import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react'

const Nested = () => {
const props = useErrorBoundaryFallbackProps()

return (
<>
<button onClick={props.reset}>Try again</button>
{props.error.message}
</>
)
}
manudeli marked this conversation as resolved.
Show resolved Hide resolved

const ErrorBoundaryFallback = () => <Nested />
const Example = () => (
<ErrorBoundary fallback={ErrorBoundaryFallback}>
minsoo-web marked this conversation as resolved.
Show resolved Hide resolved
manudeli marked this conversation as resolved.
Show resolved Hide resolved
<Error />
</ErrorBoundary>
)
```

:::

### props.resetKeys

If you want to reset `<ErrorBoundary/>` by component where is outside of `<ErrorBoundary/>`'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.
Expand Down Expand Up @@ -123,25 +155,9 @@ useErrorBoundary is experimental feature, this interfaces could be changed

:::

We can reset `<ErrorBoundary/>` without props using useErrorBoundary.reset in ErrorBoundaryFallback.

```tsx
import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'

const ErrorBoundaryFallback = () => {
const errorBoundary = useErrorBoundary()

return <button onClick={errorBoundary.reset} />
}

const Example = () => (
<ErrorBoundary fallback={ErrorBoundaryFallback}>
<Error />
</ErrorBoundary>
)
```
### useErrorBoundary().setError

In children of `<ErrorBoundary/>`, we can use useErrorBoundary.setError to make `<ErrorBoundary/>` aware of the Error without throw.
In children of `<ErrorBoundary/>`, we can use useErrorBoundary().setError to make `<ErrorBoundary/>` aware of the Error without throw.

```tsx
import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
Expand Down
54 changes: 35 additions & 19 deletions packages/react/src/ErrorBoundary.ko.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,41 @@ const ErrorAfter4s = () => {

![Example banner](/gif/errorboundary-example.gif)

:::tip

## `<ErrorBoundary/>` fallback props을 prop drilling 없이 사용하기
manudeli marked this conversation as resolved.
Show resolved Hide resolved

### useErrorBoundaryFallbackProps

ErrorBoundaryFallback에서 useErrorBoundaryFallbackProps().reset/error을 사용해 prop을 직접 사용하지 않고도 `<ErrorBoundary/>`을 reset하고 error를 사용할 수 있습니다.

```tsx
import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react'

const Nested = () => {
const props = useErrorBoundaryFallbackProps()

return (
<>
<button onClick={props.reset}>Try again</button>
{props.error.message}
</>
)
}
manudeli marked this conversation as resolved.
Show resolved Hide resolved

const ErrorBoundaryFallback = () => <Nested />
const Example = () => (
<ErrorBoundary fallback={ErrorBoundaryFallback}>
<Error />
</ErrorBoundary>
)
```

:::

### props.resetKeys

`<ErrorBoundary/>`의 fallback 외부에 있는 컴포넌트가 `<ErrorBoundary/>`를 reset하려면 resetKeys배열에 resetKey를 할당하면 됩니다. resetKeys는 배열의 하나 이상의 요소가 변경된 경우에만 작동합니다. useEffect의 종속성 배열이 작동하는 방식과 같이 resetKeys로 매 렌더링마다 새 배열을 주입하는 것을 걱정할 필요도 없습니다.
`<ErrorBoundary/>`의 fallback 외부에 있는 컴포넌트가 `<ErrorBoundary/>`를 reset하려면 resetKeys 배열에 resetKey를 할당하면 됩니다. resetKeys는 배열의 하나 이상의 요소가 변경된 경우에만 작동합니다. useEffect의 종속성 배열이 작동하는 방식과 같이 resetKeys로 매 렌더링마다 새 배열을 주입하는 것을 걱정할 필요도 없습니다.

```tsx
import { ErrorBoundary } from '@suspensive/react'
Expand Down Expand Up @@ -123,25 +155,9 @@ useErrorBoundary는 실험 기능이므로 이 인터페이스는 변경될 수

:::

ErrorBoundaryFallback에서 useErrorBoundary.reset을 사용해 props 없이도 `<ErrorBoundary/>`을 reset할 수 있습니다.

```tsx
import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'

const ErrorBoundaryFallback = () => {
const errorBoundary = useErrorBoundary()

return <button onClick={errorBoundary.reset} />
}

const Example = () => (
<ErrorBoundary fallback={ErrorBoundaryFallback}>
<Error />
</ErrorBoundary>
)
```
### useErrorBoundary().setError

`<ErrorBoundary/>`의 children에서 useErrorBoundary.setError을 사용해 throw 없이도 `<ErrorBoundary/>`에서 Error를 알도록 할 수 있습니다.
`<ErrorBoundary/>`의 children에서 useErrorBoundary().setError을 사용해 throw 없이도 `<ErrorBoundary/>`에서 Error를 알게 할 수 있습니다.

```tsx
import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
Expand Down
146 changes: 97 additions & 49 deletions packages/react/src/ErrorBoundary.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { act, render } from '@testing-library/react'
import { ComponentProps, ComponentRef, createElement, createRef, useEffect } from 'react'
import { ComponentProps, ComponentRef, 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)
Expand Down Expand Up @@ -249,22 +249,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(
<ErrorBoundary fallback={ERROR_MESSAGE}>
{createElement(() => {
useErrorBoundary()
return <>{TEXT}</>
})}
</ErrorBoundary>
).getByText(TEXT)
).toBeInTheDocument()

return <>{error.message}</>
expect(() =>
render(
<ErrorBoundary
fallback={() => {
useErrorBoundary()
return <></>
}}
>
<ThrowError message={ERROR_MESSAGE} after={0} />
</ErrorBoundary>
)
).toThrow(assert.message.useErrorBoundary.onlyInChildrenOfErrorBoundary)
})
})

describe('useErrorBoundaryFallbackProps', () => {
beforeEach(() => {
container = document.createElement('div')
root = createRoot(container)
ThrowError.reset()
})
const renderErrorBoundary = (props: Partial<ComponentProps<typeof ErrorBoundary>>) =>
act(() =>
root.render(<ErrorBoundary ref={errorBoundaryRef} fallback={(props) => <>{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: (
<ThrowError message={ERROR_MESSAGE} after={MS_100}>
Expand All @@ -285,49 +342,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(
<ErrorBoundary fallback={(props) => <>{props.error.message}</>}>
{createElement(() => {
useErrorBoundaryFallbackProps()
return <>{TEXT}</>
})}
</ErrorBoundary>
).getByText(assert.message.useErrorBoundaryFallbackProps.onlyInFallbackOfErrorBoundary)
).toBeInTheDocument()
})

it('should guarantee ErrorBoundary in parent', () => {
const rendered = render(
<ErrorBoundary fallback={ERROR_MESSAGE}>
{createElement(() => {
useErrorBoundary()
return <>{TEXT}</>
})}
</ErrorBoundary>
)
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(
<ErrorBoundary fallback={(props) => <>{props.error.message}</>}>
{createElement(() => {
useErrorBoundaryFallbackProps()
return <>{TEXT}</>
})}
</ErrorBoundary>
).getByText(assert.message.useErrorBoundaryFallbackProps.onlyInFallbackOfErrorBoundary)
).toBeInTheDocument()
})
})
Loading
Loading