Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/react-router/src/Scripts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { useRouterState } from './useRouterState'
import { useRouter } from './useRouter'
import type { RouterManagedTag } from '@tanstack/router-core'

/**
* Render body script tags collected from route matches and SSR manifests.
* Should be placed near the end of the document body.
*/
export const Scripts = () => {
const router = useRouter()
const nonce = router.options.ssr?.nonce
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/src/ScrollRestoration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function useScrollRestoration() {
}

/**
* @deprecated use createRouter's `scrollRestoration` option instead
* @deprecated Use the `scrollRestoration` router option instead.
*/
export function ScrollRestoration(_props: ScrollRestorationOptions) {
useScrollRestoration()
Expand Down
5 changes: 5 additions & 0 deletions packages/react-router/src/awaited.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type AwaitOptions<T> = {
promise: Promise<T>
}

/** Suspend until a deferred promise resolves/rejects and return its data. */
export function useAwaited<T>({
promise: _promise,
}: AwaitOptions<T>): [T, DeferredPromise<T>] {
Expand All @@ -23,6 +24,10 @@ export function useAwaited<T>({
return [promise[TSR_DEFERRED_PROMISE].data, promise]
}

/**
* Component that suspends on a deferred promise and renders its child with
* the resolved value. Optionally provides a Suspense fallback.
*/
export function Await<T>(
props: AwaitOptions<T> & {
fallback?: React.ReactNode
Expand Down
18 changes: 0 additions & 18 deletions packages/react-router/src/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,6 @@ declare module '@tanstack/router-core' {
}
}

/**
* Returns a route-specific API that exposes type-safe hooks pre-bound
* to a single route ID. Useful for consuming a route's APIs from files
* where the route object isn't directly imported (e.g. code-split files).
*
* @param id Route ID string literal for the target route.
* @returns A `RouteApi` instance bound to the given route ID.
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/getRouteApiFunction
*/
/**
* Returns a route-specific API that exposes type-safe hooks pre-bound
* to a single route ID. Useful for consuming a route's APIs from files
* where the route object isn't directly imported (e.g. code-split files).
*
* @param id Route ID string literal for the target route.
* @returns A `RouteApi` instance bound to the given route ID.
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/getRouteApiFunction
*/
/**
* Returns a route-specific API that exposes type-safe hooks pre-bound
* to a single route ID. Useful for consuming a route's APIs from files
Expand Down
10 changes: 0 additions & 10 deletions packages/react-router/src/useRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@ import warning from 'tiny-warning'
import { getRouterContext } from './routerContext'
import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core'

/**
* Access the current TanStack Router instance from React context.
* Must be used within a `RouterProvider`.
*
* Options:
* - `warn`: Log a warning if no router context is found (default: true).
*
* @returns The registered router instance.
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/useRouterHook
*/
/**
* Access the current TanStack Router instance from React context.
* Must be used within a `RouterProvider`.
Expand Down
22 changes: 22 additions & 0 deletions packages/router-core/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface Segment {
readonly hasStaticAfter?: boolean
}

/** Join path segments, cleaning duplicate slashes between parts. */
export function joinPaths(paths: Array<string | undefined>) {
return cleanPath(
paths
Expand All @@ -32,23 +33,28 @@ export function joinPaths(paths: Array<string | undefined>) {
)
}

/** Remove repeated slashes from a path string. */
export function cleanPath(path: string) {
// remove double slashes
return path.replace(/\/{2,}/g, '/')
}

/** Trim leading slashes (except preserving root '/'). */
export function trimPathLeft(path: string) {
return path === '/' ? path : path.replace(/^\/{1,}/, '')
}

/** Trim trailing slashes (except preserving root '/'). */
export function trimPathRight(path: string) {
return path === '/' ? path : path.replace(/\/{1,}$/, '')
}

/** Trim both leading and trailing slashes. */
export function trimPath(path: string) {
return trimPathRight(trimPathLeft(path))
}

/** Remove a trailing slash from value when appropriate for comparisons. */
export function removeTrailingSlash(value: string, basepath: string): string {
if (value?.endsWith('/') && value !== '/' && value !== `${basepath}/`) {
return value.slice(0, -1)
Expand Down Expand Up @@ -149,6 +155,10 @@ function segmentToString(segment: Segment): string {
return value
}

/**
* Resolve a destination path against a base, honoring trailing-slash policy
* and supporting relative segments (`.`/`..`) and absolute `to` values.
*/
export function resolvePath({
base,
to,
Expand Down Expand Up @@ -384,6 +394,13 @@ type InterPolatePathResult = {
usedParams: Record<string, unknown>
isMissingParams: boolean // true if any params were not available when being looked up in the params object
}
/**
* Interpolate params and wildcards into a route path template.
*
* - Encodes params safely (configurable allowed characters)
* - Supports `{-$optional}` segments, `{prefix{$id}suffix}` and `{$}` wildcards
* - Optionally leaves placeholders or wildcards in place
*/
export function interpolatePath({
path,
params,
Expand Down Expand Up @@ -510,6 +527,10 @@ function encodePathParam(value: string, decodeCharMap?: Map<string, string>) {
return encoded
}

/**
* Match a pathname against a route destination and return extracted params
* or `undefined`. Uses the same parsing as the router for consistency.
*/
export function matchPathname(
currentPathname: string,
matchLocation: Pick<MatchLocation, 'to' | 'fuzzy' | 'caseSensitive'>,
Expand All @@ -525,6 +546,7 @@ export function matchPathname(
return pathParams ?? {}
}

/** Low-level matcher that compares two path strings and extracts params. */
export function matchByPath(
from: string,
{
Expand Down
2 changes: 2 additions & 0 deletions packages/router-core/src/qss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* // Expected output: "token=foo&key=value"
* ```
*/
/** Encode a plain object into a URL query string using URLSearchParams. */
export function encode(
obj: Record<string, any>,
stringify: (value: any) => string = String,
Expand Down Expand Up @@ -62,6 +63,7 @@ function toValue(str: unknown) {
* // Example input: decode("token=foo&key=value")
* // Expected output: { "token": "foo", "key": "value" }
*/
/** Decode a URL query string into an object with basic type coercion. */
export function decode(str: any): any {
const searchParams = new URLSearchParams(str)

Expand Down
2 changes: 2 additions & 0 deletions packages/router-core/src/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,14 @@ export function isRedirect(obj: any): obj is AnyRedirect {
return obj instanceof Response && !!(obj as any).options
}

/** True if value is a redirect with a resolved `href` location. */
export function isResolvedRedirect(
obj: any,
): obj is AnyRedirect & { options: { href: string } } {
return isRedirect(obj) && !!obj.options.href
}

/** Parse a serialized redirect object back into a redirect Response. */
export function parseRedirect(obj: any) {
if (obj !== null && typeof obj === 'object' && obj.isSerializedRedirect) {
return redirect(obj)
Expand Down
4 changes: 4 additions & 0 deletions packages/router-core/src/rewrite.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { joinPaths, trimPath } from './path'
import type { LocationRewrite } from './router'

/** Compose multiple rewrite pairs into a single in/out rewrite. */
export function composeRewrites(rewrites: Array<LocationRewrite>) {
return {
input: ({ url }) => {
Expand All @@ -18,6 +19,7 @@ export function composeRewrites(rewrites: Array<LocationRewrite>) {
} satisfies LocationRewrite
}

/** Create a rewrite pair that strips/adds a basepath on input/output. */
export function rewriteBasepath(opts: {
basepath: string
caseSensitive?: boolean
Expand Down Expand Up @@ -54,6 +56,7 @@ export function rewriteBasepath(opts: {
} satisfies LocationRewrite
}

/** Execute a location input rewrite if provided. */
export function executeRewriteInput(
rewrite: LocationRewrite | undefined,
url: URL,
Expand All @@ -69,6 +72,7 @@ export function executeRewriteInput(
return url
}

/** Execute a location output rewrite if provided. */
export function executeRewriteOutput(
rewrite: LocationRewrite | undefined,
url: URL,
Expand Down
39 changes: 39 additions & 0 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,15 @@ export type CreateRouterFn = <
TDehydrated
>

/**
* Core, framework-agnostic router engine that powers TanStack Router.
*
* Provides navigation, matching, loading, preloading, caching and event APIs
* used by framework adapters (React/Solid). Prefer framework helpers like
* `createRouter` in app code.
*
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType
*/
export class RouterCore<
in out TRouteTree extends AnyRoute,
in out TTrailingSlashOption extends TrailingSlashOption,
Expand Down Expand Up @@ -1096,6 +1105,12 @@ export class RouterCore<
}
}

/**
* Subscribe to router lifecycle events like `onBeforeNavigate`, `onLoad`,
* `onResolved`, etc. Returns an unsubscribe function.
*
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterEventsType
*/
subscribe: SubscribeFn = (eventType, fn) => {
const listener: RouterListener<any> = {
eventType,
Expand All @@ -1117,6 +1132,10 @@ export class RouterCore<
})
}

/**
* Parse a HistoryLocation into a strongly-typed ParsedLocation using the
* current router options, rewrite rules and search parser/stringifier.
*/
parseLocation: ParseLocationFn<TRouteTree> = (
locationToParse,
previousLocation,
Expand Down Expand Up @@ -1172,6 +1191,7 @@ export class RouterCore<
return location
}

/** Resolve a path against the router basepath and trailing-slash policy. */
resolvePathWithBase = (from: string, path: string) => {
const resolvedPath = resolvePath({
base: from,
Expand Down Expand Up @@ -1538,6 +1558,13 @@ export class RouterCore<
})
}

/**
* Build the next ParsedLocation from navigation options without committing.
* Resolves `to`/`from`, params/search/hash/state, applies search validation
* and middlewares, and returns a stable, stringified location object.
*
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType#buildlocation-method
*/
buildLocation: BuildLocationFn = (opts) => {
const build = (
dest: BuildNextOptions & {
Expand Down Expand Up @@ -1785,6 +1812,10 @@ export class RouterCore<

commitLocationPromise: undefined | ControlledPromise<void>

/**
* Commit a previously built location to history (push/replace), optionally
* using view transitions and scroll restoration options.
*/
commitLocation: CommitLocationFn = ({
viewTransition,
ignoreBlocker,
Expand Down Expand Up @@ -1875,6 +1906,7 @@ export class RouterCore<
return this.commitLocationPromise
}

/** Convenience helper: build a location from options, then commit it. */
buildAndCommitLocation = ({
replace,
resetScroll,
Expand Down Expand Up @@ -1911,6 +1943,13 @@ export class RouterCore<
})
}

/**
* Imperatively navigate using standard `NavigateOptions`. When `reloadDocument`
* or an absolute `href` is provided, performs a full document navigation.
* Otherwise, builds and commits a client-side location.
*
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/NavigateOptionsType
*/
navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
if (!reloadDocument && href) {
try {
Expand Down
18 changes: 18 additions & 0 deletions packages/router-core/src/scroll-restoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function getSafeSessionStorage() {
return undefined
}

/** SessionStorage key used to persist scroll restoration state. */
export const storageKey = 'tsr-scroll-restoration-v1_3'

const throttle = (fn: (...args: Array<any>) => void, wait: number) => {
Expand Down Expand Up @@ -70,6 +71,7 @@ function createScrollRestorationCache(): ScrollRestorationCache | null {
}
}

/** In-memory handle to the persisted scroll restoration cache. */
export const scrollRestorationCache = createScrollRestorationCache()

/**
Expand All @@ -79,10 +81,14 @@ export const scrollRestorationCache = createScrollRestorationCache()
* The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.
*/

/**
* Default scroll restoration cache key: location state key or full href.
*/
export const defaultGetScrollRestorationKey = (location: ParsedLocation) => {
return location.state.__TSR_key! || location.href
}

/** Best-effort nth-child CSS selector for a given element. */
export function getCssSelector(el: any): string {
const path = []
let parent: HTMLElement
Expand All @@ -101,6 +107,9 @@ let ignoreScroll = false
// unless they are passed in as arguments. Why? Because we need to be able to
// toString() it into a script tag to execute as early as possible in the browser
// during SSR. Additionally, we also call it from within the router lifecycle
/**
* Restore scroll positions for window/elements based on cached entries.
*/
export function restoreScroll({
storageKey,
key,
Expand Down Expand Up @@ -200,6 +209,7 @@ export function restoreScroll({
ignoreScroll = false
}

/** Setup global listeners and hooks to support scroll restoration. */
export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
if (!scrollRestorationCache && !router.isServer) {
return
Expand Down Expand Up @@ -357,6 +367,14 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
})
}

/**
* @private
* Handles hash-based scrolling after navigation completes.
* To be used in framework-specific <Transitioner> components during the onResolved event.
*
* Provides hash scrolling for programmatic navigation when default browser handling is prevented.
* @param router The router instance containing current location and state
*/
/**
* @private
* Handles hash-based scrolling after navigation completes.
Expand Down
Loading
Loading