@tanstack/react-router
Version:
Modern and scalable routing for React applications
484 lines (483 loc) • 18.4 kB
JavaScript
const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
const require_utils = require("./utils.cjs");
const require_ClientOnly = require("./ClientOnly.cjs");
const require_useRouter = require("./useRouter.cjs");
let _tanstack_router_core = require("@tanstack/router-core");
let react = require("react");
react = require_runtime.__toESM(react);
let react_jsx_runtime = require("react/jsx-runtime");
let _tanstack_react_store = require("@tanstack/react-store");
let _tanstack_router_core_isServer = require("@tanstack/router-core/isServer");
let react_dom = require("react-dom");
//#region src/link.tsx
/**
* Build anchor-like props for declarative navigation and preloading.
*
* Returns stable `href`, event handlers and accessibility props derived from
* router options and active state. Used internally by `Link` and custom links.
*
* Options cover `to`, `params`, `search`, `hash`, `state`, `preload`,
* `activeProps`, `inactiveProps`, and more.
*
* @returns React anchor props suitable for `<a>` or custom components.
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/useLinkPropsHook
*/
function useLinkProps(options, forwardedRef) {
const router = require_useRouter.useRouter();
const innerRef = require_utils.useForwardedRef(forwardedRef);
const _isServer = _tanstack_router_core_isServer.isServer ?? router.isServer;
const { activeProps, inactiveProps, activeOptions, to, preload: userPreload, preloadDelay: userPreloadDelay, hashScrollIntoView, replace, startTransition, resetScroll, viewTransition, children, target, disabled, style, className, onClick, onBlur, onFocus, onMouseEnter, onMouseLeave, onTouchStart, ignoreBlocker, params: _params, search: _search, hash: _hash, state: _state, mask: _mask, reloadDocument: _reloadDocument, unsafeRelative: _unsafeRelative, from: _from, _fromLocation, ...propsSafeToSpread } = options;
if (_isServer) {
const safeInternal = isSafeInternal(to);
if (typeof to === "string" && !safeInternal && to.indexOf(":") > -1) try {
new URL(to);
if ((0, _tanstack_router_core.isDangerousProtocol)(to, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`);
return {
...propsSafeToSpread,
ref: innerRef,
href: void 0,
...children && { children },
...target && { target },
...disabled && { disabled },
...style && { style },
...className && { className }
};
}
return {
...propsSafeToSpread,
ref: innerRef,
href: to,
...children && { children },
...target && { target },
...disabled && { disabled },
...style && { style },
...className && { className }
};
} catch {}
const next = router.buildLocation({
...options,
from: options.from
});
const hrefOption = getHrefOption(next.maskedLocation ? next.maskedLocation.publicHref : next.publicHref, next.maskedLocation ? next.maskedLocation.external : next.external, router.history, disabled);
const externalLink = (() => {
if (hrefOption?.external) {
if ((0, _tanstack_router_core.isDangerousProtocol)(hrefOption.href, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${hrefOption.href}`);
return;
}
return hrefOption.href;
}
if (safeInternal) return void 0;
if (typeof to === "string" && to.indexOf(":") > -1) try {
new URL(to);
if ((0, _tanstack_router_core.isDangerousProtocol)(to, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`);
return;
}
return to;
} catch {}
})();
const isActive = (() => {
if (externalLink) return false;
const currentLocation = router.stores.location.state;
const exact = activeOptions?.exact ?? false;
if (exact) {
if (!(0, _tanstack_router_core.exactPathTest)(currentLocation.pathname, next.pathname, router.basepath)) return false;
} else {
const currentPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(currentLocation.pathname, router.basepath);
const nextPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(next.pathname, router.basepath);
if (!(currentPathSplit.startsWith(nextPathSplit) && (currentPathSplit.length === nextPathSplit.length || currentPathSplit[nextPathSplit.length] === "/"))) return false;
}
if (activeOptions?.includeSearch ?? true) {
if (currentLocation.search !== next.search) {
const currentSearchEmpty = !currentLocation.search || typeof currentLocation.search === "object" && Object.keys(currentLocation.search).length === 0;
const nextSearchEmpty = !next.search || typeof next.search === "object" && Object.keys(next.search).length === 0;
if (!(currentSearchEmpty && nextSearchEmpty)) {
if (!(0, _tanstack_router_core.deepEqual)(currentLocation.search, next.search, {
partial: !exact,
ignoreUndefined: !activeOptions?.explicitUndefined
})) return false;
}
}
}
if (activeOptions?.includeHash) return false;
return true;
})();
if (externalLink) return {
...propsSafeToSpread,
ref: innerRef,
href: externalLink,
...children && { children },
...target && { target },
...disabled && { disabled },
...style && { style },
...className && { className }
};
const resolvedActiveProps = isActive ? (0, _tanstack_router_core.functionalUpdate)(activeProps, {}) ?? STATIC_ACTIVE_OBJECT : STATIC_EMPTY_OBJECT;
const resolvedInactiveProps = isActive ? STATIC_EMPTY_OBJECT : (0, _tanstack_router_core.functionalUpdate)(inactiveProps, {}) ?? STATIC_EMPTY_OBJECT;
const resolvedStyle = (() => {
const baseStyle = style;
const activeStyle = resolvedActiveProps.style;
const inactiveStyle = resolvedInactiveProps.style;
if (!baseStyle && !activeStyle && !inactiveStyle) return;
if (baseStyle && !activeStyle && !inactiveStyle) return baseStyle;
if (!baseStyle && activeStyle && !inactiveStyle) return activeStyle;
if (!baseStyle && !activeStyle && inactiveStyle) return inactiveStyle;
return {
...baseStyle,
...activeStyle,
...inactiveStyle
};
})();
const resolvedClassName = (() => {
const baseClassName = className;
const activeClassName = resolvedActiveProps.className;
const inactiveClassName = resolvedInactiveProps.className;
if (!baseClassName && !activeClassName && !inactiveClassName) return "";
let out = "";
if (baseClassName) out = baseClassName;
if (activeClassName) out = out ? `${out} ${activeClassName}` : activeClassName;
if (inactiveClassName) out = out ? `${out} ${inactiveClassName}` : inactiveClassName;
return out;
})();
return {
...propsSafeToSpread,
...resolvedActiveProps,
...resolvedInactiveProps,
href: hrefOption?.href,
ref: innerRef,
disabled: !!disabled,
target,
...resolvedStyle && { style: resolvedStyle },
...resolvedClassName && { className: resolvedClassName },
...disabled && STATIC_DISABLED_PROPS,
...isActive && STATIC_ACTIVE_PROPS
};
}
const isHydrated = require_ClientOnly.useHydrated();
const _options = react.useMemo(() => options, [
router,
options.from,
options._fromLocation,
options.hash,
options.to,
options.search,
options.params,
options.state,
options.mask,
options.unsafeRelative
]);
const currentLocation = (0, _tanstack_react_store.useStore)(router.stores.location, (l) => l, (prev, next) => prev.href === next.href);
const next = react.useMemo(() => {
const opts = {
_fromLocation: currentLocation,
..._options
};
return router.buildLocation(opts);
}, [
router,
currentLocation,
_options
]);
const hrefOptionPublicHref = next.maskedLocation ? next.maskedLocation.publicHref : next.publicHref;
const hrefOptionExternal = next.maskedLocation ? next.maskedLocation.external : next.external;
const hrefOption = react.useMemo(() => getHrefOption(hrefOptionPublicHref, hrefOptionExternal, router.history, disabled), [
disabled,
hrefOptionExternal,
hrefOptionPublicHref,
router.history
]);
const externalLink = react.useMemo(() => {
if (hrefOption?.external) {
if ((0, _tanstack_router_core.isDangerousProtocol)(hrefOption.href, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${hrefOption.href}`);
return;
}
return hrefOption.href;
}
if (isSafeInternal(to)) return void 0;
if (typeof to !== "string" || to.indexOf(":") === -1) return void 0;
try {
new URL(to);
if ((0, _tanstack_router_core.isDangerousProtocol)(to, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`);
return;
}
return to;
} catch {}
}, [
to,
hrefOption,
router.protocolAllowlist
]);
const isActive = react.useMemo(() => {
if (externalLink) return false;
if (activeOptions?.exact) {
if (!(0, _tanstack_router_core.exactPathTest)(currentLocation.pathname, next.pathname, router.basepath)) return false;
} else {
const currentPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(currentLocation.pathname, router.basepath);
const nextPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(next.pathname, router.basepath);
if (!(currentPathSplit.startsWith(nextPathSplit) && (currentPathSplit.length === nextPathSplit.length || currentPathSplit[nextPathSplit.length] === "/"))) return false;
}
if (activeOptions?.includeSearch ?? true) {
if (!(0, _tanstack_router_core.deepEqual)(currentLocation.search, next.search, {
partial: !activeOptions?.exact,
ignoreUndefined: !activeOptions?.explicitUndefined
})) return false;
}
if (activeOptions?.includeHash) return isHydrated && currentLocation.hash === next.hash;
return true;
}, [
activeOptions?.exact,
activeOptions?.explicitUndefined,
activeOptions?.includeHash,
activeOptions?.includeSearch,
currentLocation,
externalLink,
isHydrated,
next.hash,
next.pathname,
next.search,
router.basepath
]);
const resolvedActiveProps = isActive ? (0, _tanstack_router_core.functionalUpdate)(activeProps, {}) ?? STATIC_ACTIVE_OBJECT : STATIC_EMPTY_OBJECT;
const resolvedInactiveProps = isActive ? STATIC_EMPTY_OBJECT : (0, _tanstack_router_core.functionalUpdate)(inactiveProps, {}) ?? STATIC_EMPTY_OBJECT;
const resolvedClassName = [
className,
resolvedActiveProps.className,
resolvedInactiveProps.className
].filter(Boolean).join(" ");
const resolvedStyle = (style || resolvedActiveProps.style || resolvedInactiveProps.style) && {
...style,
...resolvedActiveProps.style,
...resolvedInactiveProps.style
};
const [isTransitioning, setIsTransitioning] = react.useState(false);
const hasRenderFetched = react.useRef(false);
const preload = options.reloadDocument || externalLink ? false : userPreload ?? router.options.defaultPreload;
const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0;
const doPreload = react.useCallback(() => {
router.preloadRoute({
..._options,
_builtLocation: next
}).catch((err) => {
console.warn(err);
console.warn(_tanstack_router_core.preloadWarning);
});
}, [
router,
_options,
next
]);
require_utils.useIntersectionObserver(innerRef, react.useCallback((entry) => {
if (entry?.isIntersecting) doPreload();
}, [doPreload]), intersectionObserverOptions, { disabled: !!disabled || !(preload === "viewport") });
react.useEffect(() => {
if (hasRenderFetched.current) return;
if (!disabled && preload === "render") {
doPreload();
hasRenderFetched.current = true;
}
}, [
disabled,
doPreload,
preload
]);
const handleClick = (e) => {
const elementTarget = e.currentTarget.getAttribute("target");
const effectiveTarget = target !== void 0 ? target : elementTarget;
if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) {
e.preventDefault();
(0, react_dom.flushSync)(() => {
setIsTransitioning(true);
});
const unsub = router.subscribe("onResolved", () => {
unsub();
setIsTransitioning(false);
});
router.navigate({
..._options,
replace,
resetScroll,
hashScrollIntoView,
startTransition,
viewTransition,
ignoreBlocker
});
}
};
if (externalLink) return {
...propsSafeToSpread,
ref: innerRef,
href: externalLink,
...children && { children },
...target && { target },
...disabled && { disabled },
...style && { style },
...className && { className },
...onClick && { onClick },
...onBlur && { onBlur },
...onFocus && { onFocus },
...onMouseEnter && { onMouseEnter },
...onMouseLeave && { onMouseLeave },
...onTouchStart && { onTouchStart }
};
const enqueueIntentPreload = (e) => {
if (disabled || preload !== "intent") return;
if (!preloadDelay) {
doPreload();
return;
}
const eventTarget = e.currentTarget;
if (timeoutMap.has(eventTarget)) return;
const id = setTimeout(() => {
timeoutMap.delete(eventTarget);
doPreload();
}, preloadDelay);
timeoutMap.set(eventTarget, id);
};
const handleTouchStart = (_) => {
if (disabled || preload !== "intent") return;
doPreload();
};
const handleLeave = (e) => {
if (disabled || !preload || !preloadDelay) return;
const eventTarget = e.currentTarget;
const id = timeoutMap.get(eventTarget);
if (id) {
clearTimeout(id);
timeoutMap.delete(eventTarget);
}
};
return {
...propsSafeToSpread,
...resolvedActiveProps,
...resolvedInactiveProps,
href: hrefOption?.href,
ref: innerRef,
onClick: composeHandlers([onClick, handleClick]),
onBlur: composeHandlers([onBlur, handleLeave]),
onFocus: composeHandlers([onFocus, enqueueIntentPreload]),
onMouseEnter: composeHandlers([onMouseEnter, enqueueIntentPreload]),
onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
disabled: !!disabled,
target,
...resolvedStyle && { style: resolvedStyle },
...resolvedClassName && { className: resolvedClassName },
...disabled && STATIC_DISABLED_PROPS,
...isActive && STATIC_ACTIVE_PROPS,
...isHydrated && isTransitioning && STATIC_TRANSITIONING_PROPS
};
}
var STATIC_EMPTY_OBJECT = {};
var STATIC_ACTIVE_OBJECT = { className: "active" };
var STATIC_DISABLED_PROPS = {
role: "link",
"aria-disabled": true
};
var STATIC_ACTIVE_PROPS = {
"data-status": "active",
"aria-current": "page"
};
var STATIC_TRANSITIONING_PROPS = { "data-transitioning": "transitioning" };
var timeoutMap = /* @__PURE__ */ new WeakMap();
var intersectionObserverOptions = { rootMargin: "100px" };
var composeHandlers = (handlers) => (e) => {
for (const handler of handlers) {
if (!handler) continue;
if (e.defaultPrevented) return;
handler(e);
}
};
function getHrefOption(publicHref, external, history, disabled) {
if (disabled) return void 0;
if (external) return {
href: publicHref,
external: true
};
return {
href: history.createHref(publicHref) || "/",
external: false
};
}
function isSafeInternal(to) {
if (typeof to !== "string") return false;
const zero = to.charCodeAt(0);
if (zero === 47) return to.charCodeAt(1) !== 47;
return zero === 46;
}
/**
* Creates a typed Link-like component that preserves TanStack Router's
* navigation semantics and type-safety while delegating rendering to the
* provided host component.
*
* Useful for integrating design system anchors/buttons while keeping
* router-aware props (eg. `to`, `params`, `search`, `preload`).
*
* @param Comp The host component to render (eg. a design-system Link/Button)
* @returns A router-aware component with the same API as `Link`.
* @link https://tanstack.com/router/latest/docs/framework/react/guide/custom-link
*/
function createLink(Comp) {
return react.forwardRef(function CreatedLink(props, ref) {
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Link, {
...props,
_asChild: Comp,
ref
});
});
}
/**
* A strongly-typed anchor component for declarative navigation.
* Handles path, search, hash and state updates with optional route preloading
* and active-state styling.
*
* Props:
* - `preload`: Controls route preloading (eg. 'intent', 'render', 'viewport', true/false)
* - `preloadDelay`: Delay in ms before preloading on hover
* - `activeProps`/`inactiveProps`: Additional props merged when link is active/inactive
* - `resetScroll`/`hashScrollIntoView`: Control scroll behavior on navigation
* - `viewTransition`/`startTransition`: Use View Transitions/React transitions for navigation
* - `ignoreBlocker`: Bypass registered blockers
*
* @returns An anchor-like element that navigates without full page reloads.
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/linkComponent
*/
var Link = react.forwardRef((props, ref) => {
const { _asChild, ...rest } = props;
const { type: _type, ...linkProps } = useLinkProps(rest, ref);
const children = typeof rest.children === "function" ? rest.children({ isActive: linkProps["data-status"] === "active" }) : rest.children;
if (!_asChild) {
const { disabled: _, ...rest } = linkProps;
return react.createElement("a", rest, children);
}
return react.createElement(_asChild, linkProps, children);
});
function isCtrlEvent(e) {
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
}
/**
* Validate and reuse navigation options for `Link`, `navigate` or `redirect`.
* Accepts a literal options object and returns it typed for later spreading.
* @example
* const opts = linkOptions({ to: '/dashboard', search: { tab: 'home' } })
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/linkOptions
*/
var linkOptions = (options) => {
return options;
};
/**
* Type-check a literal object for use with `Link`, `navigate` or `redirect`.
* Use to validate and reuse navigation options across your app.
* @example
* const opts = linkOptions({ to: '/dashboard', search: { tab: 'home' } })
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/linkOptions
*/
//#endregion
exports.Link = Link;
exports.createLink = createLink;
exports.linkOptions = linkOptions;
exports.useLinkProps = useLinkProps;
//# sourceMappingURL=link.cjs.map