Writing Field note

Web Development · 3 min read

The Cost of Switching from Next.js to Remix

Leaving Next.js for Remix taught me why the annoying-looking use client boundary exists—and what Remix gives up without it.

In this article5 sections

Three months ago, I ditched Next.js for Remix. Remix felt like regular web development without the mental overhead of React Server Components and "use client" boundaries.

I didn’t expect to learn why those boundaries exist.

The “use client” Boundary I Didn’t Understand

In Next.js, "use client" felt like an annoying annotation. Need a click handler? Add "use client". Want useState? Add "use client". Access localStorage? Same.

I thought it was just ceremony, but I was wrong.

That directive creates a hard boundary in your component tree. The marked module and its imports enter the client bundle. It is a module boundary, not simply a division between parent and child elements in the rendered tree. Server and client code have different capabilities and constraints.

On the server side, you have database queries, file system access, and environment variables but no user interaction. On the client side, you have DOM APIs, user events, and localStorage but no direct database access.

The boundary forces you to think about this split instead of accidentally mixing concerns.

The Bundle Creep Problem

Check the client boundary "use client" brings the module’s imports into the client graph. A Server Component passed through children can still remain server-rendered; React’s directive reference explains the distinction.

I added "use client" to a layout component for a mobile menu toggle. My entire page, including the header, sidebar, content, and footer, started shipping more JavaScript to the browser. Client Components can still be rendered to initial HTML on the server.

Some Next.js teams ban "use client" except in specific directories. When someone needs interactivity, they create a client component in a designated place like components/client/. This prevents one directive from dragging an entire page into the client bundle.

How Remix Handles the Server/Client Split

Remix doesn’t need "use client" because it enforces the boundary architecturally.

Server code lives in loader and action functions. These run on the server and can access databases, file systems, environment variables. They return plain data.

Client code lives in React components. These consume loader data and handle interaction. Components can also render on the server, so localStorage belongs in browser-only code such as an effect or event handler. Server-only imports still need to stay out of the client graph.

What I Actually Miss

Server-only components: Next.js can keep a Server Component’s implementation out of the browser bundle. Remix’s loader-and-action model separates server data work while its ordinary UI components still ship to the browser.

Component-level boundaries: With RSC, a server component fetches data, renders UI, and nests client components inside for interactivity. The boundary is fine-grained.

Bundle optimization: Server components don’t add to your client bundle at all.

In Remix, the entire component tree ships to the client, even parts that don’t need interactivity. You optimize by discipline, not by framework.

The Real Tradeoff

Next.js makes it explicit with "use client" but easy to mess up. One misplaced directive and your bundle explodes. Remix makes it implicit with loaders/actions but requires discipline. You can accidentally import server-only code in components if you’re not careful.

I preferred Remix because I could see the boundary in the route module. I later moved that application to React Router v7, but I kept the same loader-and-action model. That was the part I wanted, not the framework name.

Was this useful?

What was missing?

Thanks for the feedback.