KahWee - Web Development, AI Tools & Tech Trends

Expert takes on AI tools like Claude and Sora, modern web development with React and Vite, and tech trends. By KahWee.

The Cost of Switching from Next.js to Remix

The Learning

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. Everything above it runs on the server during rendering. Everything below it ships to the browser as JavaScript. 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

Warning

"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 added "use client" to a layout component for a mobile menu toggle. My entire page — header, sidebar, content, footer — started shipping to the browser as JavaScript instead of rendering 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 receive data as props from loaders and can use all browser APIs — useState, event handlers, localStorage. 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

Automatic tree shaking: Next.js strips browser-only dependencies from the server bundle when it sees a server component. In Remix, you keep server and client imports separate through discipline.

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

Important

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.


Related posts: