Believe it or not, every navigation within an app is relative, even if you aren't using explicit relative path syntax (../../somewhere
). Any time a link is clicked or an imperative navigation call is made, you will always have an origin path and a destination path which means you are navigating from one route to another route.
TanStack Router keeps this constant concept of relative navigation in mind for every navigation, so you'll constantly see two properties in the API:
from
- The origin route IDto
- The destination route ID⚠️ If a
from
route ID isn't provided the router will assume you are navigating from the root/
route and only auto-complete absolute paths. After all, you need to know where you are from in order to know where you're going 😉.
Every navigation and route matching API in TanStack Router uses the same core interface with minor differences depending on the API. This means that you can learn navigation and route matching once and use the same syntax and concepts across the library.
ToOptions
InterfaceThis is the core ToOptions
interface that is used in every navigation and route matching API:
type ToOptions< TRouteTree extends AnyRoute = AnyRoute, TFrom extends RoutePaths<TRouteTree> | string = string, TTo extends string = '',> = { // `from` is an optional route ID or path. If it is not supplied, only absolute paths will be auto-completed and type-safe. It's common to supply the route.fullPath of the origin route you are rendering from for convenience. If you don't know the origin route, leave this empty and work with absolute paths or unsafe relative paths. from: string // `to` can be an absolute route path or a relative path from the `from` option to a valid route path. ⚠️ Do not interpolate path params, hash or search params into the `to` options. Use the `params`, `search`, and `hash` options instead. to: string // `params` is either an object of path params to interpolate into the `to` option or a function that supplies the previous params and allows you to return new ones. This is the only way to interpolate dynamic parameters into the final URL. Depending on the `from` and `to` route, you may need to supply none, some or all of the path params. TypeScript will notify you of the required params if there are any. params: | Record<string, unknown> | ((prevParams: Record<string, unknown>) => Record<string, unknown>) // `search` is either an object of query params or a function that supplies the previous search and allows you to return new ones. Depending on the `from` and `to` route, you may need to supply none, some or all of the query params. TypeScript will notify you of the required search params if there are any. search: | Record<string, unknown> | ((prevSearch: Record<string, unknown>) => Record<string, unknown>) // `hash` is either a string or a function that supplies the previous hash and allows you to return a new one. hash?: string | ((prevHash: string) => string) // `state` is either an object of state or a function that supplies the previous state and allows you to return a new one. State is stored in the history API and can be useful for passing data between routes that you do not want to permanently store in URL search params. state?: | Record<string, any> | ((prevState: Record<string, unknown>) => Record<string, unknown>)}
🧠 Every route object has a
to
property, which can be used as theto
for any navigation or route matching API. Where possible, this will allow you to avoid plain strings and use type-safe route references instead:
function Comp() { return <Link to={aboutRoute.to}>About</Link>}
NavigateOptions
InterfaceThis is the core NavigateOptions
interface that extends ToOptions
. Any API that is actually performing a navigation will use this interface:
export type NavigateOptions< TRouteTree extends AnyRoute = AnyRoute, TFrom extends RoutePaths<TRouteTree> | string = string, TTo extends string = '',> = ToOptions<TRouteTree, TFrom, TTo> & { // `replace` is a boolean that determines whether the navigation should replace the current history entry or push a new one. replace?: boolean}
LinkOptions
InterfaceAnywhere an actual <a>
tag the LinkOptions
interface which extends NavigateOptions
will be available:
export type LinkOptions< TRouteTree extends AnyRoute = AnyRoute, TFrom extends RoutePaths<TRouteTree> | string = string, TTo extends string = '',> = NavigateOptions<TRouteTree, TFrom, TTo> & { // The standard anchor tag target attribute target?: HTMLAnchorElement['target'] // Defaults to `{ exact: false, includeHash: false }` activeOptions?: { exact?: boolean includeHash?: boolean includeSearch?: boolean explicitUndefined?: boolean } // If set, will preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there. preload?: false | 'intent' // Delay intent preloading by this many milliseconds. If the intent exits before this delay, the preload will be cancelled. preloadDelay?: number // If true, will render the link without the href attribute disabled?: boolean}
With relative navigation and all of the interfaces in mind now, let's talk about the different flavors of navigation API at your disposal:
<Link>
component
<a>
tag with a valid href
which can be click or even cmd/ctrl + clicked to open in a new tabuseNavigate()
hook
Link
component should be used for navigation, but sometimes you need to navigate imperatively as a result of a side-effect. useNavigate
returns a function that can be called to perform an immediate client-side navigation.<Navigate>
component
Router.navigate()
method
useNavigate
, it imperatively navigates, but is available everywhere you have access to your router.⚠️ None of these APIs are a replacement for server-side redirects. If you need to redirect a user immediately from one route to another before mounting your application, use a server-side redirect instead of a client-side navigation.
<Link>
ComponentThe Link
component is the most common way to navigate within an app. It renders an actual <a>
tag with a valid href
attribute which can be clicked or even cmd/ctrl + clicked to open in a new tab. It also supports any normal <a>
attributes including target
to open links in new windows, etc.
In addition to the LinkOptions
interface, the Link
component also supports the following props:
export type LinkProps< TFrom extends RoutePaths<RegisteredRouter['routeTree']> | string = string, TTo extends string = '',> = LinkOptions<RegisteredRouter['routeTree'], TFrom, TTo> & { // A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated) activeProps?: | React.AnchorHTMLAttributes<HTMLAnchorElement> | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>) // A function that returns additional props for the `inactive` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated) inactiveProps?: | React.AnchorHTMLAttributes<HTMLAnchorElement> | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)}
Let's make a simple static link!
import { Link } from '@tanstack/react-router'
const link = <Link to="/about">About</Link>
Dynamic links are links that have dynamic segments in them. For example, a link to a blog post might look like this:
const link = ( <Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post', }} > Blog Post </Link>)
Keep in mind that normally dynamic segment params are string
values, but they can also be any other type that you parse them to in your route options. Either way, the type will be checked at compile time to ensure that you are passing the correct type.
By default, all links are absolute unless a from
route path is provided. This means that the above link will always navigate to the /about
route regardless of what route you are currently on.
If you want to make a link that is relative to the current route, you can provide a from
route path:
const postIdRoute = createRoute({ path: '/blog/post/$postId',})
const link = ( <Link from={postIdRoute.fullPath} to="../categories"> Categories </Link>)
As seen above, it's common to provide the route.fullPath
as the from
route path. This is because the route.fullPath
is a reference that will update if you refactor your application. However, sometimes it's not possible to import the route directly, in which case it's fine to provide the route path directly as a string. It will still get type-checked as per usual!
Search params are a great way to provide additional context to a route. For example, you might want to provide a search query to a search page:
const link = ( <Link to="/search" search={{ query: 'tanstack', }} > Search </Link>)
It's also common to want to update a single search param without supplying any other information about the existing route. For example, you might want to update the page number of a search result:
const link = ( <Link to="." search={(prev) => ({ ...prev, page: prev.page + 1, })} > Next Page </Link>)
Search params are a highly dynamic state management mechanism, so it's important to ensure that you are passing the correct types to your search params. We'll see in a later section in detail how to validate and ensure search params typesafety, among other great features!
Hash links are a great way to link to a specific section of a page. For example, you might want to link to a specific section of a blog post:
const link = ( <Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post', }} hash="section-1" > Section 1 </Link>)
The Link
component supports two additional props: activeProps
and inactiveProps
. These props are functions that return additional props for the active
and inactive
states of the link. All props other than styles and classes passed here will override the original props passed to Link
. Any styles or classes passed are merged together.
Here's an example:
const link = ( <Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post', }} activeProps={{ style: { fontWeight: 'bold', }, }} > Section 1 </Link>)
data-status
attributeIn addition to the activeProps
and inactiveProps
props, the Link
component also adds a data-status
attribute to the rendered element when it is in an active state. This attribute will be active
or undefined
depending on the current state of the link. This can come in handy if you prefer to use data-attributes to style your links instead of props.
The Link
component comes with an activeOptions
property that offers a few options of determining if a link is active or not. The following interface describes those options:
export interface ActiveOptions { // If true, the link will be active if the current route matches the `to` route path exactly (no children routes) // Defaults to `false` exact?: boolean // If true, the link will only be active if the current URL hash matches the `hash` prop // Defaults to `false` includeHash?: boolean // Defaults to false // If true, the link will only be active if the current URL search params inclusively match the `search` prop // Defaults to `true` includeSearch?: boolean // This modifies the `includeSearch` behavior. // If true, properties in `search` that are explicitly `undefined` must NOT be present in the current URL search params for the link to be active. // defaults to `false` explicitUndefined?: boolean}
By default, it will check if the resulting pathname is a prefix of the current route. If any search params are provided, it will check that they inclusively match those in the current location. Hashes are not checked by default.
For example, if you are on the /blog/post/my-first-blog-post
route, the following links will be active:
const link1 = ( <Link to="/blog/post/$postId" params={{ postId: 'my-first-blog-post' }}> Blog Post </Link>)const link2 = <Link to="/blog/post">Blog Post</Link>const link3 = <Link to="/blog">Blog Post</Link>
However, the following links will not be active:
const link4 = ( <Link to="/blog/post/$postId" params={{ postId: 'my-second-blog-post' }}> Blog Post </Link>)
It's common for some links to only be active if they are an exact match. A good example of this would be a link to the home page. In scenarios like these, you can pass the exact: true
option:
const link = ( <Link to="/" activeOptions={{ exact: true }}> Home </Link>)
This will ensure that the link is not active when you are a child route.
A few more options to be aware of:
includeHash: true
optionincludeSearch: false
optionisActive
to childrenThe Link
component accepts a function for its children, allowing you to propagate its isActive
property to children. For example, you could style a child component based on whether the parent link is active:
const link = ( <Link to="/blog/post"> {({ isActive }) => { return ( <> <span>My Blog Post</span> <icon className={isActive ? 'active' : 'inactive'} /> </> ) }} </Link>)
The Link
component supports automatically preloading routes on intent (hovering or touchstart for now). This can be configured as a default in the router options (which we'll talk more about soon) or by passing a preload='intent'
prop to the Link
component. Here's an example:
const link = ( <Link to="/blog/post/$postId" preload="intent"> Blog Post </Link>)
With preloading enabled and relatively quick asynchronous route dependencies (if any), this simple trick can increase the perceived performance of your application with very little effort.
What's even better is that by using a cache-first library like @tanstack/query
, preloaded routes will stick around and be ready for a stale-while-revalidate experience if the user decides to navigate to the route later on.
Along with preloading is a configurable timeout which determines how long a user must hover over a link to trigger the intent-based preloading. The default timeout is 50 milliseconds, but you can change this by passing a preloadTimeout
prop to the Link
component with the number of milliseconds you'd like to wait:
const link = ( <Link to="/blog/post/$postId" preload="intent" preloadTimeout={100}> Blog Post </Link>)
useNavigate
⚠️ Because of the
Link
component's built-in affordances aroundhref
, cmd/ctrl + click-ability, and active/inactive capabilities, it's recommended to use theLink
component instead ofuseNavigate
for anything the user can interact with (e.g. links, buttons). However, there are some cases whereuseNavigate
is necessary to handle side-effect navigations (e.g. a successful async action that results in a navigation).
The useNavigate
hook returns a navigate
function that can be called to imperatively navigate. It's a great way to navigate to a route from a side-effect (e.g. a successful async action). Here's an example:
function Component() { const navigate = useNavigate({ from: '/posts/$postId' })
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault()
const response = await fetch('/posts', { method: 'POST', body: JSON.stringify({ title: 'My First Post' }), })
const { id: postId } = await response.json()
if (response.ok) { navigate({ to: '/posts/$postId', params: { postId } }) } }}
🧠 As shown above, you can pass the
from
option to specify the route to navigate from in the hook call. While this is also possible to pass in the resultingnavigate
function each time you call it, it's recommended to pass it here to reduce on potential error and also not type as much!
navigate
OptionsThe navigate
function returned by useNavigate
accepts the NavigateOptions
interface
Navigate
ComponentOccasionally, you may find yourself needing to navigate immediately when a component mounts. Your first instinct might be to reach for useNavigate
and an immediate side-effect (e.g. React.useEffect), but this is unnecessary. Instead, you can render the Navigate
component to achieve the same result:
function Component() { return <Navigate to="/posts/$postId" params={{ postId: 'my-first-post' }} />}
Think of the Navigate
component as a way to navigate to a route immediately when a component mounts. It's a great way to handle client-only redirects. It is definitely not a substitute for handling server-aware redirects responsibly on the server.
router.navigate
The router.navigate
method is the same as the navigate
function returned by useNavigate
and accepts the same NavigateOptions
interface. Unlike the useNavigate
hook, it is available anywhere your router
instance is available and is thus a great way to navigate imperatively from anywhere in your application, including outside of your framework.
useMatchRoute
and <MatchRoute>
The useMatchRoute
hook and <MatchRoute>
component are the same thing, but the hook is a bit more flexible. They both accept the standard navigation ToOptions
interface either as options or props and return true/false
if that route is currently matched. It also has a handy pending
option that will return true
if the route is currently pending (e.g. a route is currently transitioning to that route). This can be extremely useful for showing optimistic UI around where a user is navigating:
function Component() { return ( <div> <Link to="/users"> Users <MatchRoute to="/users" pending> <Spinner /> </MatchRoute> </Link> </div> )}
The component version <MatchRoute>
can also be used with a function as children to render something when the route is matched:
function Component() { return ( <div> <Link to="/users"> Users <MatchRoute to="/users" pending> {(match) => { return <Spinner show={match} /> }} </MatchRoute> </Link> </div> )}
The hook version useMatchRoute
returns a function that can be called programmatically to check if a route is matched:
function Component() { const matchRoute = useMatchRoute()
useEffect(() => { if (matchRoute({ to: '/users', pending: true })) { console.info('The /users route is matched and pending') } })
return ( <div> <Link to="/users">Users</Link> </div> )}
Phew! That's a lot of navigating! That said, hopefully you're feeling pretty good about getting around your application now. Let's move on!
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.