Loading...
Loading...
React bindings for TanStack Router: RouterProvider, useRouter, useRouterState, useMatch, useMatches, useLocation, useSearch, useParams, useNavigate, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack, Link, Navigate, Outlet, CatchBoundary, ErrorComponent. React-specific patterns for hooks, providers, SSR hydration, and createLink with forwardRef.
npx skill4agent add tanstack/router react-router@tanstack/react-routerCRITICAL: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. CRITICAL: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server. CRITICAL: Do not confusewith@tanstack/react-router/react-router-dom. They are completely different libraries with different APIs.react-router
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/react-router-devtools// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
// MUST come before react()
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
}),
react(),
],
})// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
export const Route = createRootRoute({
component: RootLayout,
})
function RootLayout() {
return (
<>
<nav>
<Link to="/" className="[&.active]:font-bold">
Home
</Link>
<Link to="/about" className="[&.active]:font-bold">
About
</Link>
</nav>
<hr />
<Outlet />
<TanStackRouterDevtools />
</>
)
}// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: HomePage,
})
function HomePage() {
return <h1>Welcome Home</h1>
}// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutPage,
})
function AboutPage() {
return <h1>About</h1>
}// src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({ routeTree })
// REQUIRED — without this, Link/useNavigate/useSearch have no type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
}@tanstack/react-routeruseRouter()import { useRouter } from '@tanstack/react-router'
function InvalidateButton() {
const router = useRouter()
return <button onClick={() => router.invalidate()}>Refresh data</button>
}useRouterState()useMatchesuseLocationimport { useRouterState } from '@tanstack/react-router'
function LoadingIndicator() {
const isLoading = useRouterState({ select: (s) => s.isLoading })
return isLoading ? <div>Loading...</div> : null
}useNavigate()<Link>import { useNavigate } from '@tanstack/react-router'
function AfterSubmit() {
const navigate = useNavigate()
const handleSubmit = async () => {
await saveData()
navigate({ to: '/posts/$postId', params: { postId: '123' } })
}
return <button onClick={handleSubmit}>Save</button>
}useSearch({ from })import { useSearch } from '@tanstack/react-router'
function Pagination() {
const { page } = useSearch({ from: '/products' })
return <span>Page {page}</span>
}useParams({ from })import { useParams } from '@tanstack/react-router'
function PostHeader() {
const { postId } = useParams({ from: '/posts/$postId' })
return <h2>Post {postId}</h2>
}useLoaderData({ from })import { useLoaderData } from '@tanstack/react-router'
function PostContent() {
const { post } = useLoaderData({ from: '/posts/$postId' })
return <article>{post.content}</article>
}useMatch({ from })import { useMatch } from '@tanstack/react-router'
function PostDetails() {
const match = useMatch({ from: '/posts/$postId' })
return <div>{match.loaderData.post.title}</div>
}@tanstack/react-routeruseMatches()useRouteContext({ from })beforeLoaduseBlocker({ shouldBlockFn })useCanGoBack()booleanuseLocation()pathnamesearchhashuseLinkProps({ to, params?, search? })<a>useMatchRoute()matchRoute({ to }) => match | falseRouterProvider<RouterProvider router={router} />Link<a><Link to="/posts/$postId" params={{ postId: '42' }}>
View Post
</Link>Outletfunction Layout() {
return (
<div>
<Sidebar />
<main>
<Outlet />
</main>
</div>
)
}Navigateimport { Navigate } from '@tanstack/react-router'
function OldPage() {
return <Navigate to="/new-page" />
}Awaitimport { Await } from '@tanstack/react-router'
import { Suspense } from 'react'
function PostWithComments() {
const { deferredComments } = Route.useLoaderData()
return (
<div>
<h1>Post</h1>
<Suspense fallback={<div>Loading comments...</div>}>
<Await promise={deferredComments}>
{(comments) => (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
)}
</Await>
</Suspense>
</div>
)
}CatchBoundaryerrorComponentimport { CatchBoundary } from '@tanstack/react-router'
;<CatchBoundary
getResetKey={() => 'widget'}
onCatch={(error) => console.error(error)}
errorComponent={({ error }) => <div>Error: {error.message}</div>}
>
<RiskyWidget />
</CatchBoundary>createLinkLinkimport { createLink } from '@tanstack/react-router'
import { forwardRef, type ComponentPropsWithoutRef } from 'react'
const StyledLinkComponent = forwardRef<
HTMLAnchorElement,
ComponentPropsWithoutRef<'a'>
>((props, ref) => (
<a ref={ref} {...props} className={`styled-link ${props.className ?? ''}`} />
))
const StyledLink = createLink(StyledLinkComponent)
// Usage — same type-safe props as Link
function Nav() {
return (
<StyledLink to="/posts/$postId" params={{ postId: '42' }}>
Post
</StyledLink>
)
}fromfunction PostIdDisplay({ from }: { from: '/posts/$id' | '/drafts/$id' }) {
const { id } = useParams({ from })
return <span>ID: {id}</span>
}
// Usage in different route components
<PostIdDisplay from="/posts/$id" />
<PostIdDisplay from="/drafts/$id" />strict: falsecreateRootRouteWithContextRouterProvider// CORRECT — AuthProvider wraps RouterProvider
function App() {
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
)
}
// WRONG — RouterProvider outside auth provider
function App() {
return (
<RouterProvider router={router}>
<AuthProvider>{/* ... */}</AuthProvider>
</RouterProvider>
)
}Wrapconst router = createRouter({
routeTree,
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})beforeLoadloaderbeforeLoadloader// WRONG — useAuth is a React hook, cannot be called here
beforeLoad: () => {
const auth = useAuth()
if (!auth.user) throw redirect({ to: '/login' })
}
// CORRECT — read auth from router context
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
}undefined!RouterProvidercontext// CORRECT — create router once, inject live auth via context prop
const router = createRouter({
routeTree,
context: { auth: undefined! }, // placeholder, filled by RouterProvider
})
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}AwaitAwait<Suspense>// WRONG — no Suspense boundary
<Await promise={deferredData}>{(data) => <div>{data}</div>}</Await>
// CORRECT — wrap in Suspense
<Suspense fallback={<div>Loading...</div>}>
<Await promise={deferredData}>{(data) => <div>{data}</div>}</Await>
</Suspense>