@tanstack/vue-router
Type-safe router for Vue. Prepared source: version 1.170.35 (package.json:3).
- Requires
vue >= 3.3.0(peer), Node>= 20.19(package.json:59-88) - Runtime deps:
@tanstack/router-core@1.171.32,@tanstack/history@1.162.4,@tanstack/vue-store@^0.11.0 - ESM-only (
"type": "module"), nomain/CJS entry - Entry points:
@tanstack/vue-router,@tanstack/vue-router/ssr/server,@tanstack/vue-router/ssr/client(package.json:29-50)
Docs: https://tanstack.com/router (framework pages verified at /router/latest/docs/framework/vue/...)
Hard rules
- Most composables return
Ref<T>— use.valuein script; templates auto-unwrap.useRouter(),useNavigate(),useLinkProps(),useAwaited()do NOT return refs. Full table in references/composables.md. - Never cast or annotate inferred route/router types. Types come from the route tree and the
Registerdeclaration. - Register the router for type safety — without this,
Link/useNavigate/useSearchaccept any string:
declare module '@tanstack/vue-router' {
interface Register {
router: typeof router
}
}
- Not
vue-router. Never importuseRoute/useRouterfromvue-router, never use<router-view>/<router-link>. beforeLoad/loaderare plain async functions. Vue composables (ref,computed, lifecycle hooks) cannot run inside them. Pass state through routercontext.- Route options accept Vue SFCs:
component,errorComponent,notFoundComponent,pendingComponentand thedefault*Componentrouter options take.vuefiles orh()-style function components (src/route.ts:54-58,src/router.ts:26-65).
Setup with Vite (file-based routing)
npm install @tanstack/vue-router
npm install -D @tanstack/router-plugin @vitejs/plugin-vue @vitejs/plugin-vue-jsx
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({ target: 'vue', autoCodeSplitting: true }), // before vue()
vue(),
vueJsx(), // needed for .tsx route files
],
})
Source: official Vue example, https://github.com/TanStack/router/blob/main/examples/vue/basic-file-based-sfc/vite.config.ts
// src/main.ts
import { createApp, h } from 'vue'
import { RouterProvider, createRouter } from '@tanstack/vue-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
defaultPreload: 'intent',
scrollRestoration: true,
})
declare module '@tanstack/vue-router' {
interface Register {
router: typeof router
}
}
createApp({ setup: () => () => h(RouterProvider, { router }) }).mount('#app')
File naming, split-file conventions (.route.ts, .component.vue, .lazy.ts), and the generated routeTree.gen.ts: references/file-based-routing.md.
Code-based routing
import { h } from 'vue'
import { createRootRouteWithContext, createRoute, createRouter, Outlet } from '@tanstack/vue-router'
interface RouterContext { auth: { isAuthenticated: boolean } }
const rootRoute = createRootRouteWithContext<RouterContext>()({ component: () => h(Outlet) })
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
loader: ({ context }) => fetchPosts(context.auth),
})
const routeTree = rootRoute.addChildren([postsRoute])
const router = createRouter({
routeTree,
context: { auth: { isAuthenticated: false } },
})
Route options, hooks on route objects, and property-order rules: references/routes.md.
Composables
| Composable | Returns | Notes |
|---|---|---|
useRouter() | router instance | not a ref |
useRouterState({ select }) | Ref<T> | pass select to avoid re-renders |
useNavigate({ from? }) | function | not a ref |
useSearch({ from }) | Ref<T> | throws unless from matches |
useParams({ from }) | Ref<T> | |
useMatch({ from }) | Ref<T> | |
useLoaderData({ from }) | Ref<T> | |
useLoaderDeps({ from }) | Ref<T> | |
useRouteContext({ from }) | Ref<T> | |
useLocation() | Ref<ParsedLocation> | |
useMatches() / useParentMatches() / useChildMatches() | Ref<Array<Match>> | accepts select |
useMatchRoute() | function | calling it returns Ref<false | params> (src/Matches.tsx:148-172) |
useLinkProps(options) | link props object | for custom anchors |
useBlocker({ shouldBlockFn }) | void or Ref<BlockerResolver> | resolver only with withResolver: true |
useCanGoBack() | Ref<boolean> | location.state.__TSR_index !== 0 (src/useCanGoBack.ts:4-10) |
useAwaited({ promise }) | [data, promise] tuple | for deferred data |
Route objects and getRouteApi('/path') expose the same hooks pre-scoped: Route.useSearch(), Route.useLoaderData(), Route.useNavigate(), Route.Link (src/route.ts:72-80).
Details and select/strict semantics: references/composables.md.
Components
| Component | Purpose |
|---|---|
<RouterProvider :router="router" /> | mounts the router; extra attrs update router options (src/RouterProvider.tsx:60-95) |
<Link to="..." :params="..." :search="..."> | type-safe anchor; active state sets data-status="active" + aria-current="page" (src/link.tsx:522-525); default slot receives { isActive } (src/link.tsx:918) |
<Outlet /> | renders matched child route (src/Match.tsx:275) |
<Navigate to="..." /> | declarative redirect, fires in onMounted (src/useNavigate.tsx:24-40) |
<MatchRoute to="..." :fuzzy="true"> | renders slot when matched; scoped slot receives params |
<Await :promise="p"> | deferred data with <Suspense> (src/awaited.tsx:26-43) |
<Block :should-block-fn="fn"> | navigation blocking; scoped slot receives resolver (src/useBlocker.tsx:470-491) |
<CatchBoundary> / <ErrorComponent> | error boundary via onErrorCaptured |
<ClientOnly> | renders children only after mount |
<HeadContent>, <Scripts>, <Html>, <Body>, <Asset>, <ScriptOnce> | SSR document shell |
Link options, activeProps (defaults to { class: 'active' }, src/link.tsx:477), preloading (intent/viewport/render), linkOptions(), and createLink(): references/navigation-links.md.
Data loading and redirects
const route = createFileRoute('/posts/$postId')({
beforeLoad: async ({ context, params }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { from: params.postId } })
}
},
loaderDeps: ({ search: { page } }) => ({ page }),
loader: ({ deps, params }) => fetchPost(params.postId, deps.page),
pendingComponent: PendingSpinner, // .vue file works too
})
throw redirect(), isRedirect(), deferred streaming with defer(), and context typing: references/data-loading.md.
Search params
import { z } from 'zod' // or plain validators
const route = createFileRoute('/posts')({
validateSearch: z.object({ page: z.number().default(1) }),
search: {
middlewares: [stripSearchParams({ page: 1 })],
},
})
Validators, input/output schemas, retainSearchParams/stripSearchParams, and custom serialization: references/search-params.md.
Deprecations in 1.170.x (use instead)
| Deprecated | Replacement | Proof |
|---|---|---|
Route, RootRoute, FileRoute, RouteApi classes | createRoute, createRootRoute, createFileRoute, getRouteApi | src/route.ts:95, src/route.ts:220, src/route.ts:460, src/fileRoute.ts:57 |
rootRouteWithContext | createRootRouteWithContext | src/route.ts:414 |
NotFoundRoute / routerOptions.notFoundRoute | notFoundComponent route option / defaultNotFoundComponent router option | https://tanstack.com/router/latest/docs/framework/vue/guide/not-found-errors |
<ScrollRestoration /> | scrollRestoration: true in createRouter | src/ScrollRestoration.tsx:18 |
opts.navigate in beforeLoad/loader | throw redirect({ to }) | https://tanstack.com/router/latest/docs/framework/vue/api/router/RouteOptionsType |
parseParams/stringifyParams | params.parse/params.stringify | same RouteOptionsType doc |
preSearchFilters/postSearchFilters | search.middlewares | same RouteOptionsType doc |
FileRouteLoader / separate .lazy.ts loader files | keep the loader in the route file | src/fileRoute.ts:153 |
useBlocker(fn, condition) legacy signatures | { shouldBlockFn } object | src/useBlocker.tsx:139-151 |
Best practices
- Set
defaultPreload: 'intent'on the router; preloaded data is cached (default 30 s,defaultPreloadMaxAge) — https://tanstack.com/router/latest/docs/framework/vue/guide/preloading - In
loaderDeps, select only the search params the loader uses; spreading all ofsearchre-runs the loader on every unrelated param change — https://tanstack.com/router/latest/docs/framework/vue/guide/data-loading - Use
getRouteApi('/posts/$postId')in deep components instead of importing theRouteobject (avoids circular imports) — https://tanstack.com/router/latest/docs/framework/vue/guide/data-loading - When re-throwing from
beforeLoaderror handlers, checkisRedirect(error)first so redirects are not swallowed — https://tanstack.com/router/latest/docs/framework/vue/guide/authenticated-routes - Route option order is inference-sensitive (
validateSearch/paramsbeforeloaderDeps,beforeLoadbeforeloader); thecreate-route-property-orderESLint rule is auto-fixable — https://tanstack.com/router/latest/docs/framework/vue/eslint/create-route-property-order - Prefer
linkOptions({ to, ... })over bare object literals for reusable navigation targets; works in<Link>,navigate(),redirect()— https://tanstack.com/router/latest/docs/framework/vue/guide/link-options - With
selectreturning new objects in hooks, enabledefaultStructuralSharing: trueon the router — https://tanstack.com/router/latest/docs/framework/vue/guide/render-optimizations - External links with protocols outside
protocolAllowlist(defaultDEFAULT_PROTOCOL_ALLOWLIST) are blocked to prevent XSS (src/link.tsx:543-550)
References
- references/api-surface.md — every public export, grouped
- references/composables.md — hook signatures and
Refsemantics - references/routes.md — route creation, options, route hooks
- references/navigation-links.md —
Link, navigation, blocking, preloading - references/data-loading.md — loaders, context, redirects, deferred data
- references/search-params.md — validation and middlewares
- references/file-based-routing.md — Vite plugin, naming, code splitting
- references/ssr.md — server entry points and document shell components