Remix vs TanStack Router vs React Router v7: Full-Stack Routing in 2026
Compare Remix, TanStack Router, and React Router v7 for data loading, mutations, nested layouts, and type safety. Decide which routing solution fits your React app in 2026.
Remix vs TanStack Router vs React Router v7: Full-Stack Routing in 2026
React routing has fragmented into three genuinely different philosophies, and the split matters more than it ever has. React Router v7 merged with Remix to create a full-stack framework. TanStack Router rebuilt client-side routing from scratch with end-to-end type safety as the primary design goal. Remix (now the βReact Router framework modeβ) focuses on progressive enhancement and web platform primitives.
Choosing between them isnβt a minor configuration decision β it affects how you load data, handle mutations, structure nested layouts, and think about the server-client boundary. This guide breaks down all three with real code examples, trade-offs, and a decision matrix.
The State of React Routing in 2026
The merger of Remix and React Router in late 2024 created the current landscape:
- React Router v7 β the library, usable standalone in client-only mode
- React Router framework mode β previously βRemixβ, now the opinionated full-stack layer
- TanStack Router β independent, client-side first, type-safe routing with optional server integration
All three share React as the view layer but diverge sharply on data loading, type safety, and server rendering strategy.
React Router v7: The Foundation Layer
React Router v7 is the base that the other two build on (in the case of Remix/framework mode) or compete with (TanStack Router). In standalone mode, itβs a capable client-side router with file-based routing optional.
Basic Setup
// src/main.tsx
import { createBrowserRouter, RouterProvider } from 'react-router'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorBoundary />,
children: [
{ index: true, element: <Home /> },
{
path: 'blog',
element: <BlogLayout />,
children: [
{ index: true, element: <BlogList />, loader: blogListLoader },
{ path: ':slug', element: <BlogPost />, loader: blogPostLoader },
],
},
],
},
])
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)
Data Loading with Loaders
// loaders.ts
import type { LoaderFunctionArgs } from 'react-router'
export async function blogPostLoader({ params }: LoaderFunctionArgs) {
const post = await fetchPost(params.slug!)
if (!post) throw new Response('Not Found', { status: 404 })
return { post }
}
// Component
import { useLoaderData } from 'react-router'
export function BlogPost() {
const { post } = useLoaderData<typeof blogPostLoader>()
return <article><h1>{post.title}</h1></article>
}
Mutations with Actions
import { Form, useActionData, redirect } from 'react-router'
import type { ActionFunctionArgs } from 'react-router'
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const title = formData.get('title') as string
const errors: Record<string, string> = {}
if (!title) errors.title = 'Title is required'
if (Object.keys(errors).length > 0) return { errors }
await createPost({ title })
return redirect('/blog')
}
export function NewPost() {
const actionData = useActionData<typeof action>()
return (
<Form method="post">
<input name="title" />
{actionData?.errors?.title && (
<span className="error">{actionData.errors.title}</span>
)}
<button type="submit">Create Post</button>
</Form>
)
}
React Router v7βs loader/action pattern runs on the server in SSR mode and on the client in SPA mode β the same code works both ways.
Remix (Framework Mode): Web Standards Full-Stack
Remix in framework mode (formerly Remix v2, now React Router framework mode) adds file-based routing, server rendering, and a strong emphasis on progressive enhancement via native HTML forms.
File-Based Route Structure
app/
routes/
_index.tsx β /
blog._index.tsx β /blog
blog.$slug.tsx β /blog/:slug
blog.new.tsx β /blog/new
_layout.tsx β shared layout (no URL segment)
admin/
_index.tsx β /admin
users.$id.tsx β /admin/users/:id
Nested Layouts and Outlet
// app/routes/blog.tsx β layout for all /blog routes
import { Outlet, NavLink } from 'react-router'
import { useLoaderData } from 'react-router'
export async function loader() {
const categories = await getCategories()
return { categories }
}
export default function BlogLayout() {
const { categories } = useLoaderData<typeof loader>()
return (
<div className="blog-layout">
<aside>
<nav>
{categories.map(cat => (
<NavLink
key={cat.slug}
to={`/blog/category/${cat.slug}`}
className={({ isActive }) => isActive ? 'active' : ''}
>
{cat.name}
</NavLink>
))}
</nav>
</aside>
<main>
<Outlet /> {/* Child route renders here */}
</main>
</div>
)
}
Parallel Data Loading
Remix loads all route loaders in parallel β the parent layoutβs data loads simultaneously with the child routeβs data, not sequentially:
// app/routes/blog.$slug.tsx
export async function loader({ params }: LoaderFunctionArgs) {
// This runs in parallel with the parent /blog loader
const [post, relatedPosts] = await Promise.all([
fetchPost(params.slug!),
fetchRelatedPosts(params.slug!),
])
return { post, relatedPosts }
}
Streaming with defer
For slow data, Remix can stream partial content with defer:
import { defer } from 'react-router'
import { Await, Suspense } from 'react'
export async function loader({ params }: LoaderFunctionArgs) {
return defer({
post: await fetchPost(params.slug!), // Awaited β critical content
comments: fetchComments(params.slug!), // Not awaited β streamed
})
}
export default function BlogPost() {
const { post, comments } = useLoaderData<typeof loader>()
return (
<article>
<h1>{post.title}</h1>
<Suspense fallback={<p>Loading comments...</p>}>
<Await resolve={comments}>
{(data) => <CommentList comments={data} />}
</Await>
</Suspense>
</article>
)
}
TanStack Router: End-to-End Type Safety
TanStack Router takes a fundamentally different approach: full TypeScript inference on every route, parameter, search param, and loader return value. Itβs client-first but integrates with TanStack Start for SSR.
Route Tree Setup
// src/routeTree.gen.ts (auto-generated by router devtools)
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/blog">Blog</Link>
</nav>
<Outlet />
</div>
),
})
// src/routes/blog/$slug.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blog/$slug')({
loader: async ({ params }) => {
// params.slug is typed as string β no casting
return fetchPost(params.slug)
},
component: BlogPost,
})
function BlogPost() {
// Fully typed β TypeScript knows the shape of loaderData
const post = Route.useLoaderData()
const { slug } = Route.useParams() // Typed as string
return <article><h1>{post.title}</h1></article>
}
Type-Safe Search Params
TanStack Routerβs killer feature for data-heavy apps is typed search params with validation:
import { createFileRoute } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const searchSchema = z.object({
page: z.number().min(1).default(1),
q: z.string().optional(),
tags: z.array(z.string()).optional(),
sort: z.enum(['date', 'popularity', 'title']).default('date'),
})
export const Route = createFileRoute('/blog')({
validateSearch: zodValidator(searchSchema),
loaderDeps: ({ search }) => search,
loader: async ({ deps }) => {
// deps is fully typed from searchSchema
return fetchPosts({
page: deps.page,
query: deps.q,
tags: deps.tags,
sort: deps.sort,
})
},
component: BlogList,
})
function BlogList() {
const search = Route.useSearch() // Typed as z.infer<typeof searchSchema>
const navigate = useNavigate({ from: Route.fullPath })
const posts = Route.useLoaderData()
return (
<div>
<input
value={search.q ?? ''}
onChange={(e) =>
navigate({ search: (prev) => ({ ...prev, q: e.target.value }) })
}
/>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
)
}
This is the key differentiator: URL state is fully typed and validated. No URLSearchParams parsing, no manual casting, no bugs from misspelled params.
Route Preloading
import { Link } from '@tanstack/react-router'
// Preload data on hover β 300ms head start before navigation
<Link
to="/blog/$slug"
params={{ slug: post.slug }}
preload="intent"
>
{post.title}
</Link>
// Preload immediately β for high-probability navigation paths
<Link
to="/checkout"
preload="render"
>
Checkout
</Link>
Side-by-Side Comparison
| Feature | Remix / RR Framework | TanStack Router | React Router v7 |
|---|---|---|---|
| Routing model | File-based + SSR | File-based + type-safe | Code or file-based |
| Type safety | Good (via types) | Excellent (inference) | Good (via types) |
| Search params | Manual parsing | Schema-validated, typed | Manual parsing |
| Data loading | Parallel loaders | Per-route loaders | Loaders (SSR or SPA) |
| Mutations | Form actions | Manual (no built-in) | Form actions |
| SSR | First-class | Via TanStack Start | Yes |
| Bundle size | ~40KB | ~50KB | ~25KB |
| Progressive enhance | Yes (native forms) | No | Partial |
| Pending UI | useNavigation | useRouterState | useNavigation |
Decision Matrix
Choose Remix / React Router framework mode when:
- You want a batteries-included full-stack React framework with SSR
- Progressive enhancement (works without JS) is a priority
- Your team is comfortable with web platform primitives (FormData, Request, Response)
- Youβre migrating from a traditional MPA and want to add interactivity gradually
Choose TanStack Router when:
- Type safety across URL state (params, search) is critical
- Youβre building a complex SPA or dashboard with many URL-driven states
- You want exhaustive TypeScript inference without workarounds
- You pair it with TanStack Query for server state management
Choose React Router v7 (standalone) when:
- You want a stable, well-documented client-side router without the framework opinions
- Youβre adding routing to an existing React app
- Server rendering isnβt a priority
Summary
The merger of Remix and React Router has clarified the landscape rather than unified it. Remix framework mode is the progressive-enhancement full-stack path. TanStack Router is the type-safe SPA path. React Router v7 remains the solid general-purpose foundation.
The right choice depends on where your complexity lives: in the server/mutation surface (lean toward Remix) or in the URL/client state surface (lean toward TanStack Router).
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.