-
Notifications
You must be signed in to change notification settings - Fork 52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(react): add experimental wrap to remove unnecessary hocs #270
Changes from all commits
e2067db
5e85da0
b93b7a8
527164f
4d38677
de308f8
b1f5f04
c852a81
e814f1a
7e1f097
d1a04f7
cb23858
8d323e0
cae241a
95cb71e
8273979
13d856e
b92b159
75238e8
20db7a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@suspensive/react": minor | ||
--- | ||
|
||
feat(react): add experimental wrap to remove unnecessary hocs' implementation |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ export { ErrorBoundaryGroup, withErrorBoundaryGroup, useErrorBoundaryGroup } fro | |
export { AsyncBoundary, withAsyncBoundary } from './AsyncBoundary' | ||
export { Delay, withDelay } from './Delay' | ||
|
||
export { wrap } from './wrap' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree about this My suggestion is It is like jest description, using
it is readable continually by combining before// v1
withErrorBoundary(ErrorBoundary, {})(MyComponent)
// v2 wrap case
wrap(ErrorBoundary, {})(MyComponent) suggestionwith(ErrorBoundary, {})(MyComponent) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered And also, because this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, I don't know about with is keyword in strict mode. wrap is better than I thought another naming. Let's Go ! |
||
|
||
export type { SuspenseProps } from './Suspense' | ||
export type { ErrorBoundaryProps, ErrorBoundaryFallbackProps } from './ErrorBoundary' | ||
export type { ErrorBoundaryGroupProps } from './ErrorBoundaryGroup' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { createElement } from 'react' | ||
import type { ComponentProps, ComponentType } from 'react' | ||
import type { PropsWithoutChildren } from './types' | ||
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>> = {}) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to provide default parameter?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought wrap.Suspense, wrap.Suspense.CSROnly, wrap.Delay don't need props object const Example = wrap.Suspense(/* no parameter is available*/).on(() => {
return 'success'
}) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not all wrap.XXX have optional props object. optional props object is only for wrap.Suspense, wrap.Suspense.CSROnly, wrap.Delay. wrap.ErrorBoundary, wrap.ErrorBoundaryGroup require props object. Making component by wrap will run only one time. so I thought unnecessary variable allocation is okay. but if you have idea to improve performance, could you make Pull Request to this branch? it will make all ci(test, type:check, lint, etc.) and place to easy to be review |
||
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) | ||
) | ||
|
||
if (process.env.NODE_ENV !== 'production') { | ||
wrappedComponent.displayName = this.wrappers.reduce( | ||
(acc, [wrapperComponent]) => `with${wrapperComponent.displayName}(${acc})`, | ||
component.displayName || component.name || 'Component' | ||
) | ||
} | ||
|
||
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 |
---|---|---|
@@ -0,0 +1,24 @@ | ||
'use client' | ||
|
||
import { useErrorBoundary, wrap } from '@suspensive/react' | ||
|
||
const logError = (error: Error) => console.error(error) | ||
|
||
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> | ||
) | ||
}) | ||
|
||
export default Page |
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.CSROnly( | ||
() => { | ||
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 } | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary implementations of all hocs is removed by
wrap
function