Remix can render your entire application on the server on every request, which sounds inefficient but actually makes for a snappier user experience by minimizing client-side JavaScript execution.
Let’s see this in action. Imagine a simple "About Us" page.
// app/routes/about.jsx
import { Outlet, Link, useLoaderData } from "@remix-run/react";
export async function loader() {
// In a real app, this would fetch data from a database or API
const companyInfo = {
name: "Awesome Inc.",
mission: "To build awesome things.",
founded: 2020,
};
return companyInfo;
}
export default function About() {
const data = useLoaderData();
return (
<div>
<h1>About {data.name}</h1>
<p>{data.mission}</p>
<p>Founded in {data.founded}</p>
<nav>
<Link to="/">Home</Link> | <Link to="/contact">Contact</Link>
</nav>
<Outlet />
</div>
);
}
When a user navigates to /about, Remix’s server handles the entire request. It executes the loader function, which fetches companyInfo. Then, it renders the About component with that data. The resulting HTML is sent to the browser. The user sees the page instantly. If they click "Home", the browser makes a request for /, and the Remix server again handles rendering the Home page component. Crucially, there’s no client-side JavaScript needed to "hydrate" the page and make it interactive. The HTML is the interactive page.
This server-centric approach tackles a fundamental problem: how to deliver fast, interactive web applications without requiring massive amounts of client-side JavaScript to download, parse, and execute. Next.js, in contrast, often relies on client-side hydration for interactivity after an initial server-rendered HTML payload.
The core difference lies in how they handle data fetching and rendering. Remix’s loader functions run on the server for every navigation. This means the server is always up-to-date and responsible for the initial render. Remix also leverages web fundamentals like fetch and FormData natively, making data mutations feel like standard form submissions.
In Remix, navigating between pages is as simple as a Link component, which triggers a request to the server. The server fetches the data for the new route, renders the component, and sends back HTML. This is why the initial load and subsequent navigations feel so quick – the browser receives fully formed HTML.
The levers you control in Remix are primarily around your loader and action functions. loader functions are for fetching data that the component needs to render. action functions handle data mutations (POST, PUT, DELETE requests), often triggered by form submissions. Remix automatically handles form submissions, sending the data to the action function on the server, and then revalidating loader data to update the UI.
One of the most powerful, yet often overlooked, aspects of Remix is its progressive enhancement story. Because forms and links work even if JavaScript fails or is disabled, your application remains functional. When JavaScript is available, Remix enhances the experience by handling transitions and data mutations client-side, providing a seamless, app-like feel without the user ever noticing a full page reload. The browser’s native form submission mechanism is the fallback, ensuring basic functionality is always present.
The next hurdle is understanding how Remix handles error boundaries and manages state across navigations.