Skip to main content

TanStack Start vs Remix: Full-Stack React in 2026

·PkgPulse Team

TL;DR

Remix is production-ready with 3+ years of real-world usage, a strong mental model around web fundamentals, and now backed by Shopify. TanStack Start entered stable release in late 2024, bringing the TanStack ecosystem (Query, Router, Table) into a full-stack framework with a familiar file-based routing API and Vinxi as the underlying server toolkit. Remix wins today for production readiness and documentation depth. TanStack Start wins if you're already deep in the TanStack ecosystem and want a cohesive full-stack story built on top of it.

Key Takeaways

  • Remix v2: Production battle-tested, Shopify-backed, excellent web fundamentals model
  • TanStack Start: Built on TanStack Router (type-safe routing) + Vinxi (Vite-based server toolkit), reached stable in 2024
  • Both use server functions / actions for form handling and data mutations
  • Both support streaming and React Server Components (with different maturity levels)
  • Remix uses form-first data model (loaders + actions); TanStack Start uses server functions + TanStack Query
  • Deployment: both target Node.js, edge runtimes (CF Workers, Vercel Edge), and static

The Landscape in 2026

The full-stack React space has settled around a few dominant paradigms:

  • Next.js: React Server Components + App Router, Vercel-optimized
  • Remix: Web fundamentals (forms, HTTP, progressive enhancement), Shopify-backed
  • TanStack Start: Type-safe routing + TanStack ecosystem, Vite-based

All three are viable production choices. The choice increasingly comes down to mental model fit, not technical capability.


Remix: The Web Fundamentals Framework

Remix was created by Ryan Florence and Michael Jackson (creators of React Router). Its core philosophy: leverage the platform. HTTP, forms, progressive enhancement — things that work without JavaScript first, enhanced with React when available.

Data Loading (Loaders)

// app/routes/packages.$name.tsx
import { LoaderFunctionArgs, json } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"

// Server-only — runs before component renders
export async function loader({ params }: LoaderFunctionArgs) {
  const pkg = await fetchPackageData(params.name)

  if (!pkg) {
    throw new Response("Not Found", { status: 404 })
  }

  return json({ pkg })
}

// Client component — receives loader data typed automatically
export default function PackagePage() {
  const { pkg } = useLoaderData<typeof loader>()

  return (
    <div>
      <h1>{pkg.name}</h1>
      <p>{pkg.description}</p>
    </div>
  )
}

Mutations (Actions)

// Remix actions handle form submissions server-side
export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData()
  const packageName = formData.get("package") as string

  await addToWatchlist(packageName)
  return redirect(`/packages/${packageName}`)
}

// The form works without JavaScript — true progressive enhancement
export default function WatchlistForm() {
  const actionData = useActionData<typeof action>()

  return (
    <Form method="post">
      <input name="package" placeholder="Package name" />
      <button type="submit">Watch</button>
      {actionData?.error && <p>{actionData.error}</p>}
    </Form>
  )
}

Parallel Data Loading

// Nested routes enable parallel data loading — no request waterfalls:
// routes/_layout.tsx — loads user data
// routes/_layout.packages.tsx — loads packages data (parallel with user)
// routes/_layout.packages.$name.tsx — loads specific package (after params known)

// Remix de-duplicates requests to the same loader across route segments

Remix strengths:

  1. Web fundamentals: Forms, HTTP semantics, progressive enhancement baked in
  2. Nested routing with parallel loading: Each route segment has its own loader
  3. Error boundaries per route: Partial failure without full page crash
  4. Production battle-tested: Used by Shopify, Vercel, major enterprises
  5. React Router integration: Remix v2 is built on React Router v6; v3 is merging with RR v7

TanStack Start

TanStack Start is the full-stack answer to TanStack Router — adding server functions, SSR, and deployment targets on top of the type-safe file-based router.

Type-Safe Routing

TanStack Router's defining feature is full TypeScript inference — route params, search params, and loader data are all typed from the route definition:

// routes/packages/$name.tsx
import { createFileRoute } from "@tanstack/react-router"
import { fetchPackage } from "~/server/packages"

export const Route = createFileRoute("/packages/$name")({
  // Fully typed loader — $name is inferred as string
  loader: async ({ params }) => {
    return fetchPackage(params.name)  // params.name is string, not string | undefined
  },
  component: PackagePage,
})

function PackagePage() {
  // useLoaderData is fully typed — no manual type annotation needed
  const pkg = Route.useLoaderData()
  const { name } = Route.useParams()  // TypeScript knows this is string

  return <div>{pkg.description}</div>
}

Server Functions

TanStack Start uses createServerFn for server-side logic — more explicit than Remix loaders but equally type-safe:

import { createServerFn } from "@tanstack/start"
import { z } from "zod"

// Server function — runs only on server, callable from client
const getPackage = createServerFn()
  .validator(z.object({ name: z.string() }))
  .handler(async ({ data }) => {
    return db.package.findUnique({ where: { name: data.name } })
  })

// Call from component (RPC-style):
export const Route = createFileRoute("/packages/$name")({
  loader: ({ params }) => getPackage({ data: { name: params.name } }),
})

// Or call from event handler:
function WatchButton({ name }: { name: string }) {
  return (
    <button onClick={() => addToWatchlist({ data: { name } })}>
      Watch
    </button>
  )
}

TanStack Query Integration

The killer integration: TanStack Start's server functions pair naturally with TanStack Query for client-side caching, background refetch, and optimistic updates:

import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"
import { createServerFn } from "@tanstack/start"

const getPackageData = createServerFn()
  .validator(z.string())
  .handler(async ({ data: name }) => fetchFromNpm(name))

// Define query with server function as fetcher
const packageQueryOptions = (name: string) =>
  queryOptions({
    queryKey: ["package", name],
    queryFn: () => getPackageData({ data: name }),
    staleTime: 5 * 60 * 1000,
  })

export const Route = createFileRoute("/packages/$name")({
  // Prefetch on server
  loader: ({ params, context }) =>
    context.queryClient.ensureQueryData(packageQueryOptions(params.name)),

  component: function PackagePage() {
    const { name } = Route.useParams()
    // Client-side cache — reuses prefetched data
    const { data } = useSuspenseQuery(packageQueryOptions(name))

    return <PackageDetail package={data} />
  },
})

This is TanStack Start's most compelling feature: server-side prefetch + client-side cache management, completely type-safe, with no duplicate fetch code.


Feature Comparison

FeatureRemixTanStack Start
RoutingFile-based (React Router v7)File-based (TanStack Router)
Type-safe route params⚠️ Partial✅ Full inference
Data loading modelLoaders per routeServer functions + Query
MutationsActions + FormServer functions
TanStack Query integration❌ Manual✅ First-class
Progressive enhancement✅ Forms work without JS⚠️ Client-first
Nested layouts
Streaming (Suspense)
React Server Components⚠️ Planned⚠️ Experimental
Edge deployment✅ (CF Workers, Deno)✅ (via Vinxi)
SSG/static export⚠️ Limited✅ Vinxi
Deployment adaptersNode, CF, Vercel, DenoNode, CF, Vercel, Netlify
Production maturity✅ 3+ years✅ Stable since 2024
Documentation✅ Excellent✅ Good
BackingShopifyTanStack/independent

Routing Model Comparison

Remix uses React Router v7's file conventions:

app/routes/
├── _index.tsx              → /
├── packages._index.tsx     → /packages
├── packages.$name.tsx      → /packages/:name
├── packages.$name.stats.tsx → /packages/:name/stats

TanStack Start uses TanStack Router's file conventions:

src/routes/
├── __root.tsx              → root layout
├── index.tsx               → /
├── packages/
│   ├── index.tsx           → /packages
│   ├── $name.tsx           → /packages/$name
│   └── $name/
│       └── stats.tsx       → /packages/$name/stats

Both support nested layouts, parallel loading, and code splitting per route. The main difference: TanStack Router emits a routeTree.gen.ts file with full type information for every route in your app.


Form Handling Comparison

// Remix — HTML form, progressive enhancement:
export async function action({ request }: ActionFunctionArgs) {
  const form = await request.formData()
  await createPackageAlert(form.get("name"), form.get("threshold"))
  return redirect("/alerts")
}

export default function CreateAlert() {
  return (
    <Form method="post">
      <input name="name" />
      <input name="threshold" type="number" />
      <button>Create Alert</button>
    </Form>
  )
}

// TanStack Start — server function + React state:
const createAlert = createServerFn()
  .validator(z.object({ name: z.string(), threshold: z.number() }))
  .handler(async ({ data }) => {
    await db.alert.create({ data })
    return { success: true }
  })

export default function CreateAlert() {
  const [name, setName] = useState("")
  const [threshold, setThreshold] = useState(0)
  const navigate = useNavigate()

  return (
    <form onSubmit={async (e) => {
      e.preventDefault()
      await createAlert({ data: { name, threshold } })
      navigate({ to: "/alerts" })
    }}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={threshold} onChange={e => setThreshold(+e.target.value)} type="number" />
      <button>Create Alert</button>
    </form>
  )
}

Remix's form model works without JavaScript — the server action handles the <form> POST directly. TanStack Start's approach is more familiar to SPA developers but requires JavaScript.


Migration Path

Next.js App Router → Remix

The routing model is similar (file-based, nested layouts). The biggest shift: Remix uses loaders/actions instead of Server Actions and use server.

Next.js Pages Router → TanStack Start

More similar mental model — both use client-first data fetching. The upgrade is type-safe routing and server functions replacing API routes.

Create React App / Vite SPA → TanStack Start

Most natural migration: add SSR to an existing TanStack Query + React Router app progressively.


When to Use Each

Choose Remix if:

  • Web fundamentals matter: progressive enhancement, semantic HTTP, form-based mutations
  • Shopify backing and long-term stability are priorities
  • Your team knows React Router (smooth learning curve)
  • You're migrating from Rails/Django and want the "server renders everything" model

Choose TanStack Start if:

  • You're already using TanStack Query — the integration is seamless
  • Type-safe routing (params, search params, loader data) is a priority
  • You prefer the RPC-style server function model over form-action model
  • You want the best-in-class table, query, and router from one ecosystem

Methodology

Framework comparison based on official documentation, GitHub repositories, and community benchmarks. Download data from npm registry (February 2026). Framework capabilities reflect stable releases (Remix v2.x, TanStack Start 1.x).

Explore full-stack framework comparisons on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.