Kent C. Dodds recently published a new article Why I Won't Use Next.js.
In the post, Kent shares his opinions on why he's recommending using Remix instead of Next.js. I wanted to share my thoughts on the post and make the case that:
- Learning Next.js helps you learn the web platform
- All Next.js features work self-hosted
- Server Components and Server Actions are independent of Vercel
- The React canary channel is stable for frameworks like Next.js to adopt
- Server Components are production ready
Background
Kent is a prolific educator and previously a co-founder of Remix. His new course, EpicWeb.dev, helps you learn how to build full-stack web applications using Remix.
Kent is an incredible member of the React community. I've learned a lot from him over the years (especially his material on testing) and this blog actually uses a library he created, mdx-bundler
, so thank you.
Excited to go on with @leeerob for the keynote at @reactathon in person! Catch the live stream right now!
If you're new here, I'm Lee. I work on Next.js. I've also made some courses about using Next.js before I joined Vercel.
Both Kent and I are passionate about the tools that we use. As Kent shared in his post:
As Next.js is a very popular alternative to Remix, people ask me why I chose Remix instead of Next.js for the framework I teach on EpicWeb.dev. These people are probably facing one of those scenarios I mentioned. So this post is for those people.
Similarly, I'm often asked about my opinions on Next.js versus other frameworks. This post is for the folks in the Next.js community who are wondering about some of the points Kent brings up.
The Web Platform
First, it's important to call out that Remix has pushed the React community forward in it's understanding of the web platform and web APIs.
Next.js v1 was released in 2016 (just had the 7-year anniversary) and at the time, the Node.js request/response objects were the de-facto way to build a server-rendered JavaScript framework. We were writing React class components, as well.
Remix v1 was released in 2021. A lot has changed since then. The web platform is more powerful than ever, and there's an entire new generation of developers learning the web Request
and Response
APIs first instead. They're able to learn once, and write the same JavaScript everywhere, reusing their existing knowledge. I love that.
Kent talks about how he prefers tools that give access to the underlying primitives, rather than wrappers, based on his past experience. I can empathize.
Where Next.js has utilities to allow you to interact with the request, headers, cookies, etc, Remix exposes those APIs directly to you through its
loader
s andaction
s. In Remix, these functions accept aRequest
and return aResponse
. If you need to understand how to return JSON with some set headers, you go to MDN (the de facto standard web platform documentation) rather than the Remix docs.
This is absolutely true for the Next.js Pages Router. However, a lot has changed since then. Let me explain.
Next.js Pages Router
The Next.js Pages Router was introduced in 2016. You'll notice many parts of the framework that feel closer to Node.js than web APIs. For example, you've been able to eject from the default Next.js server to have your own Express server since the beginning.
This design choice continued for many years. Next.js 9, released in 2019, introduced API Routes. These endpoints built on the Node.js request and response APIs. The API is similar to Express, as many folks were familiar with this API at the time.
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from Next.js!' });
}
API Routes and the ejected server still work today. But frameworks must evolve over time based on community feedback, how the ecosystem moves, and new functionality available in the web platform.
That's why Next.js 12, released in 2021, introduced Middleware, which is built on the Web Request
, Response
, and fetch
. It didn't make sense to build new APIs that weren't embracing the now standardized web APIs. Note: this was the same year Remix v1 was released. We agree!
Next.js App Router
I mentioned evolution, right? Kent mentions in the post:
I've been an outsider to the Next.js framework for years. It's been a long time since I shipped something with Next.js myself.
Totally fair. There's only so many hours in the day for family, work, and fun. So I don't expect Kent (or anyone really) to have kept up with the chronology of Next.js here like I have. But let me explain further about where we're headed.
After 6 years of feedback, Next.js 13 introduced a new foundation for the framework with the App Router. The Pages Router didn't go anywhere, but again, the framework must evolve with the times.
If you were designing a new framework, how would you handle reading values from the incoming request, like cookies, or headers? And how would you allow developers to write custom API endpoints?
Well, you'd use the standard web APIs, of course. Going back to Kent's feedback:
Where Next.js has utilities to allow you to interact with the request, headers, cookies, etc, Remix exposes those APIs directly to you through its
loader
s andaction
s.
Again, definitely true for the Pages Router. Inside getServerSideProps
, the equivalent for a Remix loader
, you can't use web APIs to access headers or cookies. It's Node.js APIs still.
export async function getServerSideProps({ req, res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
);
return { props: {} };
}
So you're designing this framework. You want to use web standard APIs. And you also want to make it easy for developers to do the right thing. Kent says:
To boil that down to a principle, I would say that instead of wrapping the platform APIs, Testing Library exposed the platform APIs.
We agree. But why couldn't we have both?
In the Next.js App Router, let's say you want to create an arbitrary API endpoint. These are now called Route Handlers. Here's what they look like:
export async function GET(request: Request) {
const res = await fetch('https://api.leerob.io/...', { ... } )
const data = await res.json()
return Response.json({ data })
}
Route Handlers accept a web Request
and produce a web Response
. But what about cookies, headers, and more?
Since you have access to the web APIs directly, you can browse MDN and reuse all of the knowledge you've learned from Next.js. Also, ChatGPT is really good at creating these APIs.
export async function GET(request: Request) {
// Read headers
const token = await getToken(request.headers);
// Set cookies
return new Response('Hello, Next.js!', {
status: 200,
headers: { 'Set-Cookie': `token=${token.value}` },
});
}
We can also provide abstractions that can be composed, allowing you to write reusable functions without having to pass headers as a function parameter. You can choose whichever you prefer.
import { cookies, headers } from 'next/headers';
export async function GET(request: Request) {
const cookieStore = cookies();
const headersList = headers();
const token = cookieStore.get('token');
const referer = headersList.get('referer');
}
That cookies()
function returns the same underlying Headers
web API. It can be used in both Route Handlers, as well as other server-side code in the App Router, like Server Actions or Server Components:
'use server';
import { cookies } from 'next/headers';
export function serverAction() {
cookies().set('name', 'leerob');
}
I strongly agree with Kent. I can't imagine new web frameworks being released that don't embrace web APIs. That's why Remix, SvelteKit, Nuxt, Solid, Astro, Next.js, and many more are doing this.
Independence
Kent mentions a project called OpenNext, which as quoted in the post, describes itself as:
OpenNext takes the Next.js build output and converts it into a package that can be deployed to any functions as a service platform. As of now only AWS Lambda is supported.
The maintainers of OpenNext are building a platform for easily deploying serverless applications on AWS. I would argue that this package is trying to be an open-source infrastructure as code tool, not an open-source framework. Naming is hard.
Kent then says:
OpenNext exists because Next.js is difficult to deploy anywhere but Vercel. I appreciate the company's incentives to make their own hosting offering as attractive as possible, but it's evident that this incentive has deprioritized making Next.js easy to deploy anywhere.
We're always looking to improve self-hosting Next.js. For example, I made a video and example showing how to deploy with Docker to whichever service you prefer.
next start
is how Walmart, TikTok, ChatGPT, Starbucks, Target, Doordash, and many others self-host Next.js.
Now you might be thinking: but Lee, that's not what Kent is talking about. He's talking about serverless platforms. Why wouldn't Next.js build first-party adapters for every deployment target?
Open Source and Framework Boundaries
From my post earlier this year on funding open source:
Developers don't want walled gardens. They want the freedom to eject and self-host. It's about control. This is why all Next.js features work self-hosted. Vercel provides infrastructure and a workflow on top of Next.js. You can host Next.js elsewhere, if you want.
I'm a fan of the model Next.js uses. It's clear how it's funded (through Vercel) and the incentive is aligned (I want to deploy Next.js at some point, maybe I will try Vercel). It's a similar story for Svelte. You want to deploy SvelteKit, maybe you'll try Vercel.
Vercel, the maintainers of Next.js who invest heavily in its research and development, are focused on maintaining and building a default deployment output for Next.js. We aren't doing adapters, although I love this for other frameworks.
Next.js has over 850,000 monthly active developers. And those developers expect things to work well, to have bugs fixed quickly, for new features to be released, for us to respond to their questions in a timely manner, and more.
I keep hearing how hosting Next.js yourself as a nodejs application is a huge pain, and I have no idea where this is coming from. What's difficult? Containerizing it? Creating a deploy pipeline? If you said yes to either, then you likely have trouble hosting *any* app yourself.
I get asked about this a lot (especially since the launch of Epic Web), so I've written it down. Here's why I won't use Next.js: epicweb.dev/why-i-wont-use…
I mean did I somehow do it wrong? I have containerized Next.js apps running on prod for 5+ years.
We want the default build output of Next.js to work well when self hosting, and also well on Vercel. And guess what? They're the same output. Well, almost. Let me clarify.
It took us a while at Vercel to figure out the correct format and boundary between open source framework and infrastructure platform. Our philosophy is called framework defined infrastructure. And critically, the specification that Vercel uses for frameworks is open source. It's called the Build Output API.
This output format powers Next.js, as well as Remix, SvelteKit, and many other frameworks on Vercel. We actually maintain our own Remix adapter too, which transforms the Remix output into this format, plus some other features.
We'll soon be making the default output of Next.js match the Build Output API directly, without the intermediate step. We agree with the importance of keeping these pieces open and accessible, which is why we even open sourced the intermediate step.
Pricing: Correlation or causation?
Kent mentions:
We can argue about whether Vercel is right or wrong about their current approach. But the fact remains that if Vercel's pricing or other things become a problem for you, getting off of Vercel will also be a problem. It comes back down to the incentives.
There are improvements I'd like to make to Vercel's pricing. And we'll have some updates soon! Some pricing changes have already rolled out, like lower prices for our storage products and spend controls. But I understand the sentiment.
This sentiment does not equal causation, though.
If you want to self-host, all Next.js features will work. So, why would someone choose Vercel for Next.js, then? The same reason they'd choose it for Astro, SvelteKit, or even Remix.
If you don't want to worry about infrastructure, Vercel takes care of that for you (plus some other stuff, but that's besides the point). And thanks to framework defined infra, you're not writing a bunch of CDK code or bespoke infra-as-code solutions. You're writing Next.js code that's open and portable to any server, on any platform.
Next.js is like Kubernetes, and Vercel is like Google Kubernetes Engine.
Relationship with React
Kent mentions:
I know for myself, it seems like Vercel is trying to blur the lines between what is Next.js and what is React. There is a lot of confusion for people on what is React and what is Next.js, especially with regard to the server components and server actions features.
Definitely not intentional.
Next.js is placing a large bet on the future of React. The App Router builds on many features the React team has been working on for years. Building and supporting a framework requires a non-trivial amount of work. Redwood is doing the same.
In retrospect, we could have worked more closely with the Meta team on making some docs changes directly to the React docs versus the Next.js docs. Thankfully, this has picked up a lot and we are actively collaborating with the team at Meta. Shoutout to Meta's Learning & Advocacy team.
APIs that Next.js uses, like useFormStatus
, are now documented in the React docs. Even experimental React APIs like tainting are now documented. They're publishing a lot of great docs on their new site (which they worked on for many years).
We'll keep improving here and making the boundaries more clear.
Stability
Kent mentions some concerns about the stability of Next.js, specifically:
So Next.js is building into itself a canary feature, calling it stable, and then sending it off to all your users effectively turning your app into the sentinel species.
He's referring to the React canary channel, which the Next.js App Router (not Pages) builds on. From the React post:
We'd like to offer the React community an option to adopt individual new features as soon as their design is close to final, before they're released in a stable version—similar to how Meta has long used bleeding-edge versions of React internally. We are introducing a new officially supported Canary release channel. It lets curated setups like frameworks decouple adoption of individual React features from the React release schedule.
The canary channel is stable for frameworks to adopt. Then, the framework itself should use semver. This might be another correlation ≠ causation moment, because there is some community pain here.
The App Router rollout has had bumps. Some bugs, things that didn't work, and places where the performance could be better.
This is on me. Not React. And our messaging to the community could have been better. There's a lot more I wanted to say about this, which is what we shared in the 2023 Next.js Conf Keynote.
In short, performance and reliability are still a major focus for Next.js. Kent goes on to mention:
Yes, React Server Components are very cool and I look forward to being able to use them when they're production ready
React Server Components are ready. There's now thousands of the top sites on the web using Server Components in production. The experience getting there could have been better, but they're in production.
Is Next.js Too Magical?
Kent talks about how he's not a fan of Next.js extending the web fetch
API.
I agree with part of this, specifically on adding Next.js specific extensions to the fetch
API. We're looking to move away from this direction based on community feedback.
In Next.js 14, for example, if you want to opt-out of caching, you would use noStore()
versus cache: 'no-store'
on the fetch
. And if you want to use more programatic caching and revalidating features, those will soon be standalone APIs as well.
Complexity & Stability
Kent mentions:
I keep hearing from people they're finding Next.js is getting overly complex.
The App Router is a very different model from the Pages Router. It's almost like a new framework. This was one of the many reasons why we needed to ensure you could incrementally adopt this new router, and that the existing router and foundation would be stable and maintained for many releases in the future.
It's also why we just created a new free course to teach the model.
Kent then goes on to mention:
Next.js is on version 13. React Router (built by the same team as Remix) has been around for much longer and is only version 6. Remix was on version 1 for almost two years and only a month ago hit version 2.
I don't think the number of major versions correlates to whether a framework is stable or not, especially when we've tried to take great care in publishing codemods and upgrade guides when moving between versions.
We publish major versions when a Node.js version is no longer supported (i.e. when their security lifetime has expired). With Next.js 14, for example, the Node.js version was bumped to 18.17
.
We also care deeply about backwards compatibility. The core APIs from day one still work today.
7 years ago today vercel.com/blog/next A walk down memory lane 🧵 The pictured code still works without change…
Kent shares:
Earlier this year, the Remix team shared their plans for getting version 2 features released as an opt-in part of version 1 using a strategy called “future flags”.
I love this. We have a similar process in Next.js with experimental flags. We've also done future flags before in previous releases. Their future flags blog post mentioned is worth reading.
Remix vs. Next.js for Ecommerce
Kent mentions an older blog post the Remix team wrote comparing with Next.js:
When the Remix team rewrote the Next.js ecommerce demo to answer the “Remix vs Next.js” question, it demonstrated really well that Remix resulted in a better user experience with much less code (which is an important input in user experience).
I'm thankful for the Remix team pushing us to improve Next.js Commerce. The Pages Router version needed some work.
I'd recommend re-reading their original blog post and then viewing the codebase for Next.js Commerce and updated demo so you can make your own assessment. I wanted to include a link to the Remix demo, but it appears to be down.
I think it's worth making another comparison. Remix has also learned some new tricks since that article was written, like out-of-order streaming.
The Next.js App Router has out-of-order streaming as well, it's fantastic. It's worked very well for Next.js Commerce.
Conclusion
Phew, that turned out to be a long one. As Kent mentions:
I feel like both are highly capable frameworks
We agree. You can make great web experiences with both.
While I spent most of the post referencing points from Kent, I'd like to end with general reasons why I love using Next.js:
- I never need to write separate backends for projects I want to create. I can build my entire project with Next.js.
- I never have to worry about bundler, compiler, or frontend infrastructure. I get to focus on making great products through React components. And I'm able to use the latest React features, which I personally find to have a great developer experience.
- I am able to update to the latest versions of Next.js and things continue to improve. Performance gets faster and new features get added. The iteration velocity is high. If there are changes, codemods and upgrade guides are provided.
- Next.js provides a bunch of components that help me keep my site fast. Images, fonts, scripts, and now even properly loading third-parties.
I was using Next.js before I joined Vercel, and will likely continue to after Vercel as well. I hope my work in the Next.js community can help push the web forward.
Hopefully this post helped share some insight into how Next.js has been evolving and where we see the framework heading in the future.