Remix, React Router and the separate Remix 3 project
Remix was a framework for building React applications, built around browser standards: forms, requests, and responses. That line's last release carries version 2.17.5 from June 2026, and development moved elsewhere.
The situation around this name is tangled enough today that settling it at the start pays, since confusing three different things leads to decisions made on wrong premises.
Three things under one name
The first is Remix version two, the framework most material online describes. It works, it is maintained in the sense of fixes, and new capabilities do not reach it.
The second is React Router in framework mode. The team announced that Remix's next major version would ship as React Router version seven, and so it did. That is neither inspiration nor a spiritual successor but the same thing under another name, absorbed into the routing library powering millions of React applications. That line continues and is today at version eight.
The third is Remix version three, and here the misunderstanding begins. It is a separate project that started from a fork of Preact rather than React, ships as a beta for now, and has no migration path from version two. What they share is a name and a team, not code.
The practical conclusion for somebody choosing technology today: if you want what Remix offered, you reach for React Router in framework mode. The Remix name in the context of version three refers to something else, and choosing it out of attachment to the old framework is a mistake.
What that framework offered
Worth describing, since the concepts carried over wholesale to the successor and they are what constitutes the value.
Data loading worked at the route rather than the component. A route declared a function fetching data, the framework called it before rendering, and the component received a ready result. That removed the cascade of requests firing as successive components mounted.
Data writing worked through an ordinary form. You submit it, the route handles the request server side, and afterwards the page's data refreshes by itself. With no loading state handling written and no manual cache invalidation.
Nested routes let you express that part of an interface is shared across a group of pages, with each adding its own fragment and its own data. On dashboards with a side menu and many views that removed considerable duplication.
The governing principle was working without JavaScript. A form submitted with scripts disabled still worked, since underneath it was an ordinary HTML form. That property means more in practice than it sounds: the application also works when a script has not finished loading or failed.
What survived the merge
All of those things exist still, only under another name and in another package.
React Router today has three operating modes, and the distinction deserves knowing, since material describes them interchangeably. Basic mode is browser side routing, familiar from earlier years. Data mode adds route bound loading and writing. Framework mode is the full set with server rendering, building, and everything Remix offered.
The practical consequence is convenient: you need not choose at the start. An application can begin with browser routing and move to framework mode without changing library, once server rendering becomes necessary.
Migrating from Remix version two is shallow by design. The concepts stayed the same, package names and some import paths changed, and the team prepared automated tooling. This is not rewriting an application but walking through imports and configuration.
Check dependencies, though. Libraries written against the previous name sometimes go unmaintained, and that is usually more work than swapping imports.
Remix 3, a separate story
That project deserves explaining, since it causes the most confusion.
It is not another version of what is described above. It is an attempt to build something on a different foundation: it started from a fork of Preact rather than React, and the published packages now carry their own component model with no dependency on an external view library, following the team's stated aim of eventually having no dependencies at all. There is no migration path, since there is nothing to migrate.
Know the project's state too, since it changes the risk calculation. The package reaches the registry as a beta under a separate tag, so this is something before its first stable release rather than a mature alternative.
For somebody holding a working application on version two the message is unambiguous: this is not the direction you are heading. Your route is React Router in framework mode.
For somebody starting a new project the matter is more open and deserves caution. A project built on a fork of a popular library binds you to one team's decisions in a layer usually treated as a stable foundation. That is sometimes justified and requires deliberate acceptance of different risk from React.
Practical advice: on a commercial project with a long horizon, check the surrounding library ecosystem, since that is usually the deciding factor rather than the framework's own quality.
Loaders and actions in practice
Worth showing in code, since those two concepts are the heart of the whole approach and carried over to the successor unchanged.
export async function loader({ params }: LoaderFunctionArgs) {
const order = await db.findOrder(params.id)
if (!order) throw new Response('Not found', { status: 404 })
return { order }
}
export async function action({ request }: ActionFunctionArgs) {
const data = await request.formData()
await db.cancelOrder(String(data.get('id')))
return redirect('/orders')
}Three things in that code deserve noticing.
The loading function returns data rather than an HTTP response, and that is convenient. It can, however, throw a response, and then the framework shows the right error page. That mechanism replaces checking conditions in a component and conditionally rendering an empty state.
The writing function takes a request and reads form data from it exactly as an ordinary server would. There is no textual request body and no serialisation, since a form is a form.
export default function Order() {
const { order } = useLoaderData<typeof loader>()
const navigation = useNavigation()
const submitting = navigation.state === 'submitting'
return (
<Form method="post">
<input type="hidden" name="id" value={order.id} />
<button type="submit" disabled={submitting}>
{submitting ? 'Cancelling...' : 'Cancel order'}
</button>
</Form>
)
}The form component supplied by the framework intercepts submission and performs it without a page reload, while with JavaScript disabled it behaves like an ordinary HTML form. That is the property making this whole pattern worthwhile: the page works before the scripts load.
The redirect after writing is an ordinary response rather than a call to a navigation function. That is the same pattern server applications have followed for years, and it prevents resubmitting a form after a page refresh.
Know too that data from the loading function refreshes automatically once a write completes. There is no cache invalidation and no manual refetch call, since the framework knows which routes might have changed.
Optimistic updates rest on that: the interface shows the change before the server answers, and when the answer arrives the real data simply replaces it.
const fetcher = useFetcher()
const status = fetcher.formData?.get('status') ?? order.status
return (
<fetcher.Form method="post">
<input type="hidden" name="status" value="cancelled" />
<span>{status}</span>
<button type="submit">Cancel</button>
</fetcher.Form>
)Reading the submitted data off the fetcher gives the target value without maintaining separate state. If the write fails, the value disappears by itself, because the form data field ceases to exist and the component falls back to the server value.
The limits of this approach
Worth listing honestly the cases where this is not the right choice, since promotional material does not show them.
An application with no backend. The entire value of route level loading and form based writing rests on there being a server. A static brochure site does not need that layer, and adding it means building and deploying something that could sit as files.
Worth knowing, though, that the framework does not force you to wait for everything. Slow data can be handed over as a promise while the rest of the page renders immediately.
export async function loader({ params }: LoaderFunctionArgs) {
const order = await db.findOrder(params.id)
const recommendations = db.computeRecommendations(params.id)
return { order, recommendations }
}
export default function Page() {
const { order, recommendations } = useLoaderData<typeof loader>()
return (
<>
<Summary order={order} />
<Suspense fallback={<Skeleton />}>
<Await resolve={recommendations}>
{(list) => <Recommendations list={list} />}
</Await>
</Suspense>
</>
)
}The absence of an await on the second call is the whole difference. The order blocks the response, since the page makes no sense without it, while recommendations arrive later and a skeleton stands in until they do.
A highly interactive application. A dashboard with a canvas, an editor, or a view refreshing every second works mainly in the browser, and a pattern built on requests and responses fits less well there. A query library plus browser routing is often the simpler answer.
A team accustomed to another model. Loading data at the route differs from fetching it in a component, and that is a change of habit rather than only syntax. On a team that knows one way well, the cost of switching sometimes exceeds the gain.
The last limit concerns the ecosystem. Libraries assuming a particular framework tend to be adapted to the most popular one first and to the rest afterwards. When choosing, check the ones you will actually use rather than assuming everything works everywhere.
Remix against the alternatives
| Option | Status | Ecosystem | Pick it when |
|---|---|---|---|
| Remix 2 | Maintained, no new features | Large, built on React | You have it deployed, planning a move |
| React Router, framework mode | Actively developed | Large | You want what Remix offered |
| Next.js | Actively developed | The largest | Content sites, server rendering |
| Remix 3 | A separate project, in beta | New, smaller | You deliberately choose another foundation |
The choice between the second and third rows is today's main question for a new React application, and it has no single answer.
React Router in framework mode sits closer to browser standards and imposes less. Route code looks like request handling rather than a framework specific construct, so somebody who knows web fundamentals understands it more easily.
Next.js offers a larger ecosystem, more ready made solutions, and deeper integration with its author's deployment platform. The price is more proprietary concepts to learn and stronger binding to one way of doing things.
Treat the first row purely as a description of the current state. No new application will start there, and existing ones have a clear exit.
How to run the migration
An order that works with a version two application.
Start with dependencies rather than code. List the libraries importing packages under the old name and check whether versions compatible with the new one exist. That is where migrations stall most often, and learning it before starting beats learning it midway.
Then run the tooling automating import replacement. Swapping package names and paths is mechanical, so doing it by hand across several hundred files wastes time.
The third step is build configuration. That is where most manual work usually sits, since custom settings from the previous version need carrying over deliberately rather than copying one to one.
The fourth is reviewing types. Helper type names partly changed, and errors here are explicit and quick to fix.
Do not migrate alongside other changes. A migration where the team also restructures directories and rewrites a few views ends with nobody knowing what broke the application.
Common mistakes
The first is confusing Remix version three with a successor to version two. They are separate projects on different foundations, and no migration between them exists.
The second is starting a new project on Remix version two. New capabilities do not reach it and the exit route is known, so entering directly where it ends is better.
The third is learning from material predating the merge without checking the date. The concepts stayed, while package names and import paths differ.
The fourth is migrating without checking dependencies first. A library with no version compatible with the new name halts everything, and discovering that halfway is expensive.
The fifth is combining a migration with other changes. On a failure you then cannot tell what caused it.
The sixth is choosing framework mode where browser routing suffices. An application with no need for server rendering then gains a build and deployment layer it does not require.
The seventh is fetching data in a component despite a route level loading function being available. The cascade of requests firing as components mount then returns, meaning exactly what this model existed to remove.
The eighth is assuming that server rendering makes server side validation unnecessary. The writing function takes a request that can be sent bypassing the interface, so validation belongs there rather than only in the form.
FAQ
Does Remix still exist?
Version two exists and is maintained in the sense of fixes, while development moved into React Router, where framework mode is what Remix was. Separately there is a project called Remix 3, which is something else.
What is Remix 3?
A separate project that started from a fork of Preact rather than React and ships as a beta today, with no migration path from version two. What they share is a name and a team rather than code, so choosing it out of attachment to the old framework is a misunderstanding.
What do I migrate to from Remix 2?
React Router in framework mode. It is the same thing under another name, so the concepts stay while package names and some import paths change, with automated tooling to help.
How does it differ from Next.js?
In closeness to browser standards and a smaller number of proprietary concepts. Route code looks like request handling rather than a framework construct. Next.js offers a larger ecosystem and deeper integration with its author's deployment platform in exchange.
Do I have to use framework mode?
No. The library has three modes, so you can start with browser routing and move up when server rendering becomes necessary, without changing library.
The merge is described on the project blog, and the current line's documentation on the React Router site.