Tags: web-dev reference

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-app

Should 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.]