"Is Remix dead?" is a question that now recurs in community discussions, and the honest answer takes more than a yes or no. Remix used a server-first model built on route-level loaders and actions, and the recommended approach retains those ideas. They now live in React Router framework mode, the full-stack mode powered by the Vite build tool that replaces Remix conventions. Meanwhile, "Remix" the brand name now points at a beta project that doesn't use React at all.
In brief:
- What was planned as Remix v3 shipped as React Router v7, and the Remix team recommends React Router for projects that are new and upgrading existing Remix apps.
- Remix v2 is officially End of Life: with the release of React Router v8, it no longer receives security updates.
- The patterns that mattered carry forward into React Router 7 framework mode: server-side loaders, actions with automatic revalidation, nested routes, and progressively enhanced forms that retain basic browser functionality before JavaScript loads.
- Strapi 5 changed its REST response format: attributes are flat on the
dataobject anddocumentIdreplaces the numericid, so older Remix and Strapi code samples need updating on both sides.
What is Remix? One name, three different projects
In May 2024, the Remix team announced that what it planned to release as Remix v3 would ship instead as React Router v7. React Router v7 shipped as stable on November 22, 2024, with an official upgrade codemod from Remix v2. When React Router v8 released, the team announced the EOL of React Router v6 and Remix v2. Security updates ended for both.
Then there's Remix 3. In the "Wake up, Remix!" announcement, the team described it as a reimagining of what a web framework can be, with no critical dependencies, not even React. The team published the beta preview on April 30, 2026 with the explicit caveat that it is not production ready yet. The current beta release is v3.0.0-beta.5, published July 1, 2026.
| Project | Status | What to do |
|---|---|---|
| Remix v2 | End of Life, no security updates | Upgrade to React Router v7 via the official codemod |
| React Router v7 / v8 | Stable and maintained | Use for new React projects and migrated Remix apps |
| Remix 3 | Beta; not React-based | Experiments and prototypes only, not a Remix v2 migration target |
Here, "Remix patterns" means React Router 7 framework mode, the direct continuation of Remix v2. For the backstory on the merger, see the Remix merger overview.
Why the server-first model replaced useEffect data fetching
A common client-side React data-fetching pattern uses useState, useEffect, a loading flag, and a spinner while data loads into an initially empty view. Every new application programming interface (API) call can add another loading state to manage and another potential race condition to debug. When two components fetch the same endpoint on mount, duplicate requests can result.
The Remix model, now the React Router framework mode model, moves the fetch into a route-level loader that runs on the server. Hypertext Markup Language (HTML) arrives at the browser already populated:
// app/routes/posts.tsx
import { useLoaderData } from "react-router";
export async function loader() {
const response = await fetch("http://localhost:1337/api/posts");
const posts = await response.json();
return posts;
}
export default function Posts() {
const posts = useLoaderData<typeof loader>();
return <PostList posts={posts} />;
}Mutations follow the same shape. A route exports an action, and a <Form> posts to it using standard browser behavior. Per the React Router docs, all loader data on the page revalidates once the action completes, without you writing code to do it. No cache invalidation logic, no manual refetching. The <Form> component is a progressively enhanced form, so server-rendered pages are interactive at a basic level before JavaScript loads. That matters on slow connections and for users whose scripts fail entirely.
Nested routing rounds out the model. Parent layouts persist while child segments change, so a dashboard shell renders once and only the nested segment swaps. If you're coming from plain client-side routing, our React routing guide covers how nested routes and data loading fit together.
For First Contentful Paint (FCP), Google's web.dev guidance states that server-side rendering generally produces a fast FCP and that running rendering on the server reduces the JavaScript sent to the client, though synchronous server-side rendering (SSR) can produce a slower Time to First Byte (TTFB).
Google Search Central also recommends server-side rendering or pre-rendering because it speeds up pages for users and crawlers, and not all bots run JavaScript.
On the business side, the Farfetch case study reported that conversion rate decreases on average 1.3% with each additional 100 ms of Largest Contentful Paint (LCP).
Shopify Admin handles 67 million daily page views across 1,017 routes and reported a 30% improvement in perceived load times after adopting loader-based parallel data and code fetching. Shopify's Hydrogen storefront framework also completed a React Router upgrade after running on Remix 2. At a much smaller scale, the Strapi 5 plus React Router 7 monorepo blog build linked later in this article uses the same loader and action contracts.
React Router framework mode: how Remix patterns work now (v7 and v8)
If you learned Remix v2, the concepts transfer directly, but the APIs changed. For a new project, you can start with:
# terminal
npx create-react-router@latest my-app@latest currently installs React Router v8. The v8 changelog removed the react-router-dom package, so Document Object Model (DOM)-only APIs now import from react-router/dom.
The official installation guide uses this command. Per the Remix v2 changelog, create-remix now redirects users to create-react-router. The scaffold defaults to TypeScript, runs on Vite via the @react-router/dev/vite plugin, and serves at localhost:5173 with npm run dev.
The json() helper is gone
React Router 7 removed the deprecated json utility. Loaders can return plain objects. When you need custom status codes or headers, you can use the data() utility or native Response.json().
Routes are configured in app/routes.ts
Explicit route config replaces file conventions as the default:
// app/routes.ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("contacts", "./routes/contacts.tsx"),
] satisfies RouteConfig;Teams that prefer Remix v2's file naming can keep it with flatRoutes() from @react-router/fs-routes.
Type safety is generated, not inferred by hand
The framework generates route types in .react-router/types/, so loaders and components get typed args and props:
// app/routes/product.tsx
import type { Route } from "./+types/product";
export async function loader({ params }: Route.LoaderArgs) {
const team = await fetchTeam(params.teamId);
return { name: team.name };
}
export default function Product({ loaderData }: Route.ComponentProps) {
return <h1>{loaderData.name}</h1>;
}Error handling also moved: in framework mode, route-level ErrorBoundary exports receive a typed error prop rather than through a hook.
Deployment targets are broad. React Router provides a server build: build with react-router build and serve the output with react-router-serve. Vercel documents React Router deployment with SSR and single-page application (SPA) mode. A framework preset adds streaming. Cloudflare Workers is a supported target as well, with a documented prerendering caveat under the Cloudflare Vite plugin.
Migrating a Remix v2 app to React Router 7
Since Remix v2 no longer receives security updates, migration is required maintenance. The official upgrade guide lays out the path. The upgrade requires Node 20. It also requires React 18 and react-dom 18.
- Adopt all Remix v2 future flags in your existing app.
- Run the codemod:
npx codemod remix/2/react-router/upgrade. - Update dependencies:
@remix-run/devbecomes@react-router/dev,@remix-run/nodebecomes@react-router/node, and@remix-run/reactbecomesreact-router. - Update scripts:
remix vite:devbecomesreact-router dev, andremix vite:buildbecomesreact-router build. - Move app config into
react-router.config.tsand define routes inapp/routes.ts. - Swap the Vite plugin to
reactRouterfrom@react-router/dev/vite. - Run
react-router typegen && tscto generate types and typecheck.
Single Fetch combines loader calls into one and removes defer in favor of returning raw promises. Allow time for TypeScript path adjustments.
Remix (React Router 7) vs Next.js App Router: data and mutation models
Older comparisons framed Next.js as a juggling act between getServerSideProps, getStaticProps, and Incremental Static Regeneration (ISR). That framing is stale. Those are Pages Router APIs. The App Router has been stable since Next.js 13.4 and is now the documented default that create-next-app labels the recommended option, and data fetching happens through async Server Components using native fetch. Next.js 15 also introduced uncached fetch defaults.
React Router 7 revalidates all loader data on the page automatically after an action completes. Next.js makes cache invalidation explicit: after a Server Function mutation, you call revalidatePath or revalidateTag yourself. Neither approach is wrong. Automatic revalidation means less code and fewer stale-data bugs; explicit invalidation gives finer control over what refreshes. React Router 7 framework rendering modes support client-side rendering and SSR. Static pre-rendering is available via the prerender config. The Next.js caching model uses Server Components and streaming. Cache Components are opt-in. Our Next.js Remix comparison provides a feature-by-feature breakdown.
Remix vs Create React App is no longer a useful comparison. The React team sunset Create React App on February 14, 2025, and its recommended React frameworks for new projects are Next.js, React Router v7, and Expo.
Build a contact app with React Router 7 and Strapi 5
A small create-and-list app shows the whole loop: Strapi as the headless CMS backend, React Router 7 handling reads and writes through a single route module.
Set up the Strapi 5 backend
You can scaffold a project with npx create-strapi@latest using the quick start, which defaults to SQLite so there's no database configuration. Python is a prerequisite when using SQLite; see the database configuration docs.
In the development environment, open the Content-Type Builder in the Admin Panel and create a Contact Collection Type with name, email, and phone fields. Leave Draft and Publish disabled for this Contact Collection Type so contacts created through the API are immediately published. Strapi generates REST endpoints for the Content-Type on save.
Content-Types are private by default, so making those contacts available through unauthenticated GET requests requires opening Settings > Users and Permissions Plugin > Roles > Public and ticking the checkboxes for the find action.
Keep the Contact create permission private. Generate a Full access token or a Custom API token with that permission under Settings > Global settings > API Tokens, then send it in a Bearer authorization header from your server-side action. If you're planning a larger schema than three fields, these content modeling practices will save you a restructuring pass later.
Strapi 5 introduced a flattened REST format that places fields directly on data and makes a string documentId the canonical identifier. This format replaces the data.attributes wrapper and numeric id used in older tutorials:
// example response: GET /api/contacts
{
"data": [
{
"id": 2,
"documentId": "hgv1vny5cebq2l3czil1rpb3",
"Name": "BMK Paris Bamako",
"createdAt": "2024-03-06T13:42:05.098Z",
"publishedAt": "2024-03-06T13:42:05.103Z",
"locale": "en"
}
],
"meta": {
"pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 2 }
}
}Write the route module
One file handles the list, the form, and the mutation:
// app/routes/contacts.tsx
import { redirect, Form, useLoaderData } from "react-router";
import type { Route } from "./+types/contacts";
export async function loader() {
const res = await fetch(`${process.env.STRAPI_URL}/api/contacts`);
if (!res.ok) {
throw new Response("Failed to load contacts", { status: res.status });
}
const { data } = await res.json();
return { contacts: data };
}
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const res = await fetch(`${process.env.STRAPI_URL}/api/contacts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
},
body: JSON.stringify({ data: Object.fromEntries(formData) }),
});
if (!res.ok) {
throw new Response("Failed to create contact", { status: res.status });
}
return redirect("/contacts");
}
export default function Contacts() {
const { contacts } = useLoaderData<typeof loader>();
return (
<div>
<h1>Contacts</h1>
<Form method="post">
<input name="name" placeholder="Name" required />
<input name="email" placeholder="Email" type="email" required />
<input name="phone" placeholder="Phone" />
<button type="submit">Add Contact</button>
</Form>
<ul>
{contacts.map((contact) => (
<li key={contact.documentId}>
<strong>{contact.name}</strong> ({contact.email})
</li>
))}
</ul>
</div>
);
}When a user hits /contacts, the loader runs on the server, fetches from Strapi, and the browser receives HTML with the list already rendered.
Submitting the form performs a standard POST, the action forwards it to Strapi's POST /api/contacts with field values inside a data object, and a successful response triggers the redirect and re-runs the loader.
Failed Strapi responses instead reach the route error boundary. For this route's initial server-rendered data flow, there's no client-side loading spinner or store and no manual cache invalidation.
The API token never reaches the browser because loader code is removed from client bundles and route actions run server-side.
Each piece of the stack owns a distinct job:
- Strapi manages content models, configured validation rules, permissions, and Content API access, while the configured database persists the content records.
- The React Router app owns rendering and routing.
- The React Router application or hosting layer emits cache headers like
stale-while-revalidate, and your content delivery network (CDN) applies them to cache responses at the edge. Under the Request for Comments (RFC) system, RFC 5861 definesstale-while-revalidateas letting a cache serve a stale response while it revalidates in the background.
For a longer walkthrough of this pattern, the Strapi contact app tutorial goes step by step, and the React-Strapi monorepo blog shows React Router 7 framework mode and Strapi 5 in a monorepo with search engine optimization (SEO) metadata generated through the meta function.
Next steps: Ship a React Router 7 and Strapi 5 project
Should you wait for Remix 3? Probably not, if you're building in React. Remix 3 abandons React entirely and builds its own component model, so it isn't an upgrade path for existing apps. The official merger announcement recommends React Router v7 for new projects and existing Remix apps. The beta is explicitly for experiments, demos, and prototypes. You can watch it if you're curious about post-React web frameworks. For production React apps, React Router 7 is the maintained path.
Start a small project with npx create-react-router@latest and scaffold a Strapi 5 backend with the quick start. Then connect a loader to the REST API and use a route action for form submissions. The route returns pre-rendered HTML for its initial data, while successful actions trigger automatic revalidation and progressively enhanced forms retain basic browser functionality before JavaScript loads.






