@tanstack/react-router
Version:
Modern and scalable routing for React applications
791 lines (724 loc) • 18.7 kB
text/typescript
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',
})
}
}