Why I (Personally) Prefer(ed) React Router/Remix Over Next.js
My personal experience comparing Next.js and React Router/Remix, explaining why I prefer the latter for its simplicity, performance, and alignment with web standards.
Having experimented with both React Router/Remix and Next.js, I found myself heavily favoring React Router/Remix. The developer experience (DX) and logical structure behind its APIs feel significantly more intuitive, performant, and aligned with web standards, ultimately leading to a better user experience (UX). In contrast, Next.js—especially in its latest versions (Next.js 15 and React 19)—introduced a number of abstractions meant to streamline development but ended up making things more convoluted.
Before diving into my thoughts, I want to emphasize that this is purely my personal opinion. Every developer has their own preferences and tastes when it comes to choosing a framework, and what works well for me might not necessarily work well for others. Next.js has a large and passionate user base for good reasons, and I respect that. However, based on my own experience, I found React Router/Remix (React Router) to be a better fit for my development style and priorities.
The Frustrations with Next.js
While Next.js provides useful features such as server/client components and server actions, aimed at reducing complexity in data transmission between the client and server, I found several aspects of the developer experience frustrating:
- Server Actions in Client Components
To call a server action from a client component, I have to pass it as a prop. If I have multiple client components wrapped together, I must pass the server action to all components in a cascading manner. This adds unnecessary complexity and makes the code harder to manage. An alternative approach is to create a separate file for actions marked with"use server", but this results in numerous individual files that are no longer colocated with the client-side code in the same file. This separation arguably contradicts one of the core reasons server actions exist in the first place—to blur the line between front-end and back-end—and introduces unnecessary boilerplate. - Client Components Invalidate Server Component Benefits
If I use a client-side library like Framer Motion, all components inside that wrapper effectively become client components, nullifying the benefits of server components. - The
"use client"Directive is Too Restrictive
Since"use client"must be declared at the top of a file rather than within a function, even something as simple as a single button with an event listener requires a separate file, adding boilerplate. - Questionable Performance Gains from Server Components
While server components allegedly reduce bundle size, I feel that React Router/Remix achieves similar or better UX outcomes through features and enhancements in other areas like:useHydrated()providing granular control over server/client rendering, which allows for progressive enhancement without requiring JavaScript.- Race condition handling (request interruption/deduplication)—if I hit a button twice in React Router/Remix, the first request gets canceled, whereas, in Next.js, both requests will run, not in parallel, but synchronously.
- Boilerplate for Optimistic UI and Forms
Implementing optimistic UI or handling form actions in Next.js results in a lot of repetitive code that React Router/Remix eliminates with its native abstractions. - The "Magic" of Next.js Can Be Confusing
Initially, I didn't understand why people called Next.js "magical." But after using it more deeply, I found unexpected behavior due to:- Modified
fetch()API for cache functionalities. - The
"use cache"directive, which works in a way that isn't immediately clear. (Also, there isn’t a complimentary developer tool to view cache entries?) - Odd behaviors requiring weird fixes that involve
await connection().
- Modified
- React Context Limitations in Server Components
Since React contexts are not available in server components, I frequently ran into issues where I had to redesign/restructure UI programming patterns. - Difficulties with Render Props
Render props are unintuitive to work with, as highlighted in this GitHub discussion. - Unexpected Restrictions on Server Actions
One particularly strange issue I encountered was that a server action, running on the server side, calling an async function, could not pass in a complex object. Since the function is executed on the server within the server action, this feels like an unnecessary limitation or even a bug.
Rethinking Next.js as "Production-Ready"
Previously, I assumed that many developers chose Next.js for its stability and production-readiness. However, I found myself forced to use the Canary channel and React Canary to access essential features, such as the new cache APIs. This contradicts the notion that Next.js is "stable" or "production-ready" out of the box.
For me, this realization removed one of the key reasons for choosing Next.js over React Router/Remix.
Conclusion
Ultimately, my experience with Next.js left me feeling constrained rather than empowered. While it introduces powerful abstractions, they often feel restrictive, leading to increased complexity and boilerplate. In contrast, React Router/Remix (React Router) provides a more streamlined, web-standard-aligned approach that prioritizes both DX and UX.
Despite my criticisms, I do recognize that Next.js offers some genuinely useful features. For example, the "use cache" directive is a valuable addition that simplifies caching mechanisms. Also, server actions, which are referred to by randomly generated IDs, are genuinely better than React Router/Remix’s actions, which are dependent on route paths (and subject to the restraint of one action per file), in my mind.
Also, I plan to explore TanStack. Its focus on type-safe actions, routing, and form handling appears highly appealing. If it delivers on its promises, it might become my go-to alternative for future projects.