Reference - Remix
Framework: Remix 3 — beta as of the checked date, not stable. See Libraries, Frameworks and Toolkits
Checked: 2026-08-18
If you’re looking for loaders, actions and nested routes, you want Reference - React Router — Remix v2 was merged into React Router in December 2024. The name now belongs to Remix 3, a separate project that drops React entirely for a forked Preact, and it is a genuinely different thing rather than a next version.
Read this first
"REMIX" MEANT "REMIX" MEANS NOW
Remix v2 — React, loaders, Remix 3 — no React, forked Preact,
actions, nested routes imperative state, server-rendered
│ frames
│ merged Dec 2024
▼
REACT ROUTER v7, framework mode a SEPARATE project. not a successor.
→ where new projects go migration from v2 is a rewrite, and
→ what Shopify Hydrogen runs on the team's own advice is to go to
React Router v7 instead
Nothing in Remix 3 transfers from Remix v2. Different rendering library, different component model, different routing, different mental furniture. Treat it as a new framework that inherited a name.
The rest of this note is Remix 3.
§0 The one idea
Own the whole stack, depend on almost nothing, and let the server drive. Where React Router’s premise is “the web platform already solved this”, Remix 3’s is “the dependency graph is the problem” — so it forks its own rendering library, ships routing, sessions, auth, forms, uploads and asset delivery in the box, and makes the runtime rather than a bundler the source of truth.
vs React: no hooks, no dependency arrays, no reconciler deciding when to re-render. State is a plain variable and you say when it changed.
REACT REMIX 3
useState → setState → reconcile let state = "idle"
→ framework decides what re-renders state = "copied"
handle.update() ← you decide
useEffect + cleanup on("click", async (_, signal) => …)
← abort signal handed to you
bundler is the source of truth the runtime is
1. Components
A component is a function that takes a handle and returns a render function. State lives in the closure as an ordinary variable.
import { type Handle, on } from "remix/ui";
import * as btn from "remix/ui/button";
function CopyToClipboard(handle: Handle<{ url: string }>) {
let state: "idle" | "copied" | "error" = "idle"; // just a variable
return () => (
<button
mix={[
btn.secondaryStyle,
on("click", async (_, signal) => {
await navigator.clipboard.writeText(handle.props.url);
if (signal.aborted) return; // cancellation, handed to you
state = "copied";
handle.update(); // you say when
}),
]}
>
{state === "copied" ? "Copied" : "Copy link"}
</button>
);
}Three things are doing the work here. The outer function runs once, so state is a closure variable rather than a hook. handle.update() is an explicit re-render — there is no dependency array because nothing is being tracked. And on() composes behaviour onto the element via mix, receiving an AbortSignal so async work cancels without a cleanup function.
The trade, stated plainly: more explicit and easier to reason about; more to remember, because forgetting handle.update() means the screen silently doesn’t change.
2. Routes are declared separately from handlers
The URL contract is its own file. This is unusual and it’s deliberate.
// app/routes.ts
import { get, route } from "remix/routes";
export const routes = route({
assets: get("/assets/*path"),
home: "/",
albums: {
show: get("/albums/:albumId"),
},
});That file defines the URLs and methods the app accepts, and nothing else. No handlers, no components. The benefit is that the route table is readable in one place and typed — :albumId flows through to the handler as a typed parameter.
3. Controllers return responses
Handlers are grouped into controllers bound to a route subtree.
// app/actions/albums/controller.ts
import { createController } from "remix/router";
import { routes } from "../../routes.ts";
export default createController(routes.albums, {
actions: {
show(context) {
return new Response(`Album: ${context.params.albumId}`);
},
},
});To render markup instead of a string, ask the context for it:
return context.render(<AlbumPage album={album} />);context.render() builds a Response from a component tree. The unit of return is always a web Response — so a route that returns JSON, a redirect or HTML is the same kind of function, differing only in what it hands back.
4. Frames
The idea that most distinguishes it. A frame is server-rendered UI with a src, which the client can load, navigate or reload independently while the server keeps rendering the HTML.
page
├── <frame src="/basket/summary"> reloads on its own
├── <frame src="/recommendations"> loads after the page
└── main content
Rather than shipping a client-side component that fetches JSON and re-renders, you point a region of the page at a URL and let the server re-render that region. Several observers have compared the result to htmx, and the comparison is fair — it’s server-driven partial updates rather than client-side state synchronisation.
Forms submit to URLs, so the server owns the request lifecycle in the same way. This is the one piece of continuity with Remix v2’s philosophy, even though none of the code carries over.
5. Project structure
my-remix-app/
├── server.ts the entry point
└── app/
├── routes.ts the URL contract
├── router.ts wiring
├── middleware/
├── actions/ controllers
└── ui/ components
The request flow is linear and worth holding, because it’s the whole framework in one line:
HTTP request → server.ts → app/router.ts → middleware
→ matched against app/routes.ts
→ handled by a controller action in app/actions/
→ returns a web Response
Trying it
npx remix@next new my-remix-appShould you use it
No, not for commercial work, at the checked date. It’s in beta with weekly releases, migration from v2 is a rewrite, and the ecosystem is by design almost empty — the point of owning the stack is that there isn’t one.
What it’s worth: knowing it exists, knowing it isn’t what a job advert means by “Remix”, and having a view on the bet it represents. That bet is that React’s complexity — the reconciler, hooks, the dependency graph, the bundler — is the actual problem, and that a smaller framework owning more of its own stack is the way out. It’s a coherent argument and an unproven one.
[CHECK: release status and version — this was beta and moving weekly when checked. The component and routing APIs are pre-stable and may have changed; verify against the official docs before writing anything against this note.]
Related
- Reference - React Router — what Remix v2 became, and what to learn if you want the loader/action model
- Reference - React — the library Remix 3 deliberately leaves behind
- Progressive Enhancement — the server-driven stance frames and URL-submitting forms are an expression of
- Rendering Strategies — where server-rendered fragments sit among the options