Skip to main content
Back to Journal
ReactFrameworks

Remix: Server Side React That Respects the Web Platform

Remix 1.0 shipped last month, and I have been spending my evenings rebuilding a small project with it. Ryan Florence and Michael Jackson (the React Router creators, not the musician) have been working on this for a while, and the result is a React framework that feels philosophically different from Next.js.

The core idea is that the web platform already has good primitives for data loading and mutations. HTML forms, HTTP methods, request/response patterns. Remix builds on those instead of replacing them.

Loaders and Actions

Every route in Remix can export a loader function (for GET requests) and an action function (for POST, PUT, DELETE). These run on the server and provide data to your component.

import { json } from '@remix-run/node';
import { useLoaderData, Form } from '@remix-run/react';

export async function loader() {
  const posts = await db.posts.findMany();
  return json({ posts });
}

export async function action({ request }) {
  const formData = await request.formData();
  const title = formData.get('title');
  const content = formData.get('content');
  await db.posts.create({ data: { title, content } });
  return json({ success: true });
}

export default function Posts() {
  const { posts } = useLoaderData();

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>

      <Form method="post">
        <input name="title" placeholder="Title" />
        <textarea name="content" />
        <button type="submit">Create Post</button>
      </Form>
    </div>
  );
}

The loader runs on the server when the page loads. The action runs when the form is submitted. The component just renders the data. Clean separation.

Progressive Enhancement

Here is what really sets Remix apart: that form works without JavaScript. If you disable JS in the browser, the form still submits, the action still runs, and the page reloads with the new data. This is how HTML forms have always worked, and Remix embraces it.

When JavaScript is available, Remix enhances the experience. The form submission happens via fetch, the page does not reload, and the UI updates seamlessly. But the baseline functionality works either way. This is progressive enhancement done right.

The Form component from Remix is essentially a regular HTML form that gets progressively enhanced. You do not need to write an onSubmit handler, call preventDefault, or manage loading state manually. Remix handles all of that.

Nested Routes and Parallel Loading

Remix uses file-based routing like Next.js, but it has a nested routing model that changes how you think about page structure. Each route segment can have its own loader, and Remix loads all of them in parallel.

For example, if you have a route like /dashboard/settings/profile, and each segment (dashboard, settings, profile) has a loader, all three loaders run at the same time. In a traditional setup, you would load the dashboard layout data, then the settings data, then the profile data sequentially. Remix parallelizes this automatically.

Each nested route also gets its own error boundary. If the profile loader fails, the profile section shows an error, but the dashboard layout and settings sidebar still render normally. This kind of granular error handling is difficult to achieve with other frameworks.

Error Boundaries

Speaking of errors, Remix has a built-in pattern for handling them at the route level.

export function ErrorBoundary({ error }) {
  return (
    <div className="error">
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
    </div>
  );
}

export function CatchBoundary() {
  const caught = useCatch();
  return (
    <div>
      <h2>{caught.status} {caught.statusText}</h2>
    </div>
  );
}

The ErrorBoundary catches unexpected errors. The CatchBoundary handles expected HTTP errors (like a 404 when a resource is not found). Both are scoped to their route, so an error in one part of the page does not take down the entire application.

How It Compares to Next.js

Next.js and Remix solve many of the same problems, but they approach them differently. Next.js leans heavily on static generation and ISR (Incremental Static Regeneration). It is optimized for content that can be pre-rendered at build time. Remix is optimized for dynamic, user-specific content that needs to be loaded fresh on every request.

Next.js uses getStaticProps and getServerSideProps, which are separate from the component. Remix uses loaders and actions that are co-located with the route component. The data fetching pattern in Remix feels more cohesive to me.

For a marketing site or a blog, Next.js with static generation is probably the better choice. For a dashboard, a SaaS application, or anything with authentication and dynamic data, I think Remix has a real edge. The nested routing, parallel data loading, and progressive enhancement are genuinely useful for complex applications.

My Impression

After a week with Remix, I am genuinely excited about it. The web standards approach feels refreshing after years of frameworks that abstract away everything. Forms that work without JavaScript, HTTP caching built into the data layer, and a routing model that naturally handles loading and error states.

It is not perfect. The ecosystem is brand new, so libraries and patterns are still emerging. The documentation is good but not as comprehensive as Next.js yet. And if you need static site generation, Remix is not the right tool.

But for the kind of work I do most often (dynamic, authenticated applications), Remix makes a compelling argument. I plan to use it for my next project and see how it holds up over time.

remixreactserver-side-renderingweb-standardsprogressive-enhancement