diff --git a/packages/react/src/AsyncBoundary.tsx b/packages/react/src/AsyncBoundary.tsx index 04490de3f..8db1b5444 100644 --- a/packages/react/src/AsyncBoundary.tsx +++ b/packages/react/src/AsyncBoundary.tsx @@ -49,8 +49,8 @@ AsyncBoundary.CSROnly = CSROnlyAsyncBoundary export const withAsyncBoundary = = Record>( component: ComponentType, asyncBoundaryProps: PropsWithoutChildren -) => wrap(AsyncBoundary, asyncBoundaryProps)(component) +) => wrap.AsyncBoundary(asyncBoundaryProps).on(component) withAsyncBoundary.CSROnly = = Record>( component: ComponentType, asyncBoundaryProps: PropsWithoutChildren -) => wrap(AsyncBoundary.CSROnly, asyncBoundaryProps)(component) +) => wrap.AsyncBoundary.CSROnly(asyncBoundaryProps).on(component) diff --git a/packages/react/src/Delay.tsx b/packages/react/src/Delay.tsx index aff76ea5f..7521dbe26 100644 --- a/packages/react/src/Delay.tsx +++ b/packages/react/src/Delay.tsx @@ -24,4 +24,4 @@ export const DelayContext = createContext>({ ms export const withDelay = = Record>( component: ComponentType, delayProps: PropsWithoutChildren = {} -) => wrap(Delay, delayProps)(component) +) => wrap.Delay(delayProps).on(component) diff --git a/packages/react/src/ErrorBoundary.tsx b/packages/react/src/ErrorBoundary.tsx index 5ff26a71c..e157c96a9 100644 --- a/packages/react/src/ErrorBoundary.tsx +++ b/packages/react/src/ErrorBoundary.tsx @@ -126,9 +126,9 @@ if (process.env.NODE_ENV !== 'production') { } export const withErrorBoundary = = Record>( - Component: ComponentType, + component: ComponentType, errorBoundaryProps: PropsWithoutChildren -) => wrap(ErrorBoundary, errorBoundaryProps)(Component) +) => wrap.ErrorBoundary(errorBoundaryProps).on(component) const ErrorBoundaryContext = createContext<({ reset: () => void } & ErrorBoundaryState) | null>(null) diff --git a/packages/react/src/ErrorBoundaryGroup.tsx b/packages/react/src/ErrorBoundaryGroup.tsx index 83f4a288d..6151375c4 100644 --- a/packages/react/src/ErrorBoundaryGroup.tsx +++ b/packages/react/src/ErrorBoundaryGroup.tsx @@ -73,4 +73,4 @@ export const useErrorBoundaryGroup = () => { export const withErrorBoundaryGroup = = Record>( component: ComponentType, errorBoundaryGroupProps: PropsWithoutChildren = {} -) => wrap(ErrorBoundaryGroup, errorBoundaryGroupProps)(component) +) => wrap.ErrorBoundaryGroup(errorBoundaryGroupProps).on(component) diff --git a/packages/react/src/Suspense.tsx b/packages/react/src/Suspense.tsx index 6a27cb2fe..26cdf53c6 100644 --- a/packages/react/src/Suspense.tsx +++ b/packages/react/src/Suspense.tsx @@ -47,9 +47,9 @@ Suspense.CSROnly = CSROnlySuspense export const withSuspense = = Record>( component: ComponentType, suspenseProps: PropsWithoutChildren = {} -) => wrap(Suspense, suspenseProps)(component) +) => wrap.Suspense(suspenseProps).on(component) withSuspense.CSROnly = = Record>( component: ComponentType, suspenseProps: PropsWithoutChildren = {} -) => wrap(Suspense.CSROnly, suspenseProps)(component) +) => wrap.Suspense.CSROnly(suspenseProps).on(component) diff --git a/packages/react/src/wrap.ts b/packages/react/src/wrap.ts index 70375680e..e41bab044 100644 --- a/packages/react/src/wrap.ts +++ b/packages/react/src/wrap.ts @@ -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>] + +type Wrapper = + | WrapperItem + | WrapperItem + | WrapperItem + | WrapperItem + | WrapperItem + | WrapperItem + | WrapperItem + +class WrapWithoutCSROnly { + constructor(private wrappers: Wrapper[]) {} + Suspense = (props: PropsWithoutChildren> = {}) => { + this.wrappers.unshift([Suspense, props]) + return this + } + ErrorBoundary = (props: PropsWithoutChildren>) => { + this.wrappers.unshift([ErrorBoundary, props]) + return this + } + ErrorBoundaryGroup = (props: PropsWithoutChildren> = {}) => { + this.wrappers.unshift([ErrorBoundaryGroup, props]) + return this + } + AsyncBoundary = (props: PropsWithoutChildren>) => { + this.wrappers.unshift([AsyncBoundary, props]) + return this + } + Delay = (props: PropsWithoutChildren> = {}) => { + this.wrappers.unshift([Delay, props]) + return this + } + + on = >(component: ComponentType) => { + 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, - wrapperProps: PropsWithoutChildren - ) => - >(component: ComponentType) => { - 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>) => Wrap + } + AsyncBoundary: WrapWithoutCSROnly['AsyncBoundary'] & { + CSROnly: (props: PropsWithoutChildren>) => Wrap + } +} + +const createWrap = () => { + const wrappers: Wrapper[] = [] + const builder = new WrapWithoutCSROnly(wrappers) as Wrap + builder.Suspense.CSROnly = (props: PropsWithoutChildren> = {}) => { + wrappers.unshift([Suspense.CSROnly, props]) + return builder } + builder.AsyncBoundary.CSROnly = (props: PropsWithoutChildren>) => { + wrappers.unshift([AsyncBoundary.CSROnly, props]) + return builder + } + return builder +} + +const wrapSuspense = (...[props = {}]: Parameters) => createWrap().Suspense(props) +wrapSuspense.CSROnly = (...[props = {}]: Parameters) => + createWrap().Suspense.CSROnly(props) +const wrapErrorBoundary = (...[props]: Parameters) => createWrap().ErrorBoundary(props) +const wrapErrorBoundaryGroup = (...[props = {}]: Parameters) => + createWrap().ErrorBoundaryGroup(props) +const wrapAsyncBoundary = (...[props]: Parameters) => createWrap().AsyncBoundary(props) +wrapAsyncBoundary.CSROnly = (...[props]: Parameters) => + createWrap().AsyncBoundary.CSROnly(props) +const wrapDelay = (...[props = {}]: Parameters) => createWrap().Delay(props) + +/** + * @experimental This is experimental feature. + */ +export const wrap = { + Suspense: wrapSuspense, + ErrorBoundary: wrapErrorBoundary, + ErrorBoundaryGroup: wrapErrorBoundaryGroup, + AsyncBoundary: wrapAsyncBoundary, + Delay: wrapDelay, +} diff --git a/websites/visualization/src/app/react/experimental/wrap/after/page.tsx b/websites/visualization/src/app/react/experimental/wrap/after/page.tsx index 3b3e8d4ee..c45a44f9c 100644 --- a/websites/visualization/src/app/react/experimental/wrap/after/page.tsx +++ b/websites/visualization/src/app/react/experimental/wrap/after/page.tsx @@ -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) =>
{props.error.message}
, onError: logError }) + .Suspense.CSROnly({ fallback: 'loading...' }) + .on(({ text }: { text: string }) => { + const errorBoundary = useErrorBoundary() + + return ( +
+ + {text} +
+ ) + }) - return ( - <> - - api.delay(200, { percentage: 50 })} /> - - ) - }) - ) -) +export default Page