From e2067db55b2f74a344040f9143c7c1fd959ad5dd Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Sat, 28 Oct 2023 18:48:14 +0900 Subject: [PATCH] feat(react): add experimental wrap to remove unnecessary hocs --- packages/react/src/AsyncBoundary.tsx | 35 +++------------- packages/react/src/Delay.tsx | 23 ++++------ packages/react/src/ErrorBoundary.tsx | 16 +------ packages/react/src/ErrorBoundaryGroup.tsx | 23 ++++------ packages/react/src/Suspense.tsx | 42 ++++--------------- packages/react/src/index.ts | 2 + packages/react/src/wrap.ts | 23 ++++++++++ .../react/experimental/wrap/after/page.tsx | 28 +++++++++++++ .../react/experimental/wrap/before/page.tsx | 23 ++++++++++ .../src/components/UseSuspenseQuery.tsx | 2 +- 10 files changed, 107 insertions(+), 110 deletions(-) create mode 100644 packages/react/src/wrap.ts create mode 100644 websites/visualization/src/app/react/experimental/wrap/after/page.tsx create mode 100644 websites/visualization/src/app/react/experimental/wrap/before/page.tsx diff --git a/packages/react/src/AsyncBoundary.tsx b/packages/react/src/AsyncBoundary.tsx index 55a69c66b..04490de3f 100644 --- a/packages/react/src/AsyncBoundary.tsx +++ b/packages/react/src/AsyncBoundary.tsx @@ -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 & Omit & { @@ -46,36 +47,10 @@ export const AsyncBoundary = BaseAsyncBoundary as typeof BaseAsyncBoundary & { AsyncBoundary.CSROnly = CSROnlyAsyncBoundary export const withAsyncBoundary = = Record>( - Component: ComponentType, + component: ComponentType, asyncBoundaryProps: PropsWithoutChildren -) => { - const Wrapped = (props: TProps) => ( - - - - ) - - 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 = = Record>( - Component: ComponentType, + component: ComponentType, asyncBoundaryProps: PropsWithoutChildren -) => { - const Wrapped = (props: TProps) => ( - - - - ) - - 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) diff --git a/packages/react/src/Delay.tsx b/packages/react/src/Delay.tsx index 8db099133..aff76ea5f 100644 --- a/packages/react/src/Delay.tsx +++ b/packages/react/src/Delay.tsx @@ -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 @@ -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>({ ms: 0 }) export const withDelay = = Record>( - Component: ComponentType, - delayProps?: PropsWithoutChildren -) => { - const Wrapped = (props: TProps) => ( - - - - ) - - if (process.env.NODE_ENV !== 'production') { - const name = Component.displayName || Component.name || 'Component' - Wrapped.displayName = `withDelay(${name})` - } - - return Wrapped -} + component: ComponentType, + delayProps: PropsWithoutChildren = {} +) => wrap(Delay, delayProps)(component) diff --git a/packages/react/src/ErrorBoundary.tsx b/packages/react/src/ErrorBoundary.tsx index 724a06705..a20c13ae8 100644 --- a/packages/react/src/ErrorBoundary.tsx +++ b/packages/react/src/ErrorBoundary.tsx @@ -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 = { /** @@ -127,20 +128,7 @@ if (process.env.NODE_ENV !== 'production') { export const withErrorBoundary = = Record>( Component: ComponentType, errorBoundaryProps: PropsWithoutChildren -) => { - const Wrapped = (props: TProps) => ( - - - - ) - - 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) diff --git a/packages/react/src/ErrorBoundaryGroup.tsx b/packages/react/src/ErrorBoundaryGroup.tsx index 1d766513f..83f4a288d 100644 --- a/packages/react/src/ErrorBoundaryGroup.tsx +++ b/packages/react/src/ErrorBoundaryGroup.tsx @@ -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') { @@ -36,6 +37,9 @@ export const ErrorBoundaryGroup = ({ blockOutside = false, children }: ErrorBoun return {children} } +if (process.env.NODE_ENV !== 'production') { + ErrorBoundaryGroup.displayName = 'ErrorBoundaryGroup' +} const ErrorBoundaryGroupReset = ({ trigger: Trigger, @@ -67,19 +71,6 @@ export const useErrorBoundaryGroup = () => { } export const withErrorBoundaryGroup = = Record>( - Component: ComponentType, - errorBoundaryGroupProps?: PropsWithoutChildren -) => { - const Wrapped = (props: TProps) => ( - - - - ) - - if (process.env.NODE_ENV !== 'production') { - const name = Component.displayName || Component.name || 'Component' - Wrapped.displayName = `withErrorBoundaryGroup(${name})` - } - - return Wrapped -} + component: ComponentType, + errorBoundaryGroupProps: PropsWithoutChildren = {} +) => wrap(ErrorBoundaryGroup, errorBoundaryGroupProps)(component) diff --git a/packages/react/src/Suspense.tsx b/packages/react/src/Suspense.tsx index 9d1a998bb..6a27cb2fe 100644 --- a/packages/react/src/Suspense.tsx +++ b/packages/react/src/Suspense.tsx @@ -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 @@ -43,37 +44,12 @@ export const Suspense = DefaultSuspense as typeof DefaultSuspense & { } Suspense.CSROnly = CSROnlySuspense -export function withSuspense = Record>( - Component: ComponentType, - suspenseProps?: PropsWithoutChildren -) { - const Wrapped = (props: TProps) => ( - - - - ) +export const withSuspense = = Record>( + component: ComponentType, + suspenseProps: PropsWithoutChildren = {} +) => 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 = Record ->(Component: ComponentType, suspenseProps?: PropsWithoutChildren) { - const Wrapped = (props: TProps) => ( - - - - ) - - if (process.env.NODE_ENV !== 'production') { - const name = Component.displayName || Component.name || 'Component' - Wrapped.displayName = `withSuspense.CSROnly(${name})` - } - - return Wrapped -} +withSuspense.CSROnly = = Record>( + component: ComponentType, + suspenseProps: PropsWithoutChildren = {} +) => wrap(Suspense.CSROnly, suspenseProps)(component) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index dfe975c9f..1d1a67ae2 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -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' diff --git a/packages/react/src/wrap.ts b/packages/react/src/wrap.ts new file mode 100644 index 000000000..70375680e --- /dev/null +++ b/packages/react/src/wrap.ts @@ -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, + 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})` + } + return wrapped + } diff --git a/websites/visualization/src/app/react/experimental/wrap/after/page.tsx b/websites/visualization/src/app/react/experimental/wrap/after/page.tsx new file mode 100644 index 000000000..1de9940fa --- /dev/null +++ b/websites/visualization/src/app/react/experimental/wrap/after/page.tsx @@ -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 ( + <> + + api.delay(200, { percentage: 50 })} /> + + ) + }, + { fallback: <>loading... } + ), + { fallback: (props) => <>{props.error.message}, onError: logError } + ), + { blockOutside: false } +) diff --git a/websites/visualization/src/app/react/experimental/wrap/before/page.tsx b/websites/visualization/src/app/react/experimental/wrap/before/page.tsx new file mode 100644 index 000000000..b9edeb74a --- /dev/null +++ b/websites/visualization/src/app/react/experimental/wrap/before/page.tsx @@ -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 ( + <> + + api.delay(200, { percentage: 50 })} /> + + ) + }) + ) +) diff --git a/websites/visualization/src/components/UseSuspenseQuery.tsx b/websites/visualization/src/components/UseSuspenseQuery.tsx index 513946ef9..fe119a39c 100644 --- a/websites/visualization/src/components/UseSuspenseQuery.tsx +++ b/websites/visualization/src/components/UseSuspenseQuery.tsx @@ -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 }