Skip to content

Commit

Permalink
fix(react): fix throw null (#151)
Browse files Browse the repository at this point in the history
* fix(react): fix throw null

* Create grumpy-turtles-tickle.md

* test(react): add test case for ErrorBoundary catching null thrown

* chore(react): update changeset

* Update packages/react/src/utils/toTest.tsx

Co-authored-by: tommy(Chung-il) <[email protected]>

* chore: update

---------

Co-authored-by: tommy(Chung-il) <[email protected]>
  • Loading branch information
manudeli and tooooo1 authored Sep 18, 2023
1 parent 8d8ef53 commit d636e85
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-turtles-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react": patch
---

fix(react): ErrorBoundary should catch null thrown by children
19 changes: 18 additions & 1 deletion packages/react/src/ErrorBoundary.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { act } from '@testing-library/react'
import { ComponentProps, ComponentRef, createRef } from 'react'
import { createRoot } from 'react-dom/client'
import { ERROR_MESSAGE, FALLBACK, MS_100, TEXT, ThrowError } from './utils/toTest'
import { ERROR_MESSAGE, FALLBACK, MS_100, TEXT, ThrowError, ThrowNull } from './utils/toTest'
import { ErrorBoundary } from '.'

let container = document.createElement('div')
Expand Down Expand Up @@ -60,6 +60,23 @@ describe('ErrorBoundary', () => {
expect(container.textContent).not.toBe(TEXT)
})

it('should catch it even if thrown null', () => {
const onError = vi.fn()
vi.useFakeTimers()
renderErrorBoundary({
onError,
fallback: <>{FALLBACK}</>,
children: <ThrowNull after={MS_100}>{TEXT}</ThrowNull>,
})
expect(container.textContent).toBe(TEXT)
expect(container.textContent).not.toBe(FALLBACK)
expect(onError).toHaveBeenCalledTimes(0)
act(() => vi.advanceTimersByTime(MS_100))
expect(container.textContent).toBe(FALLBACK)
expect(container.textContent).not.toBe(TEXT)
expect(onError).toHaveBeenCalledTimes(1)
})

it('should be reset by items of resetKeys, and call onReset', () => {
const onReset = vi.fn()
vi.useFakeTimers()
Expand Down
26 changes: 16 additions & 10 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,32 @@ type ErrorBoundaryProps = PropsWithChildren<{
fallback: NonNullable<ReactNode> | FunctionComponent<ErrorBoundaryFallbackProps>
}>

type ErrorBoundaryState = {
error: Error | null
}
type ErrorBoundaryState<TError extends Error = Error> =
| {
isError: true
error: TError
}
| {
isError: false
error: null
}

const initialState: ErrorBoundaryState = {
isError: false,
error: null,
}

class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
static getDerivedStateFromError(error: Error) {
return { error }
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { isError: true, error }
}

state = initialState

componentDidUpdate(prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState) {
const { error } = this.state
const { isError } = this.state
const { resetKeys } = this.props

if (error !== null && prevState.error !== null && hasResetKeysChanged(prevProps.resetKeys, resetKeys)) {
if (isError && prevState.isError && hasResetKeysChanged(prevProps.resetKeys, resetKeys)) {
this.resetErrorBoundary()
}
}
Expand All @@ -86,10 +92,10 @@ class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState
}

render() {
const { error } = this.state
const { isError, error } = this.state
const { children, fallback } = this.props

if (error !== null) {
if (isError) {
if (typeof fallback === 'function') {
return createElement(fallback, {
error,
Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/utils/toTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export const ThrowError = ({ message, after, children }: ThrowErrorProps) => {
}, [after])
return <>{children}</>
}

type ThrowNullProps = PropsWithChildren<{ after: number }>
export const ThrowNull = ({ after, children }: ThrowNullProps) => {
const [isNeedError, setIsNeedError] = useState(throwErrorIsNeed.current)
if (isNeedError) {
throw null
}
useEffect(() => {
const timerId = setTimeout(() => setIsNeedError(true), after)
return () => clearTimeout(timerId)
}, [after])
return <>{children}</>
}

ThrowError.reset = () => {
throwErrorIsNeed.current = false
}
Expand Down

1 comment on commit d636e85

@vercel
Copy link

@vercel vercel bot commented on d636e85 Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

visualization – ./websites/visualization

visualization-git-main-suspensive.vercel.app
visualization.suspensive.org
visualization-suspensive.vercel.app

Please sign in to comment.