-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: assert 유틸 함수 작성 * feat: useFunnel 커스텀 훅 작성 * feat: Funnel 컴포넌트 작성 * refactor: useFunnel 개선
- Loading branch information
Showing
3 changed files
with
139 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
'use client'; | ||
|
||
import { useEffect, useMemo, useRef } from 'react'; | ||
|
||
import type { FunnelProps, StepProps } from '@/v1/base/Funnel/Funnel'; | ||
import { assert } from '@/utils/assert'; | ||
|
||
import { Funnel, Step } from '@/v1/base/Funnel/Funnel'; | ||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; | ||
|
||
export type NonEmptyArray<T> = readonly [T, ...T[]]; | ||
|
||
type RouteFunnelProps<Steps extends NonEmptyArray<string>> = Omit< | ||
FunnelProps<Steps>, | ||
'steps' | 'step' | ||
>; | ||
|
||
type FunnelComponent<Steps extends NonEmptyArray<string>> = (( | ||
props: RouteFunnelProps<Steps> | ||
) => JSX.Element) & { | ||
Step: (props: StepProps<Steps>) => JSX.Element; | ||
}; | ||
|
||
const DEFAULT_STEP_QUERY_KEY = 'funnel-step'; | ||
|
||
/** | ||
* 사용자에게 초기 step을 강제하고 싶을 땐 | ||
* option의 initialStep을 작성해 주세요. | ||
*/ | ||
export const useFunnel = <Steps extends NonEmptyArray<string>>( | ||
steps: Steps, | ||
options?: { | ||
stepQueryKey?: string; | ||
initialStep?: Steps[number]; | ||
onStepChange?: (name: Steps[number]) => void; | ||
} | ||
): readonly [FunnelComponent<Steps>, (step: Steps[number]) => void] => { | ||
const router = useRouter(); | ||
const searchParams = useSearchParams(); | ||
const pathname = usePathname(); | ||
|
||
const hasRunOnce = useRef(false); | ||
|
||
const step = searchParams.get('funnel-step') as string; | ||
const stepQueryKey = options?.stepQueryKey ?? DEFAULT_STEP_QUERY_KEY; | ||
|
||
useEffect(() => { | ||
if (options?.initialStep && !hasRunOnce.current) { | ||
hasRunOnce.current = true; | ||
router.replace(pathname); | ||
} | ||
}, [options?.initialStep, router, pathname]); | ||
|
||
assert(steps.length > 0, 'steps가 비어있습니다.'); | ||
|
||
const FunnelComponent = useMemo( | ||
() => | ||
Object.assign( | ||
function RouteFunnel(props: RouteFunnelProps<Steps>) { | ||
const currentStep = step ?? options?.initialStep; | ||
|
||
assert( | ||
currentStep != null, | ||
`표시할 스텝을 ${stepQueryKey} 쿼리 파라미터에 지정해주세요. 쿼리 파라미터가 없을 때 초기 스텝을 렌더하려면 useFunnel의 두 번째 파라미터 options에 initialStep을 지정해주세요.` | ||
); | ||
|
||
return <Funnel<Steps> steps={steps} step={currentStep} {...props} />; | ||
}, | ||
{ | ||
Step, | ||
} | ||
), | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[step] | ||
); | ||
|
||
const setStep = (step: Steps[number]) => { | ||
const params = new URLSearchParams(searchParams.toString()); | ||
params.set('funnel-step', `${step}`); | ||
|
||
return router.replace(`?${params.toString()}`); | ||
}; | ||
|
||
return [FunnelComponent, setStep] as unknown as readonly [ | ||
FunnelComponent<Steps>, | ||
(step: Steps[number]) => Promise<void> | ||
]; | ||
}; |
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,9 @@ | ||
export const assert = (condition: unknown, error: Error | string) => { | ||
if (!condition) { | ||
if (typeof error === 'string') { | ||
throw new Error(error); | ||
} else { | ||
throw error; | ||
} | ||
} | ||
}; |
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,42 @@ | ||
import { Children, ReactElement, ReactNode, isValidElement } from 'react'; | ||
|
||
import type { NonEmptyArray } from '@/hooks/useFunnel'; | ||
|
||
import { assert } from '@/utils/assert'; | ||
|
||
export interface FunnelProps<Steps extends NonEmptyArray<string>> { | ||
steps: Steps; | ||
step: Steps[number]; | ||
children: | ||
| Array<ReactElement<StepProps<Steps>>> | ||
| ReactElement<StepProps<Steps>>; | ||
} | ||
|
||
export const Funnel = <Steps extends NonEmptyArray<string>>({ | ||
steps, | ||
step, | ||
children, | ||
}: FunnelProps<Steps>) => { | ||
const validChildren = Children.toArray(children) | ||
.filter(isValidElement) | ||
.filter(i => | ||
steps.includes((i.props as Partial<StepProps<Steps>>).name ?? '') | ||
) as Array<ReactElement<StepProps<Steps>>>; | ||
|
||
const targetStep = validChildren.find(child => child.props.name === step); | ||
|
||
assert(targetStep != null, `${step} 스텝 컴포넌트를 찾지 못했습니다.`); | ||
|
||
return <>{targetStep}</>; | ||
}; | ||
|
||
export interface StepProps<Steps extends NonEmptyArray<string>> { | ||
name: Steps[number]; | ||
children: ReactNode; | ||
} | ||
|
||
export const Step = <T extends NonEmptyArray<string>>({ | ||
children, | ||
}: StepProps<T>) => { | ||
return <>{children}</>; | ||
}; |