We placed our pages in Next's "pages" directory for years. This is about to change now.
A while ago, Next.js introduced the new App Router, significantly changing how we create pages. But not only the directory in which we store our app's pages changes — but also the available feature.
Where our Next projects used to look like this:
└── pages
├── about.js
├── index.js
└── team.js
Using the App Router, our app's structure looks similar to this:
src/
└── app
├── about
│ └── page.js
├── globals.css
├── layout.js
├── login
│ └── page.js
├── page.js
└── team
└── route.js
The conventions for creating an app are as follows:
- Every page of our app has its own directory. The directory name defines the URL path.
- The component that is rendered when accessing the path in the browser is page.js.
- We can store other components in the path's directory. These will not affect routing if they are not named page.js.
- A couple of files with reserved names can be placed in each page's directory. All of them hold a particular functionality. For example, there are loading.js, template.js, and layout.js — we will discuss the latter in a second.
So far, so good; we've covered the conventions.
As you can tell, slight refactoring is necessary for existing apps. But what is the motivation for doing so? Here are the new features usable in the App Router.
App Router features
Vercel announced a couple of great features lately. Most of them are exclusive to the App Router — they can't be used in the classic Pages Router.
Here is a list of the exciting things we can now do.
Client and server components
Any components in the app directory are now server components by default. But what does that mean? Here is a small recap.
Server components are rendered on the server. All of their code stays on the server — meaning we can't use client-side features like the window object or typical hooks in React. Server components lack interactivity with the client. They even fail when just defining a hook:
![Next.js server component useEffect bug](https://miro.medium.com/v2/resize:fit:700/1*kPb7pek3cpMKzVatQFzuxQ.png)
Client components are the opposite and similar to the previous type of components we had in Next.js. They can use the browser, provide interactivity, and ship their JS code to the client.
While all components in the app router are server components by default, client components can be declared by stating "use client" on top of the file.
This differentiation only works in the new app router. Here is a quick overview:
Client-components:
- Browser APIs
- Event listeners
- All React hooks
- Great for generating a bunch of HTML in the client
Server-components:
- Great for hiding code and secrets
- Don't ship most of the dependencies
- Direct access to the backend
- Fully integrate server actions
Easier layouting
I already mentioned the layout.js file, which can be in each path's directory. This component makes layouts easy, as the path component is automatically applied to the provided layout. Let's see an example.
In a path directory of our choice, we create a layout.js:
export default function LoginLayout({ children }) {
return <div className='login-area'>{children}</div>
}
All it needs to do is render a child component, passed automatically — the child is the page.js component. The page.js is entirely up to us. As the layout is applied automatically, we don't need to specify anything in this file.
Nested layouts
There is much more to the new layout files. A layout component can be applied to more than just one page. If sub-directories don't specify a layout on their own, the top-level layout is used. Here is an example:
src/
└── app
├── market
│ ├── buy
│ │ └── page.js
│ ├── sell
│ │ └── page.js
│ ├── layout.js
The pages "/buy" and "/sell" feature the same layout.
Server actions
Server actions are, at this time, still an experimental feature. They enable easy execution of server-side code based on events on the client:
export default function Home() {
async function serverAction() {
'use server'
console.log('server action executed')
}
return (
<form action={serverAction}>
<button type="submit">
Call server action
</button>
</form>
)
}
Press the button and the serverAction
function is executed.
At this time, server actions need to be enabled in the config.
const nextConfig = {
experimental: {
serverActions: true,
},
}
Intercepting routes
Intercepting routes do what their name suggests: Intercepting a request for a route. This feature enables us to build pages that behave differently based on some other factors. Vercel themselves implemented an exciting example: https://nextgram.vercel.app/
When you click on an image in the gallery, it is opened as a modal—also, the path changes, addressing the exact image.
![NextGram Next.js intercepting routes example](https://miro.medium.com/v2/resize:fit:700/1*YKFFKWxSpTQj0V_qT9TdFg.png)
If you refresh the page, it is rendered as a standalone page. Yet, the path didn't change. We receive different behavior for the same path, as the route got intercepted:
![NextGram Next.js intercepting routes example](https://miro.medium.com/v2/resize:fit:700/1*6utd1wP-ylDnUMdgz4_BpA.png)
Parallel routing
Parallel routes are most potent when combined with intercepting routes. The parallel routing feature itself doesn't sound too exciting:
Parallel Routing allows you to simultaneously or conditionally render one or more pages in the same layout - Official documentation
The feature can be great for combining two or more pages that can be viewed independently. The pages are still independent — therefore, they have separate code, even though rendered on the same URL.
The Pages Router
The Pages vs. App router is not an equal debate. Instead, the App Router is the next step and should become the standard way to build apps.
A couple of beloved features are bound to the Pages Router and, therefore, will not prevail:
- getStaticProps
- getServerSideProps
- getStaticPaths
- Custom document component
- Custom app component
- next/head
The good news is that both routes can be used alongside. Vercel even recommends keeping the pages router while migration, as to not break anything.
Our custom document and app component can be replaced by the root layout.
The SEO-focused next/head component is entirely redundant. The new App router offers built-in support for metadata like this:
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Hello World',
}
export default function Page() {
return <></>
}
The functions getStaticProps
, getServerSideProps
and getStaticPaths
can be replaced by the new server actions.
Performance
When it comes to the performance battle of the pages vs. app router, we see something interesting. The app router is performing worse.
To conduct a simple benchmark, I created two projects doing the same thing: Rendering a number of HTML tags passed by the URL.
Here is the result:
![Pages vs. App router Next.js performance](https://miro.medium.com/v2/resize:fit:700/1*mzJhd3MLYLPyOmT6qxUmcg.png)
Especially for 100 and 1000 rendered tags, the percentual difference is quite considerable. Fascinatingly, for more tags, both performances seem to get more closely.
This benchmark was inspired by Jack Herrington, and I recommend you to check out his in-depth performance video.
Summary
Over time, we have to make friends with the new router.
While transitioning from pages to the new approach takes some work, the new features are worth it. Yet, the established router might still be a solution for new projects if you are comfortable with its features.
Regarding potential deprecation, I am not stressed. The support for the pages router might always be there, as Next.js already got two separate pieces of documentation for both models. Yet, new features might become exclusive to the app router.
Thank you for reading! More on the new Ap Router and its features:
More content at PlainEnglish.io.
Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.