-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Suspense, Asyncboundary, ErrorBoundary, generateContext 유틸 함수 생성
- Loading branch information
1 parent
241efea
commit 159d4e2
Showing
31 changed files
with
489 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
'use client'; | ||
|
||
import { forwardRef } from 'react'; | ||
import { useQueryErrorResetBoundary } from '@tanstack/react-query'; | ||
import { chain } from '@/util'; | ||
import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary'; | ||
import SSRSafeSuspense from '../SSRSafeSuspense/SSRSafeSuspense'; | ||
import type { StrictPropsWithChildren } from '@/types'; | ||
import type { ComponentProps, ComponentRef, Suspense } from 'react'; | ||
|
||
type ErrorBoundaryProps = Omit< | ||
ComponentProps<typeof ErrorBoundary>, | ||
'renderFallback' | ||
>; | ||
type SuspenseProps = Omit<ComponentProps<typeof Suspense>, 'fallback'>; | ||
|
||
type AsyncBoundrayProps = StrictPropsWithChildren & | ||
ErrorBoundaryProps & | ||
SuspenseProps & { | ||
errorFallback?: ComponentProps<typeof ErrorBoundary>['renderFallback']; | ||
pendingFallback?: ComponentProps<typeof Suspense>['fallback']; | ||
}; | ||
|
||
export const AsyncBoundary = forwardRef< | ||
ComponentRef<typeof ErrorBoundary>, | ||
AsyncBoundrayProps | ||
>( | ||
( | ||
{ errorFallback, pendingFallback, children, ...errorBoundaryProps }, | ||
ref, | ||
) => { | ||
return ( | ||
<ErrorBoundary | ||
ref={ref} | ||
renderFallback={errorFallback} | ||
{...errorBoundaryProps}> | ||
<SSRSafeSuspense fallback={pendingFallback}>{children}</SSRSafeSuspense> | ||
</ErrorBoundary> | ||
); | ||
}, | ||
); | ||
|
||
AsyncBoundary.displayName = 'AsyncBoundary'; | ||
|
||
export const AsyncBoundaryWithQuery = forwardRef< | ||
ComponentRef<typeof ErrorBoundary>, | ||
AsyncBoundrayProps | ||
>((props, ref) => { | ||
const { children, ...otherProps } = props; | ||
const { reset } = useQueryErrorResetBoundary(); | ||
|
||
return ( | ||
<AsyncBoundary | ||
ref={ref} | ||
{...otherProps} | ||
onReset={chain(props.onReset, reset)}> | ||
{children} | ||
</AsyncBoundary> | ||
); | ||
}); | ||
|
||
AsyncBoundaryWithQuery.displayName = 'AsyncBoundaryWithQuery'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
'use client'; | ||
|
||
import { | ||
Component, | ||
forwardRef, | ||
useImperativeHandle, | ||
useRef, | ||
useState, | ||
} from 'react'; | ||
import { isDifferentArray } from '@/util'; | ||
import { ErrorboundaryProvider } from './ErrorBoundaryContext'; | ||
import type { StrictPropsWithChildren } from '@/types'; | ||
import type { | ||
ComponentPropsWithoutRef, | ||
ErrorInfo, | ||
PropsWithRef, | ||
ReactNode, | ||
} from 'react'; | ||
|
||
type RenderFallbackProps<ErrorType extends Error = Error> = { | ||
error: ErrorType; | ||
reset?: () => void; | ||
}; | ||
|
||
type RenderFallbackType = <ErrorType extends Error>( | ||
props: RenderFallbackProps<ErrorType>, | ||
) => ReactNode; | ||
|
||
type FallbackType = ReactNode; | ||
|
||
type ErrorBoundaryProps<ErrorType extends Error = Error> = { | ||
onReset?(): void; | ||
renderFallback: RenderFallbackType | FallbackType; | ||
onError?(error: ErrorType, info: ErrorInfo): void; | ||
resetKeys?: unknown[]; | ||
}; | ||
|
||
interface State<ErrorType extends Error = Error> { | ||
error: ErrorType | null; | ||
} | ||
|
||
const initialState: State = { | ||
error: null, | ||
}; | ||
|
||
export class ErrorBoundary extends Component< | ||
PropsWithRef<StrictPropsWithChildren<ErrorBoundaryProps>>, | ||
State | ||
> { | ||
hasError = false; | ||
|
||
constructor( | ||
props: PropsWithRef<StrictPropsWithChildren<ErrorBoundaryProps>>, | ||
) { | ||
super(props); | ||
this.state = initialState; | ||
} | ||
|
||
static getDerivedStateFromError(error: Error) { | ||
return { error }; | ||
} | ||
|
||
componentDidUpdate(prevProps: ErrorBoundaryProps) { | ||
const { error } = this.state; | ||
const { resetKeys } = this.props; | ||
|
||
if (error === null) { | ||
return; | ||
} | ||
if (!this.hasError) { | ||
this.hasError = true; | ||
return; | ||
} | ||
|
||
if (isDifferentArray(prevProps.resetKeys, resetKeys)) { | ||
this.resetErrorBoundary(); | ||
} | ||
} | ||
|
||
componentDidCatch(error: Error, info: ErrorInfo) { | ||
const { onError } = this.props; | ||
|
||
onError?.(error, info); | ||
} | ||
|
||
resetErrorBoundary = () => { | ||
const { onReset } = this.props; | ||
|
||
onReset?.(); | ||
this.resetState(); | ||
}; | ||
|
||
resetState() { | ||
this.hasError = false; | ||
this.setState(initialState); | ||
} | ||
|
||
genereateRenderedChildren() { | ||
const { error } = this.state; | ||
const { children, renderFallback } = this.props; | ||
|
||
if (error === null) { | ||
return children; | ||
} | ||
|
||
return typeof renderFallback === 'function' | ||
? renderFallback({ | ||
error: error as Error, | ||
reset: this.resetErrorBoundary, | ||
}) | ||
: renderFallback; | ||
} | ||
|
||
render() { | ||
const { error } = this.state; | ||
|
||
const renderedChildren = this.genereateRenderedChildren(); | ||
const ErrorboundaryProviderProps = { | ||
error, | ||
resetErrorBoundary: this.resetErrorBoundary, | ||
}; | ||
|
||
return ( | ||
<ErrorboundaryProvider {...ErrorboundaryProviderProps}> | ||
{renderedChildren} | ||
</ErrorboundaryProvider> | ||
); | ||
} | ||
} | ||
|
||
export const GlobalErrorBoundary = forwardRef< | ||
{ reset(): void }, | ||
ComponentPropsWithoutRef<typeof ErrorBoundary> | ||
>((props, resetRef) => { | ||
const ref = useRef<ErrorBoundary>(null); | ||
|
||
useImperativeHandle(resetRef, () => ({ | ||
reset: () => ref.current?.resetErrorBoundary(), | ||
})); | ||
|
||
return <ErrorBoundary {...props} ref={ref} />; | ||
}); | ||
|
||
GlobalErrorBoundary.displayName = 'GlobalErrorBoundary'; | ||
|
||
export const useErrorBoundary = <ErrorType extends Error = Error>() => { | ||
const [error, setError] = useState<ErrorType | null>(null); | ||
|
||
if (error != null) { | ||
throw error as Error; | ||
} | ||
|
||
return setError; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
'use client'; | ||
|
||
import generateContext from '../generateContext/generateContext'; | ||
|
||
export const [ErrorboundaryProvider, useErrorBoundaryContext] = | ||
generateContext<{ | ||
error: Error | null; | ||
resetErrorBoundary: () => void; | ||
}>({ | ||
name: 'global-error-boundary-context', | ||
defaultContextValue: { error: null, resetErrorBoundary: () => {} }, | ||
}); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
'use client'; | ||
|
||
import ReactQuery from './ReactQuery'; | ||
import type { PropsWithChildren } from 'react'; | ||
|
||
export default function QueryProvider({ children }: PropsWithChildren) { | ||
return <ReactQuery>{children}</ReactQuery>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
'use client'; | ||
|
||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; | ||
import { useState } from 'react'; | ||
import type { PropsWithChildren } from 'react'; | ||
import type { QueryClientConfig } from '@tanstack/react-query'; | ||
|
||
const queryClientOption: QueryClientConfig = { | ||
defaultOptions: { | ||
queries: { | ||
refetchOnWindowFocus: false, | ||
// NOTE: 추후 배포 시 변경 | ||
staleTime: Infinity, | ||
}, | ||
mutations: { | ||
networkMode: 'always', | ||
}, | ||
}, | ||
}; | ||
|
||
export default function ReactQuery({ children }: PropsWithChildren) { | ||
const [queryClient] = useState(() => new QueryClient(queryClientOption)); | ||
|
||
return ( | ||
<QueryClientProvider client={queryClient}> | ||
{children} | ||
<ReactQueryDevtools /> | ||
</QueryClientProvider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
'use client'; | ||
|
||
import { useIsClient } from '@/hooks'; | ||
import { Suspense } from 'react'; | ||
import type { ComponentProps } from 'react'; | ||
|
||
export default function SSRSafeSuspense(props: ComponentProps<typeof Suspense>) { | ||
if (!useIsClient()) { | ||
// eslint-disable-next-line react/destructuring-assignment | ||
return props.fallback; | ||
} | ||
return <Suspense {...props} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
'use client'; | ||
|
||
import { createContext, useContext as ReactUseContext, useMemo } from 'react'; | ||
import type { ReactNode, Context as ReactContext } from 'react'; | ||
|
||
interface ContextOptions<ContextType> { | ||
name: string; | ||
errorMessage?: string; | ||
defaultContextValue?: ContextType; | ||
strict?: boolean; | ||
} | ||
|
||
type GenerateContextReturnType<T extends object> = [ | ||
({ children, ...providerValues }: ProviderProps<T>) => JSX.Element, | ||
() => T, | ||
ReactContext<T>, | ||
]; | ||
|
||
type ProviderProps<ContextValuesType extends object> = | ||
| (ContextValuesType & { children: ReactNode }) | ||
| { children: ReactNode }; | ||
|
||
export default function generateContext<ContextType extends object>( | ||
options: ContextOptions<ContextType>, | ||
) { | ||
const { | ||
name, | ||
errorMessage = `${name} Context가 존재하지 않습니다. Provider를 설정해주세요.`, | ||
defaultContextValue = null, | ||
strict = true, | ||
} = options; | ||
const Context = createContext<ContextType | null>( | ||
defaultContextValue ?? null, | ||
); | ||
|
||
function useContext() { | ||
const context = ReactUseContext(Context); | ||
|
||
if (context !== null) { | ||
return context; | ||
} | ||
|
||
if (defaultContextValue !== null) { | ||
return defaultContextValue; | ||
} | ||
|
||
if (strict) { | ||
const error = new Error(errorMessage); | ||
error.name = `${name} ContextError`; | ||
Error.captureStackTrace?.(error, useContext); | ||
throw error; | ||
} | ||
|
||
return context; | ||
} | ||
|
||
function Provider({ | ||
children, | ||
...providerValues | ||
}: ProviderProps<ContextType>) { | ||
const value = useMemo( | ||
() => (Object.keys(providerValues).length > 0 ? providerValues : null), | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[...Object.entries(providerValues).flat()], | ||
) as ContextType; | ||
|
||
return <Context.Provider value={value}>{children}</Context.Provider>; | ||
} | ||
|
||
Context.displayName = name; | ||
Provider.displayName = name; | ||
|
||
return [ | ||
Provider, | ||
useContext, | ||
Context, | ||
] as GenerateContextReturnType<ContextType>; | ||
} |
Oops, something went wrong.