✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Ecosystem●11 min read

Remix Explained: The Full-Stack React Framework

●September 3, 2025●Updated on September 29, 2026
Remix Explained

"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 data object and documentId replaces the numeric id, 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.

ProjectStatusWhat to do
Remix v2End of Life, no security updatesUpgrade to React Router v7 via the official codemod
React Router v7 / v8Stable and maintainedUse for new React projects and migrated Remix apps
Remix 3Beta; not React-basedExperiments 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.

  1. Adopt all Remix v2 future flags in your existing app.
  2. Run the codemod: npx codemod remix/2/react-router/upgrade.
  3. Update dependencies: @remix-run/dev becomes @react-router/dev, @remix-run/node becomes @react-router/node, and @remix-run/react becomes react-router.
  4. Update scripts: remix vite:dev becomes react-router dev, and remix vite:build becomes react-router build.
  5. Move app config into react-router.config.ts and define routes in app/routes.ts.
  6. Swap the Vite plugin to reactRouter from @react-router/dev/vite.
  7. Run react-router typegen && tsc to 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 defines stale-while-revalidate as 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.

Paul BratslavskyDeveloper Advocate

Related Posts

Remix Tutorial: Building A Simple Contact App With Strapi Backend
Tutorials·39 min read

Remix Tutorial: Building A Simple Contact App With Strapi Backend

In this tutorial, we’ll walk through how to build a simple contact app using Remix for the frontend and Strapi as the backend

·January 30, 2025
From Remix to React Router 7: What’s Next for Modern Web Dev?
4 min read

From Remix to React Router 7: What’s Next for Modern Web Dev?

Explore what's new in React Router 7, how it compares to Remix, and why the future of web development includes flexibility, AI, and developer choice.

·July 17, 2025
App·12 min read

How to Build a Blog App with Remix and Strapi CMS

This article details the steps to building a blog app with Remix and Strapi CMS.

·August 31, 2022