Skip to content

Commit

Permalink
feat: Suspense, Asyncboundary, ErrorBoundary, generateContext 유틸 함수 생성
Browse files Browse the repository at this point in the history
  • Loading branch information
Collection50 committed Jul 19, 2024
1 parent 241efea commit 159d4e2
Show file tree
Hide file tree
Showing 31 changed files with 489 additions and 46 deletions.
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import QueryProvider from '@/context/QueryProvider/QueryProvider';

const inter = Inter({ subsets: ['latin'] });

Expand All @@ -16,7 +17,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
);
}
62 changes: 62 additions & 0 deletions src/context/AsyncBoundary/AsyncBoundary.tsx
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';
154 changes: 154 additions & 0 deletions src/context/ErrorBoundary/ErrorBoundary.tsx
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;
};
12 changes: 12 additions & 0 deletions src/context/ErrorBoundary/ErrorBoundaryContext.tsx
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: () => {} },
});
8 changes: 0 additions & 8 deletions src/context/Providers.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions src/context/QueryProvider/QueryProvider.tsx
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>;
}
31 changes: 31 additions & 0 deletions src/context/QueryProvider/ReactQuery.tsx
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>
);
}
13 changes: 13 additions & 0 deletions src/context/SSRSafeSuspense/SSRSafeSuspense.tsx
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} />;
}
78 changes: 78 additions & 0 deletions src/context/generateContext/generateContext.tsx
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>;
}
Loading

0 comments on commit 159d4e2

Please sign in to comment.