UNPKG

@router-cli/react-router

Version:

File based routing cli for react-router-dom. Types and components. see @router-cli/react-router-dev for the cli and plugins.

126 lines (113 loc) 5.36 kB
import { NavigateOptions, ParamParseKey, PathMatch, PathPattern, useLocation, useNavigate, useParams, useRouteLoaderData, useSearchParams } from 'react-router-dom'; import { ParamSchema, TypedToOrPath, AnyRouteImports, AnyRouteComponents } from '../types'; import { useCallback, useMemo } from 'react'; import { parseQuery } from '../utils/requestParser'; import { getBasicPath } from '../utils/typed'; import searchParamUtilities from '../utils/searchParams'; import { useSuspendedPromise } from '../hooks/useSuspendedPromise'; import { parseSchema } from '../utils/schemaParser'; import { ExtractRouteDataMap, PickRoutesWithSchema } from '../utils/types'; import { matchPathLogic } from './functions'; export function createTypedHooks<TPages extends AnyRouteComponents, TLayouts extends AnyRouteComponents>() { type AllRoutes = TPages & TLayouts; return { useNavigate() { const navigate = useNavigate(); return useCallback(<TPath extends keyof TPages & string>(route: TypedToOrPath<TPath, TPages[TPath]> | number, options?: NavigateOptions) => { if (typeof route === "number") { return navigate(route); } return navigate(getBasicPath(route), options); }, [navigate]); }, useLoaderData< TPath extends keyof AllRoutes & string, >(route: TPath) { const loaderData = useRouteLoaderData(route); return loaderData as AllRoutes[TPath]["routeData"]["__types"]["loader"]; }, useMatch< TParamKey extends ParamParseKey<TPath>, TPath extends keyof AllRoutes & string >(pattern: PathPattern<TPath> | TPath): PathMatch<TParamKey> | null { let { pathname } = useLocation(); return useMemo(() => matchPathLogic<TParamKey, TPath>(pattern, pathname), [pathname, pattern]); } } } export const createImportHooks = <TPageImports extends AnyRouteImports, TLayoutImports extends AnyRouteImports>({ pages, layouts }: { pages: TPageImports, layouts: TLayoutImports, }) => { type Pages = ExtractRouteDataMap<TPageImports>; type Layouts = ExtractRouteDataMap<TLayoutImports>; type AllRoutes = Pages & Layouts; type RoutesWithParams = PickRoutesWithSchema<AllRoutes, "params">; type RoutesWithSearchParams = PickRoutesWithSchema<AllRoutes, "search">; const useImport = <TRoute extends keyof AllRoutes & string>(key: TRoute) => { const modulePromise = key.endsWith("/layout") ? layouts[key] : pages[key]; // This "should" never suspend, since the route will be written to cache when the router resolves it. const getter = useSuspendedPromise(modulePromise(), key); return getter as AllRoutes[TRoute]; } return { useSearch<TPath extends keyof AllRoutes & keyof RoutesWithSearchParams & string>(route: TPath) { const module = useImport(route); return useTypedSearch<RoutesWithSearchParams[TPath]>(module.routeData.searchSchema!); }, useParams<TPath extends keyof AllRoutes & keyof RoutesWithParams & string>(route: TPath): RoutesWithParams[TPath] { const module = useImport(route); return useTypedParams(module.routeData.paramsSchema!); }, } } export type SetAction<TData> = ((data: TData | undefined) => TData | undefined) | (TData | undefined); export function useTypedSearch<TData extends Record<string, unknown>>(schema: ParamSchema<TData>): [TData, (action: SetAction<TData>) => void] { if (!schema) { throw new Error("useParams cannot be used on a route with no paramsSchema"); } const [searchParams, setSearchParams] = useSearchParams(); return [ useMemo(() => parseQuery(searchParams, schema), [searchParams, schema]), useCallback((action: SetAction<TData>) => searchParamsSetter(action, setSearchParams, schema), [setSearchParams, schema]), ]; } export function useTypedParams<TData extends Record<string, unknown>>(schema: ParamSchema<TData>): TData { if (!schema) { throw new Error("useSearch cannot be used on a route with no searchSchema"); } const params = useParams(); return useMemo(() => parseSchema(params, schema), [schema, params]); } const searchParamsSetter = <TData extends Record<string, unknown>>(action: SetAction<TData>, setter: ReturnType<typeof useSearchParams>[1], schema: ParamSchema<TData>): any => { if (action instanceof Function) { setter((current) => { const currentParsed = parseQuery<TData>(current, schema); const data = action(currentParsed); if (data) { return validateSearchParams(schema, data); } return ""; }); } else { if (action) { var str = validateSearchParams(schema, action); setter(str); } } } const validateSearchParams = <TData extends Record<string, unknown>>(schema: ParamSchema<TData>, data: TData): string => { const validation = parseSchema(data, schema); return searchParamUtilities.stringify(validation); }