@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
text/typescript
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);
}