Skip to content

Commit

Permalink
feat(react): add experimental wrap to remove unnecessary hocs
Browse files Browse the repository at this point in the history
  • Loading branch information
manudeli committed Oct 28, 2023
1 parent 8aa9e44 commit e2067db
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 110 deletions.
35 changes: 5 additions & 30 deletions packages/react/src/AsyncBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ErrorBoundary } from './ErrorBoundary'
import type { ErrorBoundaryProps } from './ErrorBoundary'
import { Suspense } from './Suspense'
import type { PropsWithoutChildren } from './types'
import { wrap } from './wrap'

export type AsyncBoundaryProps = Omit<SuspenseProps, 'fallback'> &
Omit<ErrorBoundaryProps, 'fallback'> & {
Expand Down Expand Up @@ -46,36 +47,10 @@ export const AsyncBoundary = BaseAsyncBoundary as typeof BaseAsyncBoundary & {
AsyncBoundary.CSROnly = CSROnlyAsyncBoundary

export const withAsyncBoundary = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
Component: ComponentType<TProps>,
component: ComponentType<TProps>,
asyncBoundaryProps: PropsWithoutChildren<AsyncBoundaryProps>
) => {
const Wrapped = (props: TProps) => (
<AsyncBoundary {...asyncBoundaryProps}>
<Component {...props} />
</AsyncBoundary>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withAsyncBoundary(${name})`
}

return Wrapped
}
) => wrap(AsyncBoundary, asyncBoundaryProps)(component)
withAsyncBoundary.CSROnly = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
Component: ComponentType<TProps>,
component: ComponentType<TProps>,
asyncBoundaryProps: PropsWithoutChildren<AsyncBoundaryProps>
) => {
const Wrapped = (props: TProps) => (
<AsyncBoundary.CSROnly {...asyncBoundaryProps}>
<Component {...props} />
</AsyncBoundary.CSROnly>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withAsyncBoundary.CSROnly(${name})`
}

return Wrapped
}
) => wrap(AsyncBoundary.CSROnly, asyncBoundaryProps)(component)
23 changes: 7 additions & 16 deletions packages/react/src/Delay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ComponentProps, ComponentType, PropsWithChildren } from 'react'
import { createContext, useContext, useState } from 'react'
import { useTimeout } from './hooks'
import type { PropsWithoutChildren } from './types'
import { wrap } from './wrap'

export type DelayProps = PropsWithChildren<{
ms?: number
Expand All @@ -14,23 +15,13 @@ export const Delay = ({ ms, children }: DelayProps) => {
useTimeout(() => setIsDelayed(true), delayMs)
return <>{isDelayed ? children : null}</>
}
if (process.env.NODE_ENV !== 'production') {
Delay.displayName = 'Delay'
}

export const DelayContext = createContext<PropsWithoutChildren<DelayProps>>({ ms: 0 })

export const withDelay = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
Component: ComponentType<TProps>,
delayProps?: PropsWithoutChildren<DelayProps>
) => {
const Wrapped = (props: TProps) => (
<Delay {...delayProps}>
<Component {...props} />
</Delay>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withDelay(${name})`
}

return Wrapped
}
component: ComponentType<TProps>,
delayProps: PropsWithoutChildren<DelayProps> = {}
) => wrap(Delay, delayProps)(component)
16 changes: 2 additions & 14 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { ErrorBoundaryGroupContext } from './ErrorBoundaryGroup'
import type { PropsWithoutChildren } from './types'
import { assert, hasResetKeysChanged } from './utils'
import { wrap } from './wrap'

export type ErrorBoundaryFallbackProps<TError extends Error = Error> = {
/**
Expand Down Expand Up @@ -127,20 +128,7 @@ if (process.env.NODE_ENV !== 'production') {
export const withErrorBoundary = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
Component: ComponentType<TProps>,
errorBoundaryProps: PropsWithoutChildren<ErrorBoundaryProps>
) => {
const Wrapped = (props: TProps) => (
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withErrorBoundary(${name})`
}

return Wrapped
}
) => wrap(ErrorBoundary, errorBoundaryProps)(Component)

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

Expand Down
23 changes: 7 additions & 16 deletions packages/react/src/ErrorBoundaryGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createContext, useContext, useEffect, useMemo } from 'react'
import { useIsChanged, useKey } from './hooks'
import type { PropsWithoutChildren } from './types'
import { assert } from './utils'
import { wrap } from './wrap'

export const ErrorBoundaryGroupContext = createContext<{ reset: () => void; resetKey: number } | undefined>(undefined)
if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -36,6 +37,9 @@ export const ErrorBoundaryGroup = ({ blockOutside = false, children }: ErrorBoun

return <ErrorBoundaryGroupContext.Provider value={value}>{children}</ErrorBoundaryGroupContext.Provider>
}
if (process.env.NODE_ENV !== 'production') {
ErrorBoundaryGroup.displayName = 'ErrorBoundaryGroup'
}

const ErrorBoundaryGroupReset = ({
trigger: Trigger,
Expand Down Expand Up @@ -67,19 +71,6 @@ export const useErrorBoundaryGroup = () => {
}

export const withErrorBoundaryGroup = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
Component: ComponentType<TProps>,
errorBoundaryGroupProps?: PropsWithoutChildren<ErrorBoundaryGroupProps>
) => {
const Wrapped = (props: TProps) => (
<ErrorBoundaryGroup {...errorBoundaryGroupProps}>
<Component {...props} />
</ErrorBoundaryGroup>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withErrorBoundaryGroup(${name})`
}

return Wrapped
}
component: ComponentType<TProps>,
errorBoundaryGroupProps: PropsWithoutChildren<ErrorBoundaryGroupProps> = {}
) => wrap(ErrorBoundaryGroup, errorBoundaryGroupProps)(component)
42 changes: 9 additions & 33 deletions packages/react/src/Suspense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ComponentProps, ComponentType, ReactNode, SuspenseProps as ReactSu
import { Suspense as ReactSuspense, createContext, useContext } from 'react'
import { useIsClient } from './hooks'
import type { PropsWithoutChildren } from './types'
import { wrap } from './wrap'

export type SuspenseProps = ReactSuspenseProps

Expand Down Expand Up @@ -43,37 +44,12 @@ export const Suspense = DefaultSuspense as typeof DefaultSuspense & {
}
Suspense.CSROnly = CSROnlySuspense

export function withSuspense<TProps extends ComponentProps<ComponentType> = Record<string, never>>(
Component: ComponentType<TProps>,
suspenseProps?: PropsWithoutChildren<SuspenseProps>
) {
const Wrapped = (props: TProps) => (
<Suspense {...suspenseProps}>
<Component {...props} />
</Suspense>
)
export const withSuspense = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
suspenseProps: PropsWithoutChildren<SuspenseProps> = {}
) => wrap(Suspense, suspenseProps)(component)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withSuspense(${name})`
}

return Wrapped
}

withSuspense.CSROnly = function withSuspenseCSROnly<
TProps extends ComponentProps<ComponentType> = Record<string, never>
>(Component: ComponentType<TProps>, suspenseProps?: PropsWithoutChildren<SuspenseProps>) {
const Wrapped = (props: TProps) => (
<Suspense.CSROnly {...suspenseProps}>
<Component {...props} />
</Suspense.CSROnly>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withSuspense.CSROnly(${name})`
}

return Wrapped
}
withSuspense.CSROnly = <TProps extends ComponentProps<ComponentType> = Record<string, never>>(
component: ComponentType<TProps>,
suspenseProps: PropsWithoutChildren<SuspenseProps> = {}
) => wrap(Suspense.CSROnly, suspenseProps)(component)
2 changes: 2 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export { ErrorBoundaryGroup, withErrorBoundaryGroup, useErrorBoundaryGroup } fro
export { AsyncBoundary, withAsyncBoundary } from './AsyncBoundary'
export { Delay, withDelay } from './Delay'

export { wrap } from './wrap'

export type { SuspenseProps } from './Suspense'
export type { ErrorBoundaryProps, ErrorBoundaryFallbackProps } from './ErrorBoundary'
export type { ErrorBoundaryGroupProps } from './ErrorBoundaryGroup'
Expand Down
23 changes: 23 additions & 0 deletions packages/react/src/wrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type ComponentProps, type ComponentType, createElement } from 'react'
import type { PropsWithoutChildren } from './types'
import type { AsyncBoundaryProps, DelayProps, ErrorBoundaryGroupProps, ErrorBoundaryProps, SuspenseProps } from '.'

/**
* @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})`
}
return wrapped
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'
import { useErrorBoundary, withErrorBoundary, withErrorBoundaryGroup, withSuspense } from '@suspensive/react'
import { UseSuspenseQuery } from '~/components'
import { api } from '~/utils'

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

export default withErrorBoundaryGroup(
withErrorBoundary(
withSuspense(
() => {
const errorBoundary = useErrorBoundary()

return (
<>
<button onClick={() => errorBoundary.setError(new Error('trigger error by useErrorBoundary().setError'))}>
trigger error by useErrorBoundary().setError
</button>
<UseSuspenseQuery queryKey={['wrap', 1] as const} queryFn={() => api.delay(200, { percentage: 50 })} />
</>
)
},
{ fallback: <>loading...</> }
),
{ fallback: (props) => <>{props.error.message}</>, onError: logError }
),
{ blockOutside: false }
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'
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, { 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 })} />
</>
)
})
)
)
2 changes: 1 addition & 1 deletion websites/visualization/src/components/UseSuspenseQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useSuspenseQuery } from '@suspensive/react-query'
import { Box, Description } from './uis'

type Props = {
queryKey: Readonly<['query' | 'boundary', number]>
queryKey: Readonly<[string, number]>
queryFn: () => Promise<string>
}

Expand Down

0 comments on commit e2067db

Please sign in to comment.