Skip to main content
ASoc
Comparison

React vs. Remix: A Mismatched Pairing, and the Real Question

Remix runs on React — they aren't competitors. The real comparison is Next.js vs. React Router v7, and it comes down to which way the static default points.

The ASoc Team8 min read

"React vs. Remix" is a mismatched pairing — Remix is built on React Router and renders with React underneath, so the two aren't competitors. What the search usually means is Next.js vs. Remix (now React Router v7's "Framework Mode," merged from Remix in 2025), and the axis that actually decides it is which way each framework's default points: Next.js defaults to static per route and you opt into a server; Remix/React Router defaults to server-rendered per request and you opt into static.

The decision in one table

Next.js App RouterRemix / React Router v7 (Framework Mode)
Default renderingStatic, inferred per route from what the route touchesServer-rendered per request
Getting static outputAutomatic — generateStaticParams for dynamic segments, nothing for the restOpt-in prerender config in react-router.config.ts, listing or generating the paths
Where that config livesColocated in the route file itselfCentralized, separate from the route module
Data fetchingasync Server Component reads data in the component treeloader export, colocated per route, runs server-side
MutationsA Server Action — a plain importable function, callable from any component on any routeAn action export, colocated per route, invoked via <Form> targeting that route
Mixing static and dynamic in one appPer-route, automaticSupported, but each route's mode is a config decision rather than inferred

Both frameworks reached the same conclusion — some routes should be static, some shouldn't, and a real app needs both — from opposite starting points. That convergence is the actual news for anyone picking between them today; three years ago this would have been a much starker "SSR framework vs. static-first framework" comparison, and it no longer is.

What "inferred" looks like against this build

Next.js decides a route's rendering mode by watching what the route does — call generateStaticParams, read cookies()/headers(), or set dynamic = "force-dynamic", and the framework picks accordingly, with no separate config file listing which routes are which. The static-rendering post already covers all four triggers this app hits in full; the number that matters here is what they add up to, measured fresh for this comparison:

$ node -e "const m=require('./.next/prerender-manifest.json');
  console.log(Object.keys(m.routes).length, 'prerendered routes');"
352 prerendered routes

Against 8 dynamic route patterns — /api/download, /api/webhooks/lemonsqueezy, /auth/callback, /dashboard, /dashboard/settings, /login, /reset-password, /signup — the account and commerce surface, the only part of this app that has any reason to run per request. None of that 352 required a single line of central configuration. Every product page, blog post, and category hub decided its own rendering mode from what its route file does.

React Router v7's prerender option reaches a comparable static count for a comparable app, but the decision is made once, centrally, in react-router.config.ts — either prerender: true for everything static-path-based, an explicit array of paths, or an async function computing them. That's not a worse mechanism; a single file naming every static path is easier to audit at a glance than "read every route file to know what's static," which is the real cost on the Next.js side of this row. It's a different tradeoff, not a missing feature — Remix didn't have build-time prerendering at all before it merged with React Router; it does now.

Where the colocation argument runs the other way

Remix's loader/action model has a genuine advantage this comparison shouldn't skip: both are colocated with the route they belong to, in the same file, with no separate registration step. A Next.js Server Action, by contrast, is a plain exported function that can be imported into any component on any route:

// Both imports point at the same action, called from two different places
// src/components/molecules/BuyButton.tsx
import { createCheckout } from "@/lib/actions/checkout";
// src/components/molecules/RedemptionPicker.tsx
import { redeemSlot } from "@/lib/actions/redemption";

That portability is exactly why the Server Actions vs. API Routes post makes the point it does: a Server Action compiles to a public POST endpoint regardless of which component imports it, so ownership has to come from the verified session inside the action, never from where it's called. A Remix action doesn't have this specific footgun in the same shape — it's reached only via a <Form> posting to the route that owns it, so the "which endpoint is this really" question mostly doesn't arise. The tradeoff is the flip side of the static-config row above: Remix's colocation buys clarity about where a mutation lives at the cost of it being harder to reuse the same mutation logic from an arbitrary component the way checkout.ts is reused here. SvelteKit's named form actions make the same colocation bet, which is why SvelteKit vs. Remix finds the two frameworks agreeing on the data layer and disagreeing about what reaches the browser.

Nested routing, and where the two conventions actually converge

Both frameworks settled on nested, file-based routing as the right default — a folder tree that maps to a URL tree, with each segment able to own its own layout, loading state and error boundary. Next.js's App Router expresses this as layout.tsx/page.tsx/error.tsx siblings per folder; React Router v7's Framework Mode expresses the same idea through a route config (routes.ts) that can be as flat or as nested as the app needs, with each route entry pointing at a module exporting its own loader/action/Component. Neither convention is meaningfully harder to reason about than the other once a team has picked one — this is the part of the comparison where "which is better" mostly resolves to "which one your team already knows," unlike the static-default question above, where the two frameworks make a genuinely different architectural bet.

Where they diverge again is error and loading boundaries: Next.js infers loading.tsx/error.tsx from file presence in the same folder, matching the same "convention over configuration" pattern as its static/dynamic inference; React Router v7 declares them as properties on the route config object (ErrorBoundary, HydrateFallback) rather than sibling files. Same capability, same nesting model, opposite instinct about whether behavior should live in a filename or in an explicit config value — which is the pattern running through every row of this comparison.

Common mistakes

MistakeSymptomFix
Comparing "React" to "Remix" as if they competeCategory error — Remix runs on React, the real comparison is meta-framework to meta-frameworkCompare Next.js to Remix/React Router, or React-the-library to nothing (it has no direct opponent)
Assuming Remix can't do static outputTrue before the React Router v7 merge, not true nowCheck prerender in react-router.config.ts before ruling it out
Assuming Next.js needs no config to go staticIgnores the four real triggers (cookies(), force-dynamic, missing generateStaticParams, a shared dynamic layout) that flip a route to dynamic without an explicit config lineRead the route for what it touches, not just the folder name
Importing a Server Action into a component that shouldn't be able to trigger itThe action becomes callable from more places than intended, widening the attack surface an ownership check has to coverTreat every Server Action as a public endpoint no matter where it's imported
Picking based on which one is "newer"React Router v7 (2025) absorbed Remix; "Remix" as a separate product is now legacy naming for most new projectsEvaluate React Router v7's Framework Mode directly, not old Remix documentation

Frequently asked questions

Is Remix still a separate thing from React Router? As of React Router v7, Remix's framework capabilities were merged directly into React Router itself, branded "Framework Mode." New projects generally start from React Router v7 rather than a standalone Remix install; the comparison in this post applies to both under that merged model.

Can a Remix/React Router v7 app be fully static, like a Next.js static export? Yes, via the prerender config — set it to true to prerender every static path derivable from your route tree, or pass specific paths/an async function for more control. It's a build-time operation producing static HTML plus client navigation payloads, comparable in output to Next.js's static routes, just configured centrally rather than inferred per file.

Which framework has less to learn? Depends what you already know. A team fluent in plain HTML forms and progressive enhancement tends to find Remix's loader/action/<Form> triad more direct. A team already deep in React Server Components finds Next.js's model a smaller conceptual jump, since it's the same component tree with fewer new primitives layered on top.

Does this affect hosting choice? Somewhat — Next.js's per-route static/dynamic split means a route can be served from a CDN with zero compute at request time, which the Vercel vs. GitHub Pages post covers for the no-compute-host case specifically. Remix/React Router v7's prerender output can be served the same way for the routes it covers; routes left dynamic need a running server or edge function either way.

Templates where this ships

ASoc Folio is a developer-portfolio site built on the static-by-default model this post measures. ASoc Forge is an AI resume-builder landing page on the same foundation. ASoc Frame markets an AI image generator with the identical per-route rendering split.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the four triggers behind this build's static/dynamic split, read static rendering in the App Router; for why a Server Action is a public endpoint no matter who imports it, read Server Actions vs. API Routes.

Keep reading

Comparison10 min read

React vs. Bootstrap: 74 KB of CSS for 420 Routes, Zero jQuery

React is a UI library, Bootstrap a CSS framework — the real fork is Tailwind vs. Bootstrap, measured against this site's single 74 KB stylesheet and zero jQuery dependency.

Read more
Comparison7 min read

Sass vs. Tailwind: 133 Lines and One Arbitrary Selector

Sass's four features, checked one at a time against this codebase's real @theme block, group-hover usage, and the single [&_selector] Tailwind still reaches for.

Read more