Skip to content

Commit

Permalink
feat(react): update wrap as builder pattern (#280)
Browse files Browse the repository at this point in the history
related with #269 

# Overview

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

I update wrap as builder pattern to remove wrap nesting.

### AS-IS (wrap nesting with importing all public apis(ErrorBoundary,
Suspense, etc.))
```tsx
import { ErrorBoundary, ErrorBoundaryGroup, Suspense, useErrorBoundary, wrap } from '@suspensive/react'
import { UseSuspenseQuery } from '~/components'
import { api } from '~/utils'

const logError = (error: Error) => console.error(error)

export default wrap(ErrorBoundaryGroup, { blockOutside: false })(
  wrap(ErrorBoundary, { fallback: (props) => <>{props.error.message}</>, onError: logError })(
    wrap(Suspense.CSROnly, { fallback: <>loading...</> })(() => {
      const errorBoundary = useErrorBoundary()

      return (
        <>
          <button onClick={() => errorBoundary.setError(new Error('trigger error by useErrorBoundary().setError'))}>
            trigger error by useErrorBoundary().setError
          </button>
          <UseSuspenseQuery queryKey={['with', 1] as const} queryFn={() => api.delay(200, { percentage: 50 })} />
        </>
      )
    })
  )
)

```

### TO-BE (wrap builder with no import)

```tsx
import { useErrorBoundary, wrap } from '@suspensive/react' // It's easy

const logError = (error: Error) => console.error(error)

export default wrap
  .ErrorBoundaryGroup({ blockOutside: false })
  .ErrorBoundary({ fallback: (props) => <div>{props.error.message}</div>, onError: logError })
  .Suspense.CSROnly({ fallback: 'loading...' }) // CSROnly chaining available
  .on<{ text: string }>(({ text }) => { // We can type of Page at once. generic also available
    const errorBoundary = useErrorBoundary()

    return (
      <div>
        <button onClick={() => errorBoundary.setError(new Error('trigger error by useErrorBoundary().setError'))}>
          trigger error by useErrorBoundary().setError
        </button>
        {text}
      </div>
    )
  })

```

## 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 Nov 4, 2023
1 parent d1a04f7 commit cb23858
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 44 deletions.
4 changes: 2 additions & 2 deletions packages/react/src/AsyncBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ AsyncBoundary.CSROnly = CSROnlyAsyncBoundary
export const withAsyncBoundary = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
asyncBoundaryProps: PropsWithoutChildren<AsyncBoundaryProps>
) => wrap(AsyncBoundary, asyncBoundaryProps)(component)
) => wrap.AsyncBoundary(asyncBoundaryProps).on(component)
withAsyncBoundary.CSROnly = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
asyncBoundaryProps: PropsWithoutChildren<AsyncBoundaryProps>
) => wrap(AsyncBoundary.CSROnly, asyncBoundaryProps)(component)
) => wrap.AsyncBoundary.CSROnly(asyncBoundaryProps).on(component)
2 changes: 1 addition & 1 deletion packages/react/src/Delay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ export const DelayContext = createContext<PropsWithoutChildren<DelayProps>>({ ms
export const withDelay = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
delayProps: PropsWithoutChildren<DelayProps> = {}
) => wrap(Delay, delayProps)(component)
) => wrap.Delay(delayProps).on(component)
4 changes: 2 additions & 2 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ if (process.env.NODE_ENV !== 'production') {
}

export const withErrorBoundary = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
Component: ComponentType<TProps>,
component: ComponentType<TProps>,
errorBoundaryProps: PropsWithoutChildren<ErrorBoundaryProps>
) => wrap(ErrorBoundary, errorBoundaryProps)(Component)
) => wrap.ErrorBoundary(errorBoundaryProps).on(component)

const ErrorBoundaryContext = createContext<({ reset: () => void } & ErrorBoundaryState) | null>(null)

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/ErrorBoundaryGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@ export const useErrorBoundaryGroup = () => {
export const withErrorBoundaryGroup = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
errorBoundaryGroupProps: PropsWithoutChildren<ErrorBoundaryGroupProps> = {}
) => wrap(ErrorBoundaryGroup, errorBoundaryGroupProps)(component)
) => wrap.ErrorBoundaryGroup(errorBoundaryGroupProps).on(component)
4 changes: 2 additions & 2 deletions packages/react/src/Suspense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ Suspense.CSROnly = CSROnlySuspense
export const withSuspense = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
suspenseProps: PropsWithoutChildren<SuspenseProps> = {}
) => wrap(Suspense, suspenseProps)(component)
) => wrap.Suspense(suspenseProps).on(component)

withSuspense.CSROnly = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
suspenseProps: PropsWithoutChildren<SuspenseProps> = {}
) => wrap(Suspense.CSROnly, suspenseProps)(component)
) => wrap.Suspense.CSROnly(suspenseProps).on(component)
123 changes: 105 additions & 18 deletions packages/react/src/wrap.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,110 @@
import { type ComponentProps, type ComponentType, createElement } from 'react'
import { createElement } from 'react'
import type { ComponentProps, ComponentType } from 'react'
import type { PropsWithoutChildren } from './types'
import type { AsyncBoundaryProps, DelayProps, ErrorBoundaryGroupProps, ErrorBoundaryProps, SuspenseProps } from '.'
import { AsyncBoundary, Delay, ErrorBoundary, ErrorBoundaryGroup, Suspense } from '.'

type WrapperItem<
TWrapperComponent extends
| typeof Suspense
| typeof Suspense.CSROnly
| typeof ErrorBoundary
| typeof ErrorBoundaryGroup
| typeof AsyncBoundary
| typeof AsyncBoundary.CSROnly
| typeof Delay
> = [TWrapperComponent, PropsWithoutChildren<ComponentProps<TWrapperComponent>>]

type Wrapper =
| WrapperItem<typeof Suspense>
| WrapperItem<typeof Suspense.CSROnly>
| WrapperItem<typeof ErrorBoundary>
| WrapperItem<typeof ErrorBoundaryGroup>
| WrapperItem<typeof AsyncBoundary>
| WrapperItem<typeof AsyncBoundary.CSROnly>
| WrapperItem<typeof Delay>

class WrapWithoutCSROnly {
constructor(private wrappers: Wrapper[]) {}
Suspense = (props: PropsWithoutChildren<ComponentProps<typeof Suspense>> = {}) => {
this.wrappers.unshift([Suspense, props])
return this
}
ErrorBoundary = (props: PropsWithoutChildren<ComponentProps<typeof ErrorBoundary>>) => {
this.wrappers.unshift([ErrorBoundary, props])
return this
}
ErrorBoundaryGroup = (props: PropsWithoutChildren<ComponentProps<typeof ErrorBoundaryGroup>> = {}) => {
this.wrappers.unshift([ErrorBoundaryGroup, props])
return this
}
AsyncBoundary = (props: PropsWithoutChildren<ComponentProps<typeof AsyncBoundary>>) => {
this.wrappers.unshift([AsyncBoundary, props])
return this
}
Delay = (props: PropsWithoutChildren<ComponentProps<typeof Delay>> = {}) => {
this.wrappers.unshift([Delay, props])
return this
}

on = <TProps extends ComponentProps<ComponentType>>(component: ComponentType<TProps>) => {
const wrappedComponent = (props: TProps) =>
this.wrappers.reduce(
(acc, [wrapperComponent, wrapperProps]) => createElement(wrapperComponent as any, wrapperProps as any, acc),
createElement(component, props)
)

/**
* @experimental This is experimental feature.
*/
export const wrap =
<
TWrapperProps extends SuspenseProps | ErrorBoundaryProps | ErrorBoundaryGroupProps | DelayProps | AsyncBoundaryProps
>(
wrapper: ComponentType<TWrapperProps>,
wrapperProps: PropsWithoutChildren<TWrapperProps>
) =>
<TProps extends ComponentProps<ComponentType>>(component: ComponentType<TProps>) => {
const wrapped = (props: TProps) =>
createElement(wrapper, wrapperProps as TWrapperProps, createElement(component, props))
if (process.env.NODE_ENV !== 'production') {
const name = component.displayName || component.name || 'Component'
wrapped.displayName = `with${wrapper.displayName}(${name})`
wrappedComponent.displayName = this.wrappers.reduce(
(acc, [wrapperComponent]) => `with${wrapperComponent.displayName}(${acc})`,
component.displayName || component.name || 'Component'
)
}
return wrapped

return wrappedComponent
}
}

type Wrap = WrapWithoutCSROnly & {
Suspense: WrapWithoutCSROnly['Suspense'] & {
CSROnly: (props?: PropsWithoutChildren<ComponentProps<typeof Suspense.CSROnly>>) => Wrap
}
AsyncBoundary: WrapWithoutCSROnly['AsyncBoundary'] & {
CSROnly: (props: PropsWithoutChildren<ComponentProps<typeof AsyncBoundary.CSROnly>>) => Wrap
}
}

const createWrap = () => {
const wrappers: Wrapper[] = []
const builder = new WrapWithoutCSROnly(wrappers) as Wrap
builder.Suspense.CSROnly = (props: PropsWithoutChildren<ComponentProps<typeof Suspense.CSROnly>> = {}) => {
wrappers.unshift([Suspense.CSROnly, props])
return builder
}
builder.AsyncBoundary.CSROnly = (props: PropsWithoutChildren<ComponentProps<typeof AsyncBoundary.CSROnly>>) => {
wrappers.unshift([AsyncBoundary.CSROnly, props])
return builder
}
return builder
}

const wrapSuspense = (...[props = {}]: Parameters<Wrap['Suspense']>) => createWrap().Suspense(props)
wrapSuspense.CSROnly = (...[props = {}]: Parameters<Wrap['Suspense']['CSROnly']>) =>
createWrap().Suspense.CSROnly(props)
const wrapErrorBoundary = (...[props]: Parameters<Wrap['ErrorBoundary']>) => createWrap().ErrorBoundary(props)
const wrapErrorBoundaryGroup = (...[props = {}]: Parameters<Wrap['ErrorBoundaryGroup']>) =>
createWrap().ErrorBoundaryGroup(props)
const wrapAsyncBoundary = (...[props]: Parameters<Wrap['AsyncBoundary']>) => createWrap().AsyncBoundary(props)
wrapAsyncBoundary.CSROnly = (...[props]: Parameters<Wrap['AsyncBoundary']['CSROnly']>) =>
createWrap().AsyncBoundary.CSROnly(props)
const wrapDelay = (...[props = {}]: Parameters<Wrap['Delay']>) => createWrap().Delay(props)

/**
* @experimental This is experimental feature.
*/
export const wrap = {
Suspense: wrapSuspense,
ErrorBoundary: wrapErrorBoundary,
ErrorBoundaryGroup: wrapErrorBoundaryGroup,
AsyncBoundary: wrapAsyncBoundary,
Delay: wrapDelay,
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
'use client'
import { ErrorBoundary, ErrorBoundaryGroup, Suspense, useErrorBoundary, wrap } from '@suspensive/react'
import { UseSuspenseQuery } from '~/components'
import { api } from '~/utils'

import { useErrorBoundary, wrap } from '@suspensive/react'

const logError = (error: Error) => console.error(error)

export default wrap(ErrorBoundaryGroup, { blockOutside: false })(
wrap(ErrorBoundary, { fallback: (props) => <>{props.error.message}</>, onError: logError })(
wrap(Suspense.CSROnly, { fallback: <>loading...</> })(() => {
const errorBoundary = useErrorBoundary()
const Page = wrap
.ErrorBoundaryGroup({ blockOutside: false })
.ErrorBoundary({ fallback: (props) => <div>{props.error.message}</div>, onError: logError })
.Suspense.CSROnly({ fallback: 'loading...' })
.on(({ text }: { text: string }) => {
const errorBoundary = useErrorBoundary()

return (
<div>
<button onClick={() => errorBoundary.setError(new Error('trigger error by useErrorBoundary().setError'))}>
trigger error by useErrorBoundary().setError
</button>
{text}
</div>
)
})

return (
<>
<button onClick={() => errorBoundary.setError(new Error('trigger error by useErrorBoundary().setError'))}>
trigger error by useErrorBoundary().setError
</button>
<UseSuspenseQuery queryKey={['with', 1] as const} queryFn={() => api.delay(200, { percentage: 50 })} />
</>
)
})
)
)
export default Page

0 comments on commit cb23858

Please sign in to comment.