import { type ParamListBase, useNavigation as useNavigationNative, useRoute, } from '@react-native-bedrock/native/@react-navigation/native'; import { NativeStackNavigationProp } from '@react-native-bedrock/native/@react-navigation/native-stack'; import { useMemo } from 'react'; import { RESERVED_PATHS } from './constants'; import { defaultParserParams } from './utils/defaultParserParams'; export interface BedrockRouteOptions> { parserParams?: (params: Record) => Record; validateParams?: (params: Readonly) => T; component: React.FC; } export type BedrockNavigationProps = NativeStackNavigationProp< // @ts-expect-error - override type keyof RegisterScreen extends never ? ParamListBase : RegisterScreen >; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface RegisterScreen {} export function useNavigation() { return useNavigationNative(); } export type BedrockRouteHooksOptions = | { from: TScreen; strict?: true; } | { strict: false; from?: never; }; export const routeMap = new Map< keyof RegisterScreen, { options: Omit, 'component'>; component: React.FC } >(); export function useMatchOptions(options: BedrockRouteHooksOptions) { const route = useRoute(); const from = 'from' in options ? options.from : (route.name as TScreen); const strict = 'from' in options ? true : options.strict; if (strict && from !== route.name) { throw new Error(`Cannot access parameters from route '${from}' in current route '${route.name}'`); } return useMemo(() => { if (!from) { return null; } if (!(routeMap.has(from) || RESERVED_PATHS.includes(from))) { throw new Error(`Route '${from}' is not registered`); } const routeOptions = routeMap.get(from); return { ...routeOptions?.options, parserParams: routeOptions?.options.parserParams ?? defaultParserParams, }; }, [from]); } export function useParams(options: { from: TScreen; strict?: true; }): RegisterScreen[TScreen]; export function useParams(options: { strict: false }): Readonly; /** * @public * @category 화면 제어 * @name useParams * @description * * `useParams`는 지정된 라우트에서 파라미터를 가져오는 훅이에요. * 이 훅을 사용하면 현재 라우트의 파라미터에 쉽게 접근할 수 있어요. * `validateParams` 옵션을 사용하면 파라미터 구조를 검증하고 타입을 변환할 수 있어서 런타임 에러를 줄이고 안전한 코드를 작성할 수 있어요. * * @param {BedrockRouteHooksOptions} options 가져올 라우트의 정보를 담은 객체예요. * @param {string} [options.from] 파라미터를 가져올 라우트 경로예요. 이 값을 지정하지 않으면 현재 라우트에서 파라미터를 가져와요. 엄격 모드(`strict`)가 `true`인 경우 반드시 지정해야 해요. * @param {boolean} [options.strict] 엄격 모드 설정이에요. `true`로 설정하면 지정된 라우트와 현재 라우트가 일치하지 않으면 에러를 발생시켜요. `false`로 설정하면 `validateParams` 검증을 생략하고 현재 화면의 파라미터를 그대로 반환해요. * * @example * ### 라우트 파라미터 가져오기 * * * ::: code-group * * ```tsx [pages/examples/use-params.tsx] * import React from 'react'; * import { Text } from 'react-native'; * import { BedrockRoute, useParams } from 'react-native-bedrock'; * * export const Route = BedrockRoute('/examples/use-params', { * validateParams: (params) => params as { id: string }, * component: UseParamsExample, * }); * * function UseParamsExample() { * // 첫 번째 방법: 라우트 객체의 `useParams` 메서드 사용 * const params = Route.useParams(); * * // 두 번째 방법: useParams 훅 직접 사용 * const params2 = useParams({ from: '/examples/use-params' }); * * // 세 번째 방법: strict 모드를 false로 설정하여 사용 * // strict: false로 설정하면 현재 라우트의 파라미터를 가져오며, * // validateParams가 정의되어 있어도 검증을 건너뛰어요. * const params3 = useParams({ strict: false }) as { id: string }; * * return ( * <> * {params.id} * {params2.id} * {params3.id} * * ); * } * ``` * ::: */ export function useParams( options: BedrockRouteHooksOptions ): TScreen extends keyof RegisterScreen ? RegisterScreen[TScreen] : Readonly { const routeOptions = useMatchOptions(options); const route = useRoute(); const params = useMemo(() => { if (!routeOptions) { return (route.params ?? {}) as Readonly; } const parsedParams = routeOptions.parserParams(route.params as Record); return options.strict && routeOptions.validateParams ? routeOptions.validateParams(parsedParams) : parsedParams; }, [routeOptions, route.params, options.strict]); return params; } export const BedrockRoute = >( path: keyof RegisterScreen, options: BedrockRouteOptions ) => { const { component, ...restOptions } = options; routeMap.set(path, { options: restOptions, component }); const _path = path as keyof RegisterScreen; return { _path, useNavigation, useParams: () => useParams({ from: _path, strict: true }) as T, }; };