Skip to content

Commit

Permalink
feat(react): add ErrorBoundary.Consumer, ErrorBoundaryGroup.Consumer (#…
Browse files Browse the repository at this point in the history
…610)

fix #502 

# Overview

<!--
    A clear and concise description of what this pr is about.
 -->

I mark ErrorBoundary.Reset as deprecated
and added 2 new apis ErrorBoundary.Consumer, ErrorBoundaryGroup.Consumer

I made it like Provider, Consumer

## 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.
  • Loading branch information
manudeli authored Jan 22, 2024
1 parent 7ce3bd9 commit 4db9027
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 101 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-ladybugs-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react": patch
---

feat(react): add ErrorBoundary.Consumer, ErrorBoundaryGroup.Consumer
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ import { Suspense, ErrorBoundary, ErrorBoundaryGroup, Delay } from '@suspensive/

const Example = () => (
<ErrorBoundaryGroup>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Reset All</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <Button onClick={group.reset}>Reset All</Button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary
fallback={(props) => (
<>
Expand Down
22 changes: 15 additions & 7 deletions docs/suspensive.org/src/pages/docs/react/ErrorBoundaryGroup.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ import { Callout } from '@/components'

This is a component for managing multiple `<ErrorBoundary/>`s easily.

`<ErrorBoundary/>`s as children of nested `<ErrorBoundaryGroup/>` will also be reset by parent `<ErrorBoundaryGroup.Reset/>`.
`<ErrorBoundary/>`s as children of nested `<ErrorBoundaryGroup/>` will also be reset by parent `<ErrorBoundaryGroup.Consumer/>`.

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

const Example = () => (
<ErrorBoundaryGroup>
{/* Resets all ErrorBoundaries that are children of ErrorBoundaryGroup. All ErrorBoundaries within nested ErrorBoundaryGroups are also reset. */}
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
<ErrorBoundaryGroup>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
Expand All @@ -30,18 +34,22 @@ const Example = () => (

If you want to block resetting nested `<ErrorBoundaryGroup/>` by parent `<ErrorBoundaryGroup/>`, Use blockOutside.

```tsx /blockOutside/
```jsx /blockOutside/
import { ErrorBoundaryGroup, ErrorBoundary } from '@suspensive/react'

const Example = () => (
<ErrorBoundaryGroup>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
{/* blockOutside prop prevents reset by the parent ErrorBoundaryGroup*/}
<ErrorBoundaryGroup blockOutside>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
Expand Down
22 changes: 15 additions & 7 deletions docs/suspensive.org/src/pages/docs/react/ErrorBoundaryGroup.ko.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ import { Callout } from '@/components'

이 컴포넌트로 children인 `<ErrorBoundary/>`들을 쉽게 관리할 수 있습니다.

중첩된 `<ErrorBoundaryGroup/>`의 children인 `<ErrorBoundary/>`들도 상위 `<ErrorBoundaryGroup.Reset/>`으로도 reset됩니다.
중첩된 `<ErrorBoundaryGroup/>`의 children인 `<ErrorBoundary/>`들도 상위 `<ErrorBoundaryGroup.Consumer/>`으로도 reset됩니다.

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

const Example = () => (
<ErrorBoundaryGroup>
{/* ErrorBoundaryGroup의 children인 ErrorBoundary를 모두 reset합니다. 중첩된 ErrorBoundaryGroup내의 ErrorBoundary도 모두 reset합니다. */}
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
<ErrorBoundaryGroup>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
Expand All @@ -30,18 +34,22 @@ const Example = () => (

만약 상위 `<ErrorBoundaryGroup/>`에 의한 하위 `<ErrorBoundaryGroup/>`의 reset을 막고 싶다면 blockOutside을 사용하면 됩니다.

```tsx /blockOutside/
```jsx /blockOutside/
import { ErrorBoundaryGroup, ErrorBoundary } from '@suspensive/react'

const Example = () => (
<ErrorBoundaryGroup>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
{/* blockOutside prop으로 상위 ErrorBoundaryGroup에 의한 reset을 막습니다 */}
<ErrorBoundaryGroup blockOutside>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>Try again</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Try again</button>}
</ErrorBoundaryGroup.Consumer>
<ErrorBoundary fallback={(props) => <>{props.error}</>}>
<CanThrowError />
</ErrorBoundary>
Expand Down
33 changes: 16 additions & 17 deletions docs/suspensive.org/src/pages/docs/react/wrap.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

wrap was created to wrap components with Suspense, ErrorBoudnary, ErrorBoundaryGroup, etc. provided by @suspensive/react.

```tsx /wrap/
import { wrap } from '@suspensive/react'
```jsx /wrap/
import { wrap, ErrorBoundaryGroup } from '@suspensive/react'
import { useSuspenseQuery } from '@suspensive/react-query'

export const Page = wrap
Expand All @@ -13,15 +13,16 @@ export const Page = wrap
.on(
// will make <Page/> component wrapped in <ErrorBoundaryGroup/>, <ErrorBoundary/> and <Suspense/>
() => {
const errorBoundaryGroup = useErrorBoundaryGroup()
const { data: postList } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: () => fetch(`https://exmaple.com/posts`).then((res) => res.json()),
})

return (
<>
<button onClick={errorBoundaryGroup.reset}>Reset all posts</button>
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Reset all posts</button>}
</ErrorBoundaryGroup.Consumer>
{postList.map((post) => (
<PostItem id={post.id} />
))}
Expand All @@ -30,18 +31,16 @@ export const Page = wrap
}
)

const PostItem = wrap
.ErrorBoundary({ fallback: ({ error }) => <>{error.message}</> })
.Suspense({ fallback: <PostSkeleton /> })
.on<{ id: string }>(
// will make <Post/> component have PostProps wrapped in <ErrorBoundary/> and <Suspense/>
(props) => {
const { data: post } = useSuspenseQuery({
queryKey: ['posts', props.id],
queryFn: () => fetch(`https://exmaple.com/posts/${props.id}`).then((res) => res.json()),
})
const PostItem =
wrap.ErrorBoundary({ fallback: ({ error }) => <>{error.message}</> }).Suspense({ fallback: <PostSkeleton /> }).on <
{ id: string } >
// will make <Post/> component have PostProps wrapped in <ErrorBoundary/> and <Suspense/>
((props) => {
const { data: post } = useSuspenseQuery({
queryKey: ['posts', props.id],
queryFn: () => fetch(`https://exmaple.com/posts/${props.id}`).then((res) => res.json()),
})

return <>{post.title}</>
}
)
return <>{post.title}</>
})
```
33 changes: 16 additions & 17 deletions docs/suspensive.org/src/pages/docs/react/wrap.ko.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

wrap은 @suspensive/react에서 제공하는 Suspense, ErrorBoudnary, ErrorBoundaryGroup 등으로 컴포넌트를 감싸기 위해 만들어졌습니다.

```tsx /wrap/
import { wrap } from '@suspensive/react'
```jsx /wrap/
import { wrap, ErrorBoundaryGroup } from '@suspensive/react'
import { useSuspenseQuery } from '@suspensive/react-query'

export const Page = wrap
Expand All @@ -13,15 +13,16 @@ export const Page = wrap
.on(
// <ErrorBoundaryGroup />, <ErrorBoundary />, <Suspense />에 감싸진 <Page/> 컴포넌트를 만듭니다.
() => {
const errorBoundaryGroup = useErrorBoundaryGroup()
const { data: postList } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: () => fetch(`https://exmaple.com/posts`).then((res) => res.json()),
})

return (
<>
<button onClick={errorBoundaryGroup.reset}>Reset all posts</button>
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>Reset all posts</button>}
</ErrorBoundaryGroup.Consumer>
{postList.map((post) => (
<PostItem id={post.id} />
))}
Expand All @@ -30,18 +31,16 @@ export const Page = wrap
}
)

const PostItem = wrap
.ErrorBoundary({ fallback: ({ error }) => <>{error.message}</> })
.Suspense({ fallback: <PostSkeleton /> })
.on<{ id: string }>(
// <ErrorBoundary/>, <Suspense/>에 감싸진 <PostItem/> 컴포넌트를 만듭니다.
(props) => {
const { data: post } = useSuspenseQuery({
queryKey: ['posts', props.id],
queryFn: () => fetch(`https://exmaple.com/posts/${props.id}`).then((res) => res.json()),
})
const PostItem =
wrap.ErrorBoundary({ fallback: ({ error }) => <>{error.message}</> }).Suspense({ fallback: <PostSkeleton /> }).on <
{ id: string } >
// <ErrorBoundary/>, <Suspense/>에 감싸진 <PostItem/> 컴포넌트를 만듭니다.
((props) => {
const { data: post } = useSuspenseQuery({
queryKey: ['posts', props.id],
queryFn: () => fetch(`https://exmaple.com/posts/${props.id}`).then((res) => res.json()),
})

return <>{post.title}</>
}
)
return <>{post.title}</>
})
```
54 changes: 32 additions & 22 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,32 +144,42 @@ class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState
* This component provide 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}
*/
export const ErrorBoundary = forwardRef<{ reset(): void }, ErrorBoundaryProps>(
({ devMode, fallback, children, onError, onReset, resetKeys, ...props }, ref) => {
const group = useContext(ErrorBoundaryGroupContext) ?? { resetKey: 0 }
const baseErrorBoundaryRef = useRef<BaseErrorBoundary>(null)
useImperativeHandle(ref, () => ({
reset: () => baseErrorBoundaryRef.current?.reset(),
}))
export const ErrorBoundary = Object.assign(
(() => {
const ErrorBoundary = forwardRef<{ reset(): void }, ErrorBoundaryProps>(
({ devMode, fallback, children, onError, onReset, resetKeys, ...props }, ref) => {
const group = useContext(ErrorBoundaryGroupContext) ?? { resetKey: 0 }
const baseErrorBoundaryRef = useRef<BaseErrorBoundary>(null)
useImperativeHandle(ref, () => ({
reset: () => baseErrorBoundaryRef.current?.reset(),
}))

return (
<BaseErrorBoundary
{...props}
fallback={fallback}
onError={onError}
onReset={onReset}
resetKeys={[group.resetKey, ...(resetKeys || [])]}
ref={baseErrorBoundaryRef}
>
{children}
{process.env.NODE_ENV !== 'production' && devMode && <ErrorBoundaryDevMode {...devMode} />}
</BaseErrorBoundary>
return (
<BaseErrorBoundary
{...props}
fallback={fallback}
onError={onError}
onReset={onReset}
resetKeys={[group.resetKey, ...(resetKeys || [])]}
ref={baseErrorBoundaryRef}
>
{children}
{process.env.NODE_ENV !== 'production' && devMode && <ErrorBoundaryDevMode {...devMode} />}
</BaseErrorBoundary>
)
}
)
if (process.env.NODE_ENV !== 'production') {
ErrorBoundary.displayName = 'ErrorBoundary'
}

return ErrorBoundary
})(),
{
Consumer: ({ children }: { children: (errorBoundary: ReturnType<typeof useErrorBoundary>) => ReactNode }) =>
children(useErrorBoundary()),
}
)
if (process.env.NODE_ENV !== 'production') {
ErrorBoundary.displayName = 'ErrorBoundary'
}

const ErrorBoundaryContext = createContext<({ reset: () => void } & ErrorBoundaryState) | null>(null)
if (process.env.NODE_ENV !== 'production') {
Expand Down
8 changes: 6 additions & 2 deletions packages/react/src/ErrorBoundaryGroup.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ describe('<ErrorBoundaryGroup/>', () => {
it('should reset all ErrorBoundaries in children', async () => {
render(
<ErrorBoundaryGroup>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>{resetButtonText}</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>{resetButtonText}</button>}
</ErrorBoundaryGroup.Consumer>
{Array.from({ length: innerErrorBoundaryCount }).map((_, key) => (
<ErrorBoundary key={key} fallback={(props) => <div>{props.error.message}</div>}>
<ThrowError message={ERROR_MESSAGE} after={ms('0.1s')}>
Expand All @@ -37,7 +39,9 @@ describe('<ErrorBoundaryGroup/>', () => {
it('should reset all ErrorBoundaries in children even if it is nested, but if use blockOutside, can block reset by outside', async () => {
render(
<ErrorBoundaryGroup>
<ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>{resetButtonText}</button>} />
<ErrorBoundaryGroup.Consumer>
{(group) => <button onClick={group.reset}>{resetButtonText}</button>}
</ErrorBoundaryGroup.Consumer>
{Array.from({ length: innerErrorBoundaryCount }).map((_, index) => (
<ErrorBoundaryGroup key={index} blockOutside={index === innerErrorBoundaryCount - 1}>
<ErrorBoundary fallback={(props) => <div>{props.error.message}</div>}>
Expand Down
Loading

0 comments on commit 4db9027

Please sign in to comment.