https://pilcrowonpaper.com/blog/nextjs-why/
profile picture
Github Twitter Donate
Next.js, just why?
September 9, 2023
I don't want this to be just a rant. I really don't. But out of all
the frameworks I've worked with for Lucia, Next.js has been
consistently infuriating to work with. And it hasn't improved in
months.
In Lucia, Auth.handleRequest() is a method that creates a new
AuthRequest instance, which includes the method AuthRequest.validate
(). This checks if the request is coming from a trusted origin (CSRF
protection), validates the session cookie, and sets a new cookie if
required (this is optional). At a minimum, this requires the request
URL or host, request method, and request headers. This shouldn't be
an issue since most, if not all JS frameworks (Express, SvelteKit,
Astro, Nuxt, etc.) provide you with some request object, usually
either a Request or IncomingMessage.
And then there's Next.js.
Next.js 12
Next.js 12 and the Pages Router were fine. You get access to
IncomingMessage and OutgoingMessage inside getServerSideProps(),
which allows you to run some code in the server before SSR-ing the
page.
export const getServerSideProps = async (req: IncomingMessage, res: OutgoingMessage) => {
req.headers.cookie; // read header
res.setHeader("Set-Cookie", cookie.serialize()); // set cookie
return {};
};
There were few issues with it however. First, you just can't set
cookies when you deploy the page to the Edge. You just can't. I'm not
sure about the history of Next.js, but it looks to me that the API
wasn't thought out very well. Another issue is that the middleware
uses the web standard Request. Props to the Next.js team for
transitioning to web standards, but I'd argue it just made things
worse with inconsistent APIs (IncomingMessage vs Request). But, at
the end of the way, it works... I guess.
Next.js 13
Next.js being production-ready is a joke.
Next.js 13 introduced a new router - the App Router. All components
inside it are React Server Components by default so they always run
on the server. Everything is rendered on the server and gets sent to
the client as pure HTML.
// app/page.tsx
const Page = async () => {
console.log("I always run on the server"); // only gets logged in the server
return
Hello world!
;
};
If you've ever used Remix, SvelteKit, or Astro, it's similar to the
loader pattern. If you've used Express or Express-like libraries,
it's just an app.get("/", handler). So you'd expect to get the
request or a request-context object to be passed to the function...
right? Right?
// app/page.tsx
// something along this line
const Page = async (request) => {
console.log(request);
return Hello world!
;
};
Inconsistent APIs
So, how do you get the request inside your pages? Well here's the
thing, you can't! Yup, what a genius idea! Let's go all in with
servers and not let your users access the request object.
Actually, they do, but don't. They do provide cookies() and headers
(), which you need import for some reason.
// app/page.tsx
import { cookies, headers } from "next/headers";
const Page = async () => {
cookies().get("session"); // get cookie
headers().get("Origin"); //get header
return Hello world!
;
};
Ok fine. Maybe there's a great reason why they can't just pass them
as arguments. But then why would you only provide an API for
accessing cookies and headers? Just export a request() that returns a
Request or the request context?. This makes less sense when realize
that API route handlers and middleware lets you access the Request
object.
// app/api/index.ts
export const GET = async (request: Request) => {
// ...
};
And here's the fun part. You can't use cookies() and headers() inside
middleware (middleware.ts)!
Just provide us with a single API to interact with incoming requests.
Arbitrary limitations
Remember how you couldn't set cookies in getServerSideProps() when
the page runs on the Edge? Well, with the App Router you can't set
cookies when rendering pages, period. Not even when running on
Node.js. Wait, why can't we use cookies()?
// app/page.tsx
import { cookies } from "next/headers";
const Page = async () => {
cookies().set("cookie1", "foo");
return Hello world!
;
};
They expose the set() method but you get an error when you try do
this! Why???? I cannot come up with a single valid reason why this
restriction is necessary. SvelteKit does this fine. Every HTTP
frameworks do this fine. Even Astro, the framework that focuses on
static-sites (or used to anyway), did this fine before 1.0.
Also, headers() is always read-only, unlike cookies() which can set
cookies inside API routes. Another consistency issue.
My final gripe is with middleware. Why does it always run on the
Edge? Why limit it from running database queries or using Node.js
modules? It just makes everything complicated and it makes passing
state between middleware and routes impossible - something Express,
SvelteKit, and again, even Astro can do.
Just, why?
All these little issues add up and just make supporting Next.js as a
library author frustrating at best, and near-impossible at worst. The
slow boot-up and compilation time, as well as buggy dev servers, just
make using Next.js in general not enjoyable. Caching is a whole
another issue I didn't touch too.
I don't want to assume anything malicious on Next.js' or Vercel's
end, but they just seem to outright ignore issues on setting cookies
inside page.tsx. Their dev-rel is pretty good at responding on GitHub
and Twitter, but they haven't responded to any tweets or Github
issues on the matter. Their dev-rel and even the CEO reached out to
me to ask if they were anything that could be improved, and I
mentioned the cookie issue, and no response. I even tweeted out to
them multiple times. Like I don't expect any changes, especially
immediately, but some kind of acknowledgment would be nice.
Like, I get it. I shouldn't expect anything from open-source
projects. I'm a library author myself. But come on. It's a massive
framework backed by a massive company. Is it bad to have some
expectations?
I think the root cause is 2 folds. First, a rushed release.
Documentation is still spotty and everything seems to be incomplete
to a varying degree. And second, React, and server components
specifically. React still tries to be a library when it's definitely
a framework at this point. The goo of Next.js APIs and React APIs
with overlapping responsibilities in the server isn't working. React
needs to embrace a single framework, whether it be their own or
Next.js, and fully commit to it.
Update: I've been told a some of these issues stem from streaming.
You can't set status codes and headers after streaming has started
(everything is streamed in RSCs). I don't see how that makes anything
better. It just makes them look worse; they were aware of the issue
yet built a whole framework around it without addressing it.