Next.js App Router. Experience of use. Path to the future or wrong turn
Two years ago, the Next.js team introduced a new approach to routing, which was supposed to replace the so-called Pages Router and added a range of fundamentally new functionality.
Practically in every release, I found plenty of useful and necessary things for both personal and commercial projects. Nevertheless, I bypassed the 13th version for commercial projects, as the functionality proved to be extremely unstable and insufficient. However, now this functionality has been moved to the stable category, the App Router is considered the main one, and the Pages Router is rather supported for backward compatibility and gradual transition.
Next.js has taken a big step, taking responsibility for caching and working with requests, adding server components, introducing parallel and interception routes, as well as a series of other abstractions. This article will discuss the reasons for this step, the possibilities, problems, and personal opinion — was this a step into the future or a step straight into a pit.
Retrospective
Before diving into the latest update, it’s worth describing a bit of retrospect. I have been using next.js since version 8, for about five years now and have closely followed all subsequent updates, I delve under its hood for understanding (and occasionally fixing) problems and make additional insignificant packages. My impressions of the new version are almost always repeated — from “what the hell is that” and “who did this!”, to “this is brilliant!” and back around the circle.
What I definitely understood over these years — next.js cannot be called the ideal of stability. If the functionality has moved into the stable categories — it means you should wait a couple more versions until its main errors are fixed (if you’re lucky). I encountered a dozen bugs, some of which were the reason for rolling back months of team work, and some were a solid basis for temporary crutches (well, you understand).
The number of fixed bugs even became an item of recent releases.
Regarding the stability of Next.js, an interesting article was recently published by one of the Remix developers (at the moment former) Kent Dodds, to which subsequently responded the VP of Vercel — Lee Robinson. Personal opinion about this dispute will be at the end of the article.
The framework has been very actively developed all these years. Frequent major updates have led to the emergence of a large number of bugs, but at the same time to the rapid implementation of very useful and promising functionality (there is no better test environment than prod).
For example, with the appearance of rewrites and redirects, it became possible to abandon frequent changes to nginx, with the implementation of middleware — rewrite the routing handling logic and improve the capabilities of a/b tests. With the advent of ISR — to make the possibility of updating pages without re-deployment of the service, with its improvement (on-demand ISR) — significantly simplify it.
Next.js Today
No matter how many problems I encountered, next.js continues to be the most technologically advanced framework. It’s a multitude of useful functionality that covers the absolute majority of needs. And with each update, this coverage area only grows (precisely in terms of size, not quality).
The most important update of recent versions, perhaps, can be called server components, even despite the fact that this is the development of the react.js team.
Server components provide the ability to perform complex logic at the build stage without additional overlays. With them, it is not necessary to throw large packages to the client, worry about secrets or optimize translations — this will be the server’s responsibility.
Despite all the usefulness of server components, they have one huge minus for use in real projects — the lack of contexts, due to which data has to be thrown tens of levels of nesting, including paths and page parameters. Partly this was circumvented by making the next-impl-getters package, but, of course, such functionality should not exist separately from the framework.
In addition to participating in the development of server components, Next.js develops and supports dozens of its solutions, and if we talk about version 14, then it is exactly the App Router — a new approach in routing configuration, which combines the possibilities of build, server, and runtime.
App Router
An important feature of Next.js is its routing — or rather, the use of the file system as a configuration of this routing. Previously, everything about this was simple — the path to the file where the page is located is the path through which it will be available on the site. In the new version, routing has become more complex, and the first thing that violates this logic is groups.
Groups
Groups allow you to combine pages by any feature. This allows, for example, to create different layouts for pages at the same level.
Layouts and Templates
Layouts are a kind of wrapper for all pages below in the directory — this is an alternative to the previous abstractions _app and _document. An important feature of them is that they are not re-rendered when switching between pages. This is also their minus — if the common layout has any difference depending on the pages — it will not be possible to use this abstraction.
Unlike layouts, templates are rebuilt every time. However, dynamic parameters cannot be obtained in them (the package solves this problem, but again it should be out of the box). As a result, both of these abstractions cover far from all cases and it is necessary to add a common component to all pages.
Parallel Routes
Parallel routes are a mechanism that allows you to load several independent and independent slots on one page. They can have their own templates, error handlers, and loaders.
Intercepting Routes
Intercepting routes are used in conjunction with parallel ones. They give the ability when a user moves from one page to another, to “intercept” it and instead of loading a full-fledged page to display something else, for example when moving to an image to intercept this and display a modal with it, and after reloading to show a full-fledged image.
Other Abstractions
For directories, they also added the ability to configure errors and loaders (and dynamic parameters are also inaccessible in them). This works both with full-fledged pages and with parallel routes.
Working with data and queries
Another noteworthy change is the rework of fetch (again). Now, calling fetch triggers a next.js wrapper, which modifies the request, processes it and caches the result. All this happens incrementally, i.e. the last saved response is immediately returned upon request, and the request for current data is executed in the background.
Much of the fetch rework is tied to the fact that during the build, Next.js simultaneously builds dozens of pages, which often call the same requests. Previously, to avoid this, you had to write your own custom loader class and use it everywhere.
Caching was also used in many other places, such as processing and rebuilding pages, handling routes, redirects and rewrites, determining request status, etc.
I don’t know why libraries have recently started taking on extra responsibility — whether it’s Next.js caching or working with forms by React.js. It’s a strange attempt to fixate what used to have hundreds of variations, each with its own peculiarities, assuming it will be better. Subsequently, the Next.js team added the ability to disable caching or configure it independently, but only for part of the functionality.
Conclusions
Next.js has been enriched with capabilities and optimizations and now covers even more situations. However, without server contexts and dynamic parameters in many abstractions — the potential is significantly curtailed.
The rework of the basic API — fetch — and enhanced caching have made it possible not to worry about performance, but still only in small projects, in large ones you often stumble upon the shortcomings of this solution (eg cached rewrite when not needed, redirects wrong, caching limit, returning an outdated page despite disabled caching).
The Next.js team has started paying more attention to bugs, but to a large extent this has happened due to the sharp increase in these bugs. On the whole, however, the new functionality is largely complete. Another question — are you satisfied with the logic embedded in it and there is no definite answer here. Some people found these changes and their problems critical, so they continue to use the Pages Router, some found it insufficient and had to use a number of hacks (and I am in this group), and some people find these solutions perfect, as the project does not fall under problem areas.
Opinion on the constituents of the dispute
I read Lee Robinson’s arguments several times, but to my surprise, I didn’t find answers to many questions, so I’ll probably go through the list of problems from Kent (which I didn’t find the answer to in Lee’s article):
1. “instead of recommending using the web platform’s Stale While Revalidate Cache Control directive, they invented a highly complicated feature called Incremental Static Regeneration (ISR) to accomplish the same goal”
Why is this done so? The fact is that cache-control is a header, the responsibility for which lies with the browser and, if we passed stale-while-ravalidate with a value of 1 day, then the browser will not update the cached for the current user for a day. In the paradigm of next.js, the cache can be updated not just at a specific moment immediately for all users, but at any moment by calling the on-demand ISR functionality. Among other things, this caching logic applies not only to the browser, but also to requests from the server or during build step.
The storage can be a file system, cloud storage, or user settings. So despite the similarity of ideas, their logic differs (and in my experience, in a better way in this context).
2. “OpenNext exists because Next.js is difficult to deploy anywhere but Vercel”
Next.js itself is incredibly easy to deploy — yarn build, yarn start. That’s all. No special environment features, no secret dependencies. OpenNext is trying to replicate Vercel capabilities, no more.
At the same time, it is worth recognizing that in next.js there is some dependence on Vercel. For example, I mentioned above that cloud storage can be used as storage. And inside next.js there is logic for using Vercel’s cloud storage. However, by default, the file system is used, and if desired, you can easily configure your own cloud storage or, for example, connect Remix (which was suggested in the latest release). There are a few such places, but they are there, in any case for all of them there is a default option, which is more than enough.
3. “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”
Despite the tremendous efficiency of this collaboration, I have to agree that the line between next.js and react is blurring — if before you could say that Next.js is a test environment for React.js, now it feels like Next.js themselves are promoting ideas in react to use them. It is next.js who talk about server components and server actions, from which it creates the feeling that this is their development. Perhaps the upcoming React Conf will correct this situation.
Three key React.js developers were hired at Vercel — Andrew Clark, Sebastian Markbåge and Josh Story. However, we’ve heard about server components for a very long time and the work began before these developers moved to Vercel.
4. “Next.js violates this principle in many ways. One example of this is the decision to override the global fetch
function to add automatic caching. To me, this is a huge red flag”
Here was an answer from Lee: “In Next.js 14, for example, if you want to opt out of caching, you would use noStore()
instead of [option] cache: 'no-store'
at fetch
".
But I don’t understand how this answers the problem. I also don’t see objective reasons why the Next.js implementation was not exported to a separate API, f.e. a fetchNext() with a linter warning when using a regular fetch (just like they did with the Image tag instead of manual img).
5. Stability and Complexity
Stability is dedicated to a significant part of the article above. In terms of complexity, I want to once again acknowledge the amazing documentation of next.js, where it is very easy to search for answers. I see how PR is offered to the next.js documentation every day, even for a minor difference in wording, making it not just convenient, but also as clear as possible.
Postscript
In addition to next-impl-getters I started working on other wonderful packages:
next-impl-config — next.js essentially works in 4 environments — build, server, client and edge, with configuration described only for two of them — build and server. This package gives the opportunity to add settings for each possible environment.
next-classnames-minifier — due to the peculiarities of caching next.js it is difficult to configure class compression to symbols (.a, .b, …, .a1) and to solve this task this package was made, which was dedicated to the recent article.
next-translation — I never really liked existing solutions in the context of next.js and they stopped liking them even more now, with the advent of server components. This package was designed primarily with server components in mind and maximum optimization (due to the transfer of logic to the assembly stage and/or server side).
UPD: Added server contexts to next-impl-getters.