Should you use NextJS?
NextJS is awesome. I used it to launch a MVP in 1 week (BikinKuis for Mondiblanc), and that's with figuring out how to deploy the infra side of things.
But - and this is a big but - there are big gotchas that no-one seems to talk about.
Is SSR bad?
SSR = Server Side Rendering
No, it isn't. SSR is the least of your worries, because everyone has talked a lengthy amount about it. There are tradeoffs, and these tradeoffs are generally well-known.
TL:DR; is that SSR = great for SEO and preprocessing content, not that great for having an app-like experience with great performance.
Here's the thing: if you don't like SSR in NextJS, you can turn it off. Yes, NextJS was originally built to cater for SSR, but I feel like it's so easy to turn off now that the inclusion/exclusion of SSR by itself doesn't really affect the DevEx of NextJS.
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
// Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
// trailingSlash: true,
// Optional: Prevent automatic `/me` -> `/me/`, instead preserve `href`
// skipTrailingSlashRedirect: true,
// Optional: Change the output directory `out` -> `dist`
// distDir: 'dist',
}
module.exports = nextConfig
So what's the deal? Magic.
NextJS is very convenient to use. It magically does everything for you. Building a full-stack app on it is like a dream, because it allows you to compose your normally-lives-in-the-backend logic and frontend logic in one place. Once you learn the pattern, you can start chipping away at your app without having to worry about things like route declarations, data serialization, and authorization.
There are things like "Server Actions" which allows you to directly call your server code from your client (browser) code. Under the hood, when NextJS compiles your code, the Server Action method call gets swapped with an API call to your server's Server Action, which has its own POST URL registered as a sub-path. It magically works - and you're none the wiser for it.
At the end of the day, the technique used for Server Actions isn't rocket science - it just automates the tedium of "declaring an API route", "calling that API route", and "handling the network and friends' edge cases" into a simple callMyServerAction()
declaration that either returns a value or throws a error.
But it really freaking helps when you're a solo dev working on a project (like I was) because it cuts the context-switching to the absolute minimum. You're not working on either frontend or backend anymore; you're just working on your app. You don't need to dump your human RAM to remember ExpressJS conventions and route handlers - you just code your function and be done with it.
There's also the obvious benefits of doing SSR, which is that you don't have to deal with the three dreaded variables that are always declared whenever you fetch a data: the data
itself, the isLoading
flag, and the isError
flag.
But... NextJS has too much untraceable magic, even in its doc
Let's pick an example: building an API for your NextJS app. Should be straightforward, right? Let's look at the docs:
Great, so we've figured out how to build an API using... Route Handlers? Why do they call them Route Handlers anyways?
But... How do we check for the path of the APIs? What fields should I use to check the path of the current Route Handler? Is the client calling /api/blahblah
or /admin/api/blahblah
? Wait, I can't check it?
Okay, this does sound like I'm ranting, because I am. I distinctively remember trying to build a common auth handler that can check whether or not a given request is authorized (e.g. coming from my home server, don't @ me for my auth logic), and log the request if it is indeed authorized. Figuring out how to do this is... Not very fun, to say the least.
There's a lot of magic that NextJS does under-the-hood that I can't seem to decipher. Maybe it's just a skill-issue, but I'd like to believe that this is a common complaint echoed by other NextJS devs based on the things I see on Reddit. It's worse than Spring Boot, and Spring Boot is (or should have been) the King of magical-things-that-just-work-and-no-one-can-figure-out-why.
But hey, at least SpringBoot has really extensive docs and a really large community - though that's probably because SpringBoot has been around for way longer. Or maybe it's because NextJS has only recently released App Router and that's why their docs are fragmented as hell.
The magic leads to unexpected behaviours
So, continuing my auth middleware story: I figured out how to implement it. Sort of, anyways.
I put my auth logic in my Route Handler's parent's layout.tsx
(oops).
When I was procrastinating, I tried to figure out how my Route Handler (which I used to upload images onto my S3 bucket) was actually implemented for NextJS. I'm not going to bore you with the details, but the gist is that NextJS generates an API path that both the NextJS server registered and the NextJS React app recognises (and which the latter sends a POST request to).
When I tried sending a POST request to my Route Handler's API, it worked, much to my dismay. Fuck - anyone can upload an image to my S3 bucket.
It turns out that layout.tsx
doesn't actually protect Route Handlers. That's fine, I guess, and it makes sense in retrospect (why would a UI layout component be combined with Route Handlers?), but I felt like no-one really warned me through NextJS's docs. Either that, or I need to git gud at RTFM.
So I ended up implementing it using NextJS's middleware, which sort of goes like this:
export const middleware = async (req: NextRequest) => {
// authorize requests if we're accessing the admin panel on /admin
// this is not the buggy version btw - this is the final version. can't find the buggy version unfortunately.
if (req.nextUrl.pathname.startsWith('/admin')) {
const ip = req.headers.get('x-forwarded-for') ?? ''
if (!(await validateIp(ip))) {
console.error('Unauthorized IP=' + ip)
return new Response(`Unauthorized. IP=${ip}`, { status: 401 })
}
}
// load analytics cookies
// ...
return NextResponse.next()
}
The very unfortunate problem of using middlewares in NextJS is that you can only ever have one middleware for your entire NextJS app. If you want custom middlewares for each path, you have to implement your own path-matching logic.
And so I implemented it, and I fucked up. And I caused our entire site to go down.
Anyways, after fiddling with my path-matching logic and deploying the fix, I got the whole app working. I haven't encountered any major issues after having it in production for 3 months.
But it genuinely scares me, because I don't know what other footguns I'm going to shoot myself with Next (hah!). I don't feel reassured at all that the docs have told me the whole truth.
There's also other footguns that I've encountered, which I'll share in the last section just so that the main one doesn't get too long.
Why don't people talk about these pitfalls?
I think early iterations of NextJS used to not be so complicated (based on my memory and a few Reddit posts that complained about how bloated NextJS got). So NextJS did deserve its place as the "de-facto standard for building React apps", because it had a lot of sane defaults (directory structure, SSR, to name a few).
Somewhere along the line though, Vercel decided to commercialise. It's not really a bad thing at its core - Expo (from the neighbouring React Native community) did this as well. But Expo managed to handle this "need to make money" quite well: you pay Expo to get your RN apps built by them, and they also let you self-host your own CI/CD infra. Expo's value-proposition made sense even to most senior devs, because building mobile apps require one to have a PhD given all of the moving parts required to even start the compilation process.
Vercel on the other hand, offers to host your app for you. Which also means hosting your APIs/backends, your analytics, your auth and your databases (and a bunch of other things which I can't remember). All of which are things that senior devs can do decently well but mid-level devs might struggle with.
Unfortunately, that means that their platform is a vitamin to senior devs, not a cure. So they had to go a different direction with Vercel: they had to cater to people who have no time nor the expertise to setup their own web app. They built a magical one-stop-shop that has all the features you need without all the hassles.
In their quest to be the "one-stop-shop" for their paying users though, Vercel must've gotten confused and slightly too creative with their commercialisation, because they decided to introduce features that are meant to encourage people to onboard onto their services.
For example: middlewares. I have no idea who asked Vercel for middlewares to be compatible with edge runtimes, but they decided to introduce the limitation on NextJS's native middleware functionality. Apparently, their Serverless Function product uses edge runtimes, and so you need to use edge runtimes on your middleware, too. Even if you use your own infra on a beefy EC2.
Another example: some of their docs - I think it's the one around databases - have tutorials which assumed you were using their paid plans, and that you were happy with importing their paid plans' SDK just to get your connection pool going.
But again, as far as memory serves, I'm mostly complaining about things that are all fairly new (around 1-2 years). I get it, Vercel's employees need to feed their families, but I feel a bit icky seeing all the upsell that Vercel tries to shoehorn onto (mostly junior) devs' mouths. The direction that NextJS is going scares me. And I feel like the complexity is getting bigger and bigger just so that they can justify their development cost and paid offerings. "Do you have troubles with ISR? Let us handle it for you!"
I don't think Vercel is an evil company, FWIW. I think they're in this weird state where they have to make money, but their one cash-cow is also meant to be the "de-facto standard for building web-apps", which also means that it needs to be accessible for everyone, even the people who are scared of Vercel making money.
So... Should you use NextJS?
I admit: my ramblings have gone a bit too far.
Generally speaking, use NextJS if:
- You're a cash-strapped startup
- You're building something that people are planning on searching for on Google (or MSN, hehe)
Don't use NextJS if:
- You're planning on having people access it directly through a URL or through a landing page somewhere (something like an internal dashboard app that isn't meant to be public).
- Your users aren't meant to navigate between pages a lot (sort of like how in Miro you're mostly sitting on one page trying to perfect that diagram).
- You're planning to embed it as a mobile app's web-view. See point (1) - you're building a web app in a webview, not a web site.
- You just want to fucking ship without taking a PhD on NextJS-ology. Compared to the traditional Vite (or even create-react-app) React toolchain, It's easier to get started on NextJS, but it's way harder to develop a final working product using it.
If you are assuming that I wrote this article just so that I can rant about NextJS and Vercel, you are correct. But I hope this section has provided a little insight over how it's like for me when using NextJS. It's like the equivalent of trying to figure out how to fly a Boeing 747, whereas using create-react-app is like driving a car for me.
Appendix: NextJS footguns
- ISR will (probably) kill you. NextJS caches data & server components based on the path of your route. If your
<Admin/>
component is under/admin
, you better turn off ISR for that because there's a chance User A might be able to see User B's admin dashboard. And yes, it's turned on by default, unlike most caching mechanisms. - Middlewares only support edge functions, which means you only have a minimal NodeJS runtime even if you're running it in your own EC2 instance.
- SSR might be incompatible with your desired React design/component lib.
- Be careful of memory leaks - these things are magical and hard-to-debug (though perhaps that's a skill-issue thing...)
- ...and probably other things that I can't be f'd to remember