The Cost of Switching from Next.js to Remix
Three months ago, I ditched Next.js for Remix. The reasoning was simple: Remix felt like regular web development without the mental overhead of React Server Components and "use client"
boundaries.
What I didn't expect was how much I'd learn about why those boundaries exist in the first place.
The "use client" Boundary I Didn't Understand
Back when I was using Next.js, "use client"
felt like an annoying annotation I had to sprinkle around to make interactive components work. Need a click handler? Add "use client"
. Want to use useState? Add "use client"
. Access localStorage? You guessed it.
I thought it was just ceremony. I was wrong.
That directive creates a hard boundary in your component tree. Everything above it runs on the server during rendering. Everything below it ships to the browser as JavaScript. The React team designed it this way because server and client code have fundamentally different capabilities and constraints.
On the server: database queries, file system access, environment variables, no user interaction.
On the client: DOM APIs, user events, localStorage, no direct database access.
The boundary forces you to think about this split explicitly instead of accidentally mixing concerns.
The Bundle Creep Problem
Here's what I learned the hard way: "use client"
is contagious. Mark one parent component as client-side, and every child component becomes client-side too, even if they don't need browser APIs.
I once added "use client"
to a layout component just to handle a simple click event for a mobile menu toggle. Suddenly my entire page—header, sidebar, content, footer—was shipping to the browser as JavaScript instead of rendering on the server.
This is why some Next.js teams ban "use client"
except in specific directories. It's not about being restrictive—it's about preventing accidental complexity. When someone needs interactivity, they have to explicitly create a client component in a designated place like components/client/
instead of just adding the directive wherever.
How Remix Handles the Server/Client Split
Remix doesn't need "use client"
because it enforces the server/client 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 receive data as props from loaders and can use all the browser APIs they want—useState, event handlers, localStorage. Because this separation is automatic. You can't accidentally put a database query in a component because loaders and actions are the only places with server context.
What I Actually Miss About "use client"
Automatic tree shaking: When Next.js sees a server component, it can strip out browser-only dependencies from the server bundle automatically. In Remix, you have to be disciplined about keeping server and client imports separate.
Component-level boundaries: With RSC, you can have a server component that fetches data, renders some UI, and then has client components nested inside for interactivity. The boundary is explicit and fine-grained.
Bundle optimization: Next.js can split your JavaScript based on these boundaries. 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 magic.
The General Learning
What I learned from switching isn't that one framework is better than the other. It's that the server/client boundary is the most important architectural decision in modern React apps.
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.