DEV Community

Cover image for It’s not just you, Next.js is getting harder to use
Victoria for PropelAuth

Posted on • Originally published at propelauth.com

It’s not just you, Next.js is getting harder to use

I wrote a blog post the other day about how Next.js Middleware can be useful for working around some of the restrictions imposed by server components. This led to some fun discussions in the world about whether this was a reasonable approach or if Next.js DX was just... bad.

Screenshot of the discussion linked above

From my perspective, Next.js’ App Router has two major problems that make it difficult to adopt:

  • You need to understand a lot about the internals to do seemingly basic tasks.

  • There are many ways to shoot yourself in the foot that are opt-out instead of opt-in.

To understand this better, let’s look at its predecessor, the Pages Router.

A quick look at the Pages Router

When I first learned about Next.js, the main “competitor” was Create React App (CRA). I was using CRA for all my projects, but I switched to Next.js for two reasons:

  • I liked file-based routing because it allowed me to write less boilerplate code.

  • Whenever I ran the dev server, CRA would open http://localhost:3000 (which gets annoying fast), and Next.js didn’t.

The second one is maybe a little silly, but to me, Next.js was:

React with better defaults.

And that’s all I really wanted. It wasn’t until later that I discovered the other features Next.js had. API routes were exciting as they gave me a serverless function without setting up any extra infra - super handy for things like “Contact Us” forms on a marketing site. getServerSideProps allowed me to run basic functions on the server before the page loaded.

Those concepts were powerful, but they were also simple.

An API route looked and acted a lot like every other route handler. If you had used Express or Cloudflare Workers, you can squint at a route handler and all the concepts you already knew translated. getServerSideProps was a little different, but once you understood how to get a request and the format of the response, it turned out to be pretty straightforward too.

The App Router release

The Next 13 release introduced the App Router, adding many new features. You had Server Components which allowed you to render your React components on the server and reduce the amount of data you needed to send to your client.

You had Layouts, which allowed you to define aspects of your UI shared by multiple routes and didn’t need to be re-rendered on every navigation.

Caching got… more sophisticated.

And while these features were interesting, the biggest loss was simplicity.

When a framework doesn’t do what you think it will do

A fairly universal experience as a developer is banging your head against the wall and yelling, “Why does this not work?”

Everyone’s been there, and it always sucks. For me, it’s even more painful if it feels like it’s not a bug in my code but a misunderstanding of how things are supposed to work.

You are no longer yelling, “Why does this not work?” but rather, “Why does this work… like that?”

The App Router, unfortunately, is full of these kinds of subtleties.

Let’s look back at my original issue: I just want to get the URL in a Server Component. Here’s an answer to a popular Github issue about the topic, and I’ll post part of it here:

If we take a step back, the question "Why can't I access pathname or current URL?" is part of a bigger question: "Why can't I access the complete request and response objects?"

Next.js is both a static and dynamic rendering framework that splits work into route segments. While exposing the request/response is very powerful, these objects are inherently dynamic and affect the entire route. This limits the framework's ability to implement current (caching and streaming) and future (Partial Prerendering) optimizations.

To address this challenge, we considered exposing the request object and tracking where it's being accessed (e.g. using a proxy). But this would make it harder to track how the methods were being used in your code base, and could lead developers to unintentionally opting into dynamic rendering.

Instead, we exposed specific methods from the Web Request API, unifying and optimizing each for usage in different contexts: Components, Server Actions, Route Handlers, and Middleware. These APIs allow the developer to explicitly opt into framework heuristics like dynamic rendering, and makes it easier for Next.js to track usage, breaking the work, and optimizing as much as possible.

For example, when using headers, the framework knows to opt into dynamic rendering to handle the request. Or, in the case of cookies, you can read cookies in the React render context, but only set cookies in a mutation context (e.g. Server Actions and Route Handlers) because cookies cannot be set once streaming starts.

For what it’s worth, this response is incredible. It’s well written, it helps me understand a lot of the underlying issues, and it gives me insight into the tradeoffs associated with different approaches that I absolutely didn’t think about.

That being said, if you are a developer and all you are trying to do is get the URL in a Server Component, you probably read this and left with 5 more things to Google before realizing you probably have to restructure your code.

This post summarizes my feelings about it:

Post that says: It's extremely counter-intuitive that there's basically no way to get the pathName from a server-side component. userPathName should at the very least be callable from the server-side as well even if it hits a different code path

It’s not that it’s necessarily incorrect - it’s unexpected.

That original post also mentioned a few other subtleties. One common footgun is in how cookies are handled. You can call cookies().set("key", "value") anywhere and it will type-check, but in some cases it will fail at runtime.

Compare these to the “old” way of doing things where you got a big request object and could do anything you wanted on the server, and it’s fair to say that there’s been a jump in complexity.

I also need to point out that the “on-by-default” aggressive caching is a rough experience. I’d argue that way more people expect to opt-in to caching rather than dig through a lot of documentation to figure out how to opt-out.

I’m sure other companies had similar issues to us, but at PropelAuth we often got bug reports that weren’t bugs but amounted to “You thought you made an API call, but you didn’t, and you are just reading a cached result.”

And all of this begs the question, who are these features and optimizations for?

It’s very hard to build a one-size-fits-all product

All of these features that I’m painting as overly complex do matter for some people. If you are building an e-commerce platform, for example, there are some great features here.

Your pages load faster because you send less data to the client. Your pages load faster because everything is aggressively cached. Your pages load faster because only parts of the page need to re-render when the user navigates to a new page. And in the e-commerce world, faster page loads means more money, so you would absolutely take the tradeoff of a more complex framework for them.

But if I’m building a dashboard for my SaaS application… I don’t really care about any of that. I care way more about the speed at which I ship features, and all that complexity becomes a burden on my dev team.

My personal experience and frustrations with the App Router will be different than another person’s because we have different products, different use cases, and different resources. Speaking specifically as a person who spends a lot of time writing and helping other people write B2B SaaS applications, the App Router DX is a big step down from the Pages Router.

Is this inevitable for frameworks as they grow?

As products/frameworks grow, they tend to get more complicated. Customers ask for more things. Bigger customers ask for more specific things. Bigger customers pay more so you prioritize and build those more specific things.

Customers who previously loved the simplicity of it all get annoyed at how complicated things feel and… oh, look at that, a new framework has popped up that’s way simpler. We should all switch to that!

It’s challenging to avoid this, but one way to mitigate it is to not make everyone deal with the complexity that only some people need.

Just because something is recommended, doesn’t mean it’s right for you

One of my biggest issues with the App Router was just this:

Screenshot of a terminal showing that the App Router select is recommended

Next.js has officially recommended that you use the App Router since before it was honestly ready for production use. Next.js doesn’t have a recommendation on whether TypeScript, ESLint, or Tailwind are right for your project (despite providing defaults of Yes on TS/ESLint, No to Tailwind - sorry Tailwind fans), but absolutely believes you should be using the App Router.

The official React docs don’t share the same sentiment. They currently recommend the Pages Router and describe the App Router as a “Bleeding-edge React Framework.”

When you look at the App Router through that lens, it makes way more sense. Instead of thinking of it as the recommended default for React, you can think of it more like a beta release. The experience is more complicated and some things that were easy are now hard/impossible, but what else would you expect from something that’s still “Bleeding-edge?”

So when you are picking a framework for your next project, it’s worth recognizing that there are still many rough edges in the App Router. You might have better luck reaching for a different tool that’s more suited to your use case.

Top comments (12)

Collapse
 
calier profile image
Calie Rushton

Thanks for sharing your experience - I've just come back to learn more about Next after not using it for a while. I'm enjoying the app router on the face of it but it's really good to get perspective from someone already using it in the wild, so to speak.

I've bookmarked this article so I can come back to the details as my understanding deepens, some very interesting points here...

Collapse
 
mattlewandowski93 profile image
Matt Lewandowski

Thanks for the article.

I recently went through a lot of these pains, as I started migrating some of my pages router api routes into the app router. Vercel recently released a new utility called waitUntil, which was really useful for my app, and only works for the app router. So I thought I would migrate.

I was able to successfully migrate all of my logic, including my auth, payload, and encryption interceptors with the new architecture. I was constantly running into walls where things should have just worked, but they didn't. Like not being able to access query parameters from within a try catch.

One of my biggest pain points, was the full page re-renders when working on route handlers in dev. On the pages directory, you could modify your route handlers without causing your application to reload in dev.

Ultimately I've decided to stay away from the app directory. The DX is horrible and the amount of boilerplate files is insane. If the pages router get fully sunsetted, I would much rather just move away from NextJS. It would be a similar amount of work either way.

Collapse
 
jasonstitt profile image
Jason Stitt

Customers who previously loved the simplicity of it all get annoyed at how complicated things feel and… oh, look at that, a new framework has popped up that’s way simpler. We should all switch to that!

I feel this one.

I think a lot of things in frameworks have gotten strictly better over time but routing is maybe not one of them. Routing methods (file based or not) are something every framework tries a little differently and it's not clear there's an optimal solution vs. just different options.

Collapse
 
ezpieco profile image
Ezpie

Hey I can relate this with my biggest mistake of trying to create the world's first ever open-sourced social media app... yeah just a fancy way of advertising, lambda, it's built with nextjs 14. And it's a pain to maintain

Collapse
 
shafqatsha profile image
Shafqat M Shah

“Why does this work… like that?”

This got me 😂🤣

Collapse
 
sebastianccc profile image
Sebastian Christopher

Spot on. 10/10 🙂

Collapse
 
alxwnth profile image
Alex

Interesting view, thanks for sharing!

Collapse
 
latobibor profile image
András Tóth

It's a well-written article! I think we are seeing next.js finding its own niche, while for other things (like writing a Dashboard or doing a mostly static page with little interactivity) we will (or already have) better tools that are less complicated.
The problem is falling in love with any tech and not looking at the big picture, not looking at alternatives. Frameworks that can stay close to their programming languages, that don't force you into their own world, are going to stay longer.
People expect to reuse their knowledge. That can only happen when your framework does not require extremely custom thinking.

Collapse
 
yaireo profile image
Yair Even Or

such poll doesn't mean much without splitting voters by years of front-end experience at least, as we do not know if most are new-comers with little experience and thus get easily confused the more a technology gets advanced.

Collapse
 
hiccupq profile image
hiccupq

Very true. Pain in the ass to use itt nowadays.

Collapse
 
jojomondag profile image
Josef Nobach

Interesting, I have started using SvelteKit And I must say I like it more then NextJs.

Great post keep up the good work!

Collapse
 
pozda profile image
Ivan Pozderac

I started using Astro, feeling the same way regarding NextJs.