You're Leaving Next.js. Don't Trade One JS Backend for Another.
Where TanStack actually fits with React on Rails — and where it quietly replaces your backend instead of your framework.
For most of the last decade, "full-stack React" meant Next.js. That's breaking. Lovable made TanStack Start its default for new projects this year. Inngest migrated their dashboard off Next.js and cut local dev load times by 83%. The reasons people give — type-safe routing, code that runs on server and client by default, server logic that lives next to the components instead of behind a separate URL, fewer network round-trips with TanStack Query — are real wins.
They're also, almost word for word, the critique we've been making of Next.js for years. So the exodus isn't a threat to React on Rails. It's the market arriving at our starting premise: the meta-framework was the problem. The only open question is where teams land next.
"TanStack" is two products, not one
Most of the confusion in this conversation comes from treating TanStack as a single thing. It isn't, and the distinction decides everything:
- TanStack Query / Router / Table are client libraries. They're backend-agnostic, and Query in particular is the single best companion to a Rails JSON API we know of. These complement React on Rails.
- TanStack Start is a full-stack framework: SSR, streaming, and server functions. Its pitch is that your server code lives next to your components instead of "off on a separate backend URL." That's the Next.js model reinvented — and it means your business logic migrates into TypeScript functions. For a team that has, or wants, Rails, that's the one thing you don't want.
So when someone says "we're adopting TanStack," the only question that matters is which part. The enthusiasm is mostly Query. The thing that competes with your backend is Start.
The fork in the road
Everyone leaving Next.js hits the same fork:
- Path A — swap meta-frameworks. Trade Next.js for TanStack Start. Stay all-JS, keep your backend in TypeScript server functions. You've changed vendors, not architecture.
- Path B — keep a real backend. Realize you never wanted a JS backend, and keep Rails for business logic, persistence, auth, and jobs — with React as the view layer and TanStack Query on the client.
TanStack Start is loudly evangelizing Path A. Nobody is evangelizing Path B but us. That's not a crowded fight — it's open space the exodus just created.
The reference architecture
You don't have to imagine this — it's the official React on Rails + TanStack starter, a deployable Rails 8 + RoR Pro app. The division of responsibility is clean.
Rails owns data, logic, auth, and persistence. Api::ProjectsController does the filtering, sorting, and pagination and returns explicit JSON — never raw Active Record:
# app/controllers/api/projects_controller.rb — Rails owns filter/sort/paginate
def index
projects = scoped_projects
total = projects.count
projects = projects.limit(per_page).offset((page - 1) * per_page)
render json: {
projects: projects.map { |project| project_json(project) },
meta: { page: page, per_page: per_page, total: total }
}
endReact on Rails Pro server-renders the TanStack shell and hydrates it. The Rails view mounts the app with SSR on (react_component("DashboardApp", prerender: !Rails.env.test?)); the render function server-renders through the Pro TanStack integration and hands the client the dehydrated router state to hydrate:
// DashboardApp.tsx — the React on Rails Pro ↔ TanStack Router SSR boundary
if (railsContext.serverSide) {
return serverRenderTanStackAppAsync(
tanStackRouterOptions, props, railsContext,
DashboardRouterProvider, createMemoryHistory,
).then(({ appElement, dehydratedState }) => ({
renderedHtml: appElement,
clientProps: { __tanstackRouterDehydratedState: dehydratedState }, // router state, rehydrated client-side
}));
}TanStack Query owns the client-side data lifecycle. Each panel reads server state with a stable, URL-synced key and talks to your Rails API through one CSRF-aware helper:
// ProjectsTable — every server-side input lives in the query key
const params = new URLSearchParams({ status, sort, dir, page: String(page) });
const projectsQuery = useQuery({
queryKey: ['projects', status, sort, dir, page],
queryFn: () => apiFetch<ProjectsResponse>(`${api.projectsPath}?${params}`),
initialData, // the Rails-seeded first page (see "What's server-rendered" below)
});// apiFetch.ts — CSRF + same-origin, in exactly one place
const csrfToken = getCsrfToken();
if (csrfToken) headers.set('X-CSRF-Token', csrfToken);Mutations write through the same helper and invalidate the affected keys, so the table and metrics refresh once. TanStack Table renders the rows; TanStack Router gives you type-safe deep links (/projects/$projectId); Rails stays the source of truth.
What's server-rendered, precisely: the shell, the matched route, and the projects table's first page all land in the initial HTML. DashboardController#show passes the first page as props, and ProjectsTable adopts it via useQuery({ initialData }) when the key matches — so the rows are server-rendered with no spinner, and TanStack Query owns freshness and refetch from there. (Other panels like metrics hydrate and fetch client-side; seed them the same way when you want them in first paint too.) A deep link like /projects/123 lands correctly with no route flash. No JS backend in sight.
RSC: matching the "server functions" DX — on Rails
The strongest line in the TanStack Start pitch is colocation: server code next to the component, no separate API endpoint, no serializer. Our answer isn't a TypeScript server layer — it's React Server Components in React on Rails Pro.
With RSC, a Server Component renders on the server using data Rails hands it (async props from a Rails controller), streams HTML, and ships no client JS for that part — the colocation and zero-round-trip win, without moving your data layer into TypeScript. Rails keeps owning the data and mutations — for a Rails team that's a feature, not a gap — and you drop to TanStack Query only for the interactive islands that mutate. It's a Pro capability that runs in the node renderer, and the RSC feature set is still expanding (no server-action shorthand yet, for instance) — but the core ships today: Server Components with Flight streaming, and SSR + hydration with no double-fetch. We map it concept-for-concept against Next.js's App Router in RoR Pro vs Next.js: RSC architectures compared.
When you should just use TanStack Start
Credibility means saying it: if you're greenfield, have no Rails investment, want one language end to end, and you're a small team optimizing for raw velocity — TanStack Start is a genuinely good choice, and React on Rails isn't the pitch. Our story is for teams who have Rails or want it: established products, real business logic, teams that value Rails' productivity for everything that isn't the view.
For those teams, the TanStack wave isn't pulling you toward Start. It's confirming that the meta-framework was never the point.
Bottom line
Adopt TanStack Query, Router, and Table with enthusiasm — they make a React on Rails app better. Skip TanStack Start. Keep Rails. You get every benefit teams are chasing when they leave Next.js — fewer round-trips, server-rendered first paint, colocated server logic, type-safe routing, streaming, even RSC — without rewriting your backend into someone else's framework.
Want the full working app? Clone the official React on Rails + TanStack starter — a deployable Rails 8 + RoR Pro app that demonstrates every pattern above: shared Query defaults, CSRF JSON fetch, a Rails JSON API, TanStack Router + Table, RoR Pro SSR with dehydrate/hydrate, and an RSC-in-a-Router-loader showcase. Or try it live at starter.reactonrails.com.
Closing Remark


