UNPKG

@tanstack/react-router

Version:

Modern and scalable routing for React applications

791 lines (724 loc) • 18.7 kB
import invariant from 'tiny-invariant' import { joinPaths, rootRouteId, trimPathLeft } from '@tanstack/router-core' import { useLoaderData } from './useLoaderData' import { useLoaderDeps } from './useLoaderDeps' import { useParams } from './useParams' import { useSearch } from './useSearch' import { notFound } from './not-found' import { useNavigate } from './useNavigate' import { useMatch } from './useMatch' import { useRouter } from './useRouter' import type { AnyContext, AnyRoute, AnyRouter, Constrain, ConstrainLiteral, LazyRoute as CoreLazyRoute, Route as CoreRoute, ErrorComponentProps, NotFoundRouteProps, RegisteredRouter, ResolveFullPath, ResolveId, ResolveParams, RootRouteId, RootRouteOptions, RouteAddChildrenFn, RouteAddFileChildrenFn, RouteAddFileTypesFn, RouteConstraints, RouteIds, RouteLazyFn, RouteLoaderFn, RouteMask, RouteOptions, RoutePathOptionsIntersection, RouteTypes, RouteTypesById, Router, ToMaskOptions, TrimPathRight, UpdatableRouteOptions, UseNavigateResult, } from '@tanstack/router-core' import type { UseLoaderDataRoute } from './useLoaderData' import type { UseMatchRoute } from './useMatch' import type { UseLoaderDepsRoute } from './useLoaderDeps' import type { UseParamsRoute } from './useParams' import type { UseSearchRoute } from './useSearch' import type * as React from 'react' import type { NotFoundError } from './not-found' import type { UseRouteContextRoute } from './useRouteContext' declare module '@tanstack/router-core' { export interface UpdatableRouteOptionsExtensions { component?: RouteComponent errorComponent?: false | null | ErrorRouteComponent notFoundComponent?: NotFoundRouteComponent pendingComponent?: RouteComponent } export interface RouteExtensions< TId extends string, TFullPath extends string, > { useMatch: UseMatchRoute<TId> useRouteContext: UseRouteContextRoute<TId> useSearch: UseSearchRoute<TId> useParams: UseParamsRoute<TId> useLoaderDeps: UseLoaderDepsRoute<TId> useLoaderData: UseLoaderDataRoute<TId> useNavigate: () => UseNavigateResult<TFullPath> } } export function getRouteApi< const TId, TRouter extends AnyRouter = RegisteredRouter, >(id: ConstrainLiteral<TId, RouteIds<TRouter['routeTree']>>) { return new RouteApi<TId, TRouter>({ id }) } export class RouteApi<TId, TRouter extends AnyRouter = RegisteredRouter> { id: TId /** * @deprecated Use the `getRouteApi` function instead. */ constructor({ id }: { id: TId }) { this.id = id as any } useMatch: UseMatchRoute<TId> = (opts) => { return useMatch({ select: opts?.select, from: this.id, structuralSharing: opts?.structuralSharing, } as any) as any } useRouteContext: UseRouteContextRoute<TId> = (opts) => { return useMatch({ from: this.id as any, select: (d) => (opts?.select ? opts.select(d.context) : d.context), }) as any } useSearch: UseSearchRoute<TId> = (opts) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return useSearch({ select: opts?.select, structuralSharing: opts?.structuralSharing, from: this.id, } as any) as any } useParams: UseParamsRoute<TId> = (opts) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return useParams({ select: opts?.select, structuralSharing: opts?.structuralSharing, from: this.id, } as any) as any } useLoaderDeps: UseLoaderDepsRoute<TId> = (opts) => { return useLoaderDeps({ ...opts, from: this.id, strict: false } as any) } useLoaderData: UseLoaderDataRoute<TId> = (opts) => { return useLoaderData({ ...opts, from: this.id, strict: false } as any) } useNavigate = (): UseNavigateResult< RouteTypesById<TRouter, TId>['fullPath'] > => { const router = useRouter() return useNavigate({ from: router.routesById[this.id as string].fullPath }) } notFound = (opts?: NotFoundError) => { return notFound({ routeId: this.id as string, ...opts }) } } export class Route< in out TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute, in out TPath extends RouteConstraints['TPath'] = '/', in out TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath< TParentRoute, TPath >, in out TCustomId extends RouteConstraints['TCustomId'] = string, in out TId extends RouteConstraints['TId'] = ResolveId< TParentRoute, TCustomId, TPath >, in out TSearchValidator = undefined, in out TParams = ResolveParams<TPath>, in out TRouterContext = AnyContext, in out TRouteContextFn = AnyContext, in out TBeforeLoadFn = AnyContext, in out TLoaderDeps extends Record<string, any> = {}, in out TLoaderFn = undefined, in out TChildren = unknown, in out TFileRouteTypes = unknown, > implements CoreRoute< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TFileRouteTypes > { isRoot: TParentRoute extends AnyRoute ? true : false options: RouteOptions< TParentRoute, TId, TCustomId, TFullPath, TPath, TSearchValidator, TParams, TLoaderDeps, TLoaderFn, TRouterContext, TRouteContextFn, TBeforeLoadFn > // The following properties are set up in this.init() parentRoute!: TParentRoute private _id!: TId private _path!: TPath private _fullPath!: TFullPath private _to!: TrimPathRight<TFullPath> private _ssr!: boolean public get to() { /* invariant( this._to, `trying to access property 'to' on a route which is not initialized yet. Route properties are only available after 'createRouter' completed.`, )*/ return this._to } public get id() { /* invariant( this._id, `trying to access property 'id' on a route which is not initialized yet. Route properties are only available after 'createRouter' completed.`, )*/ return this._id } public get path() { /* invariant( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.isRoot || this._id || this._path, `trying to access property 'path' on a route which is not initialized yet. Route properties are only available after 'createRouter' completed.`, )*/ return this._path } public get fullPath() { /* invariant( this._fullPath, `trying to access property 'fullPath' on a route which is not initialized yet. Route properties are only available after 'createRouter' completed.`, )*/ return this._fullPath } public get ssr() { return this._ssr } // Optional children?: TChildren originalIndex?: number rank!: number lazyFn?: () => Promise<CoreLazyRoute> _lazyPromise?: Promise<void> _componentsPromise?: Promise<Array<void>> /** * @deprecated Use the `createRoute` function instead. */ constructor( options?: RouteOptions< TParentRoute, TId, TCustomId, TFullPath, TPath, TSearchValidator, TParams, TLoaderDeps, TLoaderFn, TRouterContext, TRouteContextFn, TBeforeLoadFn >, ) { this.options = (options as any) || {} this.isRoot = !options?.getParentRoute as any invariant( !((options as any)?.id && (options as any)?.path), `Route cannot have both an 'id' and a 'path' option.`, ) ;(this as any).$$typeof = Symbol.for('react.memo') } types!: RouteTypes< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TFileRouteTypes > init = (opts: { originalIndex: number; defaultSsr?: boolean }): void => { this.originalIndex = opts.originalIndex const options = this.options as | (RouteOptions< TParentRoute, TId, TCustomId, TFullPath, TPath, TSearchValidator, TParams, TLoaderDeps, TLoaderFn, TRouterContext, TRouteContextFn, TBeforeLoadFn > & RoutePathOptionsIntersection<TCustomId, TPath>) | undefined const isRoot = !options?.path && !options?.id // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.parentRoute = this.options.getParentRoute?.() if (isRoot) { this._path = rootRouteId as TPath } else { invariant( this.parentRoute, `Child Route instances must pass a 'getParentRoute: () => ParentRoute' option that returns a Route instance.`, ) } let path: undefined | string = isRoot ? rootRouteId : options.path // If the path is anything other than an index path, trim it up if (path && path !== '/') { path = trimPathLeft(path) } const customId = options?.id || path // Strip the parentId prefix from the first level of children let id = isRoot ? rootRouteId : joinPaths([ this.parentRoute.id === rootRouteId ? '' : this.parentRoute.id, customId, ]) if (path === rootRouteId) { path = '/' } if (id !== rootRouteId) { id = joinPaths(['/', id]) } const fullPath = id === rootRouteId ? '/' : joinPaths([this.parentRoute.fullPath, path]) this._path = path as TPath this._id = id as TId // this.customId = customId as TCustomId this._fullPath = fullPath as TFullPath this._to = fullPath as TrimPathRight<TFullPath> this._ssr = options?.ssr ?? opts.defaultSsr ?? true } addChildren: RouteAddChildrenFn< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TFileRouteTypes > = (children) => { return this._addFileChildren(children) as any } _addFileChildren: RouteAddFileChildrenFn< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TFileRouteTypes > = (children) => { if (Array.isArray(children)) { this.children = children as TChildren } if (typeof children === 'object' && children !== null) { this.children = Object.values(children) as TChildren } return this as any } _addFileTypes: RouteAddFileTypesFn< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren > = () => { return this as any } updateLoader = <TNewLoaderFn>(options: { loader: Constrain< TNewLoaderFn, RouteLoaderFn< TParentRoute, TCustomId, TParams, TLoaderDeps, TRouterContext, TRouteContextFn, TBeforeLoadFn > > }) => { Object.assign(this.options, options) return this as unknown as Route< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TNewLoaderFn, TChildren, TFileRouteTypes > } update = ( options: UpdatableRouteOptions< TParentRoute, TCustomId, TFullPath, TParams, TSearchValidator, TLoaderFn, TLoaderDeps, TRouterContext, TRouteContextFn, TBeforeLoadFn >, ): this => { Object.assign(this.options, options) return this } lazy: RouteLazyFn<this> = (lazyFn) => { this.lazyFn = lazyFn return this } useMatch: UseMatchRoute<TId> = (opts) => { return useMatch({ select: opts?.select, from: this.id, structuralSharing: opts?.structuralSharing, } as any) as any } useRouteContext: UseRouteContextRoute<TId> = (opts?) => { return useMatch({ ...opts, from: this.id, select: (d) => (opts?.select ? opts.select(d.context) : d.context), }) as any } useSearch: UseSearchRoute<TId> = (opts) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return useSearch({ select: opts?.select, structuralSharing: opts?.structuralSharing, from: this.id, } as any) as any } useParams: UseParamsRoute<TId> = (opts) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return useParams({ select: opts?.select, structuralSharing: opts?.structuralSharing, from: this.id, } as any) as any } useLoaderDeps: UseLoaderDepsRoute<TId> = (opts) => { return useLoaderDeps({ ...opts, from: this.id } as any) } useLoaderData: UseLoaderDataRoute<TId> = (opts) => { return useLoaderData({ ...opts, from: this.id } as any) } useNavigate = (): UseNavigateResult<TFullPath> => { return useNavigate({ from: this.fullPath }) } } export function createRoute< TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute, TPath extends RouteConstraints['TPath'] = '/', TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath< TParentRoute, TPath >, TCustomId extends RouteConstraints['TCustomId'] = string, TId extends RouteConstraints['TId'] = ResolveId< TParentRoute, TCustomId, TPath >, TSearchValidator = undefined, TParams = ResolveParams<TPath>, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, TChildren = unknown, >( options: RouteOptions< TParentRoute, TId, TCustomId, TFullPath, TPath, TSearchValidator, TParams, TLoaderDeps, TLoaderFn, AnyContext, TRouteContextFn, TBeforeLoadFn >, ): CoreRoute< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, AnyContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, unknown > { return new Route< TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, AnyContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren >(options) } export type AnyRootRoute = RootRoute<any, any, any, any, any, any, any, any> export function createRootRouteWithContext<TRouterContext extends {}>() { return < TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TSearchValidator = undefined, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, >( options?: RootRouteOptions< TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn >, ) => { return createRootRoute< TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn >(options as any) } } /** * @deprecated Use the `createRootRouteWithContext` function instead. */ export const rootRouteWithContext = createRootRouteWithContext export class RootRoute< in out TSearchValidator = undefined, in out TRouterContext = {}, in out TRouteContextFn = AnyContext, in out TBeforeLoadFn = AnyContext, in out TLoaderDeps extends Record<string, any> = {}, in out TLoaderFn = undefined, in out TChildren = unknown, in out TFileRouteTypes = unknown, > extends Route< any, // TParentRoute '/', // TPath '/', // TFullPath string, // TCustomId RootRouteId, // TId TSearchValidator, // TSearchValidator {}, // TParams TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, // TChildren TFileRouteTypes > { /** * @deprecated `RootRoute` is now an internal implementation detail. Use `createRootRoute()` instead. */ constructor( options?: RootRouteOptions< TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn >, ) { super(options as any) } } export function createRootRoute< TSearchValidator = undefined, TRouterContext = {}, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, >( options?: RootRouteOptions< TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn >, ) { return new RootRoute< TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn >(options) } export function createRouteMask< TRouteTree extends AnyRoute, TFrom extends string, TTo extends string, >( opts: { routeTree: TRouteTree } & ToMaskOptions<Router<TRouteTree, 'never', boolean>, TFrom, TTo>, ): RouteMask<TRouteTree> { return opts as any } export type ReactNode = any export type SyncRouteComponent<TProps> = | ((props: TProps) => ReactNode) | React.LazyExoticComponent<(props: TProps) => ReactNode> export type AsyncRouteComponent<TProps> = SyncRouteComponent<TProps> & { preload?: () => Promise<void> } export type RouteComponent<TProps = any> = AsyncRouteComponent<TProps> export type ErrorRouteComponent = RouteComponent<ErrorComponentProps> export type NotFoundRouteComponent = SyncRouteComponent<NotFoundRouteProps> export class NotFoundRoute< TParentRoute extends AnyRootRoute, TRouterContext = AnyContext, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TSearchValidator = undefined, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, TChildren = unknown, > extends Route< TParentRoute, '/404', '/404', '404', '404', TSearchValidator, {}, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren > { constructor( options: Omit< RouteOptions< TParentRoute, string, string, string, string, TSearchValidator, {}, TLoaderDeps, TLoaderFn, TRouterContext, TRouteContextFn, TBeforeLoadFn >, | 'caseSensitive' | 'parseParams' | 'stringifyParams' | 'path' | 'id' | 'params' >, ) { super({ ...(options as any), id: '404', }) } }