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): prevent recursive exposing fallback when fallback throw error #1409

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/pretty-brooms-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react": minor
---

feat(react): prevent recursive expose fallback when fallback throw error
22 changes: 22 additions & 0 deletions packages/react/src/ErrorBoundary.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,28 @@ describe('<ErrorBoundary/>', () => {
await waitFor(() => expect(screen.queryByText(errorText)).toBeInTheDocument())
}
)

it('should re-throw error occurred by fallback', async () => {
render(
<ErrorBoundary fallback={() => <>This is expected</>}>
<ErrorBoundary
fallback={() => (
<Throw.Error message={ERROR_MESSAGE} after={100}>
ErrorBoundary's fallback before error
</Throw.Error>
)}
>
<Throw.Error message={ERROR_MESSAGE} after={100}>
ErrorBoundary's children before error
</Throw.Error>
</ErrorBoundary>
</ErrorBoundary>
)

expect(screen.queryByText("ErrorBoundary's children before error")).toBeInTheDocument()
await waitFor(() => expect(screen.queryByText("ErrorBoundary's fallback before error")).toBeInTheDocument())
await waitFor(() => expect(screen.queryByText('This is expected')).toBeInTheDocument())
})
})

describe('<ErrorBoundary.Consumer/>', () => {
Expand Down
31 changes: 25 additions & 6 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState
}

componentDidCatch(error: Error, info: ErrorInfo) {
if (error instanceof ErrorInFallback) {
throw error.originalError
}
if (error instanceof SuspensiveError) {
throw error
}
Expand Down Expand Up @@ -147,12 +150,12 @@ class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState
throw error
}

if (typeof fallback === 'function') {
const FallbackComponent = fallback
childrenOrFallback = <FallbackComponent error={error} reset={this.reset} />
} else {
childrenOrFallback = fallback
}
const Fallback = fallback
childrenOrFallback = (
<FallbackBoundary>
{typeof Fallback === 'function' ? <Fallback error={error} reset={this.reset} /> : Fallback}
</FallbackBoundary>
)
}

return (
Expand All @@ -163,6 +166,22 @@ class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState
}
}

class ErrorInFallback extends Error {
originalError: Error
constructor(originalError: Error) {
super()
this.originalError = originalError
}
}
class FallbackBoundary extends Component<{ children: ReactNode }> {
componentDidCatch(originalError: Error) {
throw originalError instanceof SuspensiveError ? originalError : new ErrorInFallback(originalError)
}
render() {
return this.props.children
}
}

/**
* This component provides a simple and reusable wrapper that you can use to wrap around your components. Any rendering errors in your components hierarchy can then be gracefully handled.
* @see {@link https://suspensive.org/docs/react/ErrorBoundary Suspensive Docs}
Expand Down
Loading