Web Standards Framework

Remix

Remix is the web-standards-first React framework. It leans on HTTP, forms and progressive enhancement so your app works before the JavaScript arrives.

intermediate13 min readUpdated Sep 15, 2026
posts.$slug.tsx
tsx
// app/routes/posts.$slug.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader({ params }) {
  const post = await getPost(params.slug);
  if (!post) throw json({ message: "Not found" }, { status: 404 });
  return json({ post });
}

export default function Post() {
  const { post } = useLoaderData();
  return <article>{post.title}</article>;
}
Built on
React
Model
Nested routes
Data
loaders
Mutations
actions
Philosophy
Web standards first
Now
Merged into React Router v7

Why it matters

Why Remix takes a different path

Progressive enhancement

Forms and navigation work without JavaScript, then get faster and smoother once it loads.

Nested routes with data

Each route loads its own data and handles its own errors, in parallel with its siblings.

Fewer moving parts

Loaders and actions replace the usual tangle of client state, effects and API calls.

The big picture

The three ideas behind Remix

Routes own their data, mutations are forms, and everything degrades gracefully without JavaScript.

Routes

Structure

Nested routes define the URL, the layout and the data boundary in one place.

Loaders and actions

Data

Server functions that read data before rendering and handle mutations from forms.

Web standards

Foundation

Requests, responses, forms and headers are platform primitives, not framework abstractions.

Remix at a glance

The core of Remix

Nested routes

Route modules nest to match the UI hierarchy and load data in parallel.

Loaders

Server functions that provide data to a route before it renders.

Actions

Server functions that handle form submissions and mutations.

Forms

Real HTML forms that work without JavaScript and enhance automatically.

Error boundaries

Per-route error handling that keeps the rest of the page alive.

Resource routes

Routes that return data instead of UI, useful for APIs and webhooks.

A short history

From a Remix of the web to React Router

  1. 2020

    Remix announced

    The creators of React Router introduce a framework built on web standards.

    20
  2. 2021

    Open source

    Remix is released publicly and gains attention for its data-loading model.

    21
  3. 2022

    Remix 1.0 and Shopify

    A stable release, and Shopify acquires the team and adopts Remix.

    22
  4. 2024

    React Router v7

    Remix's ideas are folded into React Router, and Remix becomes its framework mode.

    24
  5. Today

    Standards-first React

    The model lives on as React Router's framework mode, with the same philosophy.

    Today

The complete guide

Remix: Everything you need to know

What is Remix?

Remix is a React framework built on web standards. Where many frameworks abstract away the platform, Remix leans into it: HTTP requests and responses, HTML forms, links, headers and cookies are the foundation, and the framework enhances them rather than replacing them.

The result is an application that works before JavaScript loads. Forms submit, links navigate and data appears, because the server is doing the work. When JavaScript does arrive, the same interactions become instant and partial. That property is called progressive enhancement, and it is Remix’s defining idea.

In 2024 the Remix team folded the framework into React Router v7, where it lives on as framework mode. The concepts and APIs are unchanged, so everything here applies to both.

Nested routes

Routes are files under app/routes, and dots in filenames map to URL segments.

app/routes/
├── _index.tsx              # /
├── posts._index.tsx        # /posts
├── posts.$slug.tsx         # /posts/:slug
├── posts.$slug.edit.tsx    # /posts/:slug/edit
└── api.health.tsx          # /api/health

Routes nest to match the UI. A parent route renders shared layout and an <Outlet />, and child routes render inside it. Because Remix knows the whole route tree, it can load data for a page and all of its nested routes in parallel rather than in a waterfall.

// app/routes/posts.tsx
import { Outlet } from "@remix-run/react";

export default function PostsLayout() {
  return (
    <div className="posts">
      <h1>Posts</h1>
      <Outlet />
    </div>
  );
}

Loaders

A loader is a server function that provides data to a route before it renders. It runs on the server, so it can talk to a database directly.

// app/routes/posts.$slug.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.slug);
  if (!post) throw json({ message: "Post not found" }, { status: 404 });
  return json({ post });
}

export default function Post() {
  const { post } = useLoaderData<typeof loader>();
  return <article>{post.title}</article>;
}

There is no loading state to manage and no client-side request. Remix calls the loader on the server for the initial render and again on navigation, and the component receives the data synchronously.

Actions and forms

Mutations are handled by actions, which are server functions tied to a route and triggered by form submissions.

// app/routes/posts.new.tsx
import { redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";

export async function action({ request }: ActionFunctionArgs) {
  const data = await request.formData();
  const title = String(data.get("title") ?? "").trim();

  if (!title) return json({ error: "Title is required" }, { status: 400 });

  await createPost(title);
  return redirect("/posts");
}

export default function NewPost() {
  return (
    <Form method="post">
      <input name="title" required />
      <button type="submit">Create</button>
    </Form>
  );
}

Because this is a real form, it works without JavaScript. Remix intercepts the submission when the client is ready and revalidates the affected loaders automatically, so the UI reflects the new data without manual cache updates.

Error boundaries

Each route can export an ErrorBoundary that catches errors from its loader, action or component.

// app/routes/posts.$slug.tsx
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error) && error.status === 404) {
    return <h1>Post not found</h1>;
  }

  return <h1>Something went wrong</h1>;
}

Boundaries are nested, so an error in a child route does not blank the entire page. The surrounding layout stays functional, which is a much better experience than a white screen.

Resource routes and web standards

A route with no default export is a resource route: it returns data rather than UI, which is ideal for APIs and webhooks.

// app/routes/api.health.tsx
export async function loader() {
  return json({ status: "ok", time: Date.now() });
}

Because Remix uses the platform’s Request and Response, you can read headers, set cookies, stream responses and return any status code without learning a framework-specific API. That knowledge transfers directly to other runtimes.

Why the model feels different

Most React apps manage data in effects and keep it in client state, then sync that state back to the server through API routes. Remix collapses those layers:

  • The route owns its data through a loader.
  • The route owns its mutations through an action.
  • The server owns the source of truth, and the client stays thin.

Fewer moving parts means fewer bugs, less state to synchronise and less code to write. The trade-off is a stronger opinion about how data flows, which is exactly why teams that adopt it tend to like it.

Best practices

  • Fetch data in loaders, not in effects.
  • Mutate through actions and forms, and let Remix revalidate.
  • Use Form and Link from Remix so enhancement works automatically.
  • Export an ErrorBoundary on routes that can fail.
  • Keep nested routes aligned with the UI hierarchy.
  • Use resource routes for APIs and webhooks.
  • Return proper status codes and headers from loaders and actions.

Common mistakes

  • Fetching in useEffect instead of a loader and losing server rendering.
  • Using fetch for mutations instead of an action and revalidating by hand.
  • Forgetting the <Outlet /> in a parent layout route.
  • Swallowing errors instead of letting boundaries handle them.
  • Over-fetching in a parent loader when a child route should own the data.
  • Treating Remix like a client-side SPA and fighting its server model.

Where to go next

Remix is the strongest expression of “use the platform” in the React world. Compare it with Next.js for the other major React approach, and with SvelteKit for a similar philosophy in another ecosystem. Strengthen your React, TypeScript and Node.js foundations, then build a small app with a loader, an action and an error boundary.

Loading data

A loader runs on the server before the route renders. There is no loading state and no client request.

Prefer
export async function loader() {
  return json({
    posts: await getPosts(),
  });
}

export default function Posts() {
  const { posts } = useLoaderData();
  return <List items={posts} />;
}
Avoid
export default function Posts() {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    fetch("/api/posts")
      .then((r) => r.json())
      .then(setPosts);
  }, []);
  return <List items={posts} />;
}

Handling mutations

An action handles the form on the server. The same form works with or without JavaScript.

Prefer
export async function action({ request }) {
  const data = await request.formData();
  await createPost(data.get("title"));
  return redirect("/posts");
}

<Form method="post">
  <input name="title" />
  <button>Create</button>
</Form>
Avoid
async function onSubmit(e) {
  e.preventDefault();
  await fetch("/api/posts", {
    method: "POST",
    body: JSON.stringify(data),
  });
}

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Remix?

Our interactive tutorial walks you through Remix step by step — with quizzes and real code you can run in the browser.