Learn how to upgrade your Next.js project today and enjoy the benefits of the App Router and React Server Components when you migrate from the Pages directory.
Next.js is a full-stack React framework that offers many great features, such as static site generation, server-side rendering, file-system routing and more. In version 13.4, Next.js released a stable version of the App Router, which changes how developers can build their applications.
The App Router introduced a new way of creating pages with nested routes and layouts. It offers simplified data fetching, streaming and React Server Components as a first-class citizen. It’s clear that App Router is the direction where Next.js is heading, so let’s have a look at how to migrate to it from the Pages directory.
Next.js makes it very easy to create new pages in an application. It uses the file-system as an API and creates routes for every file in the pages directory. Let’s take the following folder structure as an example:
Pages—directory structure
src/
└── pages/
├── index.js
├── about.js
└── contact/
└── index.js
For these files, Next.js would create these routes:
It’s a bit different when using the App Router. All the code for the App Router needs to be placed inside the app
directory. Here’s how we would create the aforementioned pages with the App Router.
App Router—directory structure
src/
└── app/
├── page.js
├── about/
│ └── page.js
└── contact/
└── page.js
Every page component needs to be called page
, and its extension can be either js
, jsx
or tsx
. Similarly to the Pages directory, the folder names are segments used to create route names, but the component files always need to be called page
.
There are situations when we might not know the exact name of a URL segment. For instance, we could have a page that should display the user’s profile based on the user’s ID in the URL. That’s where dynamic routes come into play. In the Pages directory, this can be achieved by wrapping a folder’s or file’s name with brackets: [id]
or [slug]
.
Pages—dynamic routes
src/
└── pages/
├── user/
│ └── [id].js
└── article/
└── [slug]/
└── index.js
To get access to the user’s ID from the URL in the Pages directory component, we need to use the Next.js router, as shown below:
Pages—accessing URL params
import { useRouter } from "next/router";
const UserProfile = props => {
const router = useRouter();
return <div>User ID: {router.query.id}</div>;
};
export default UserProfile;
And here’s how we can achieve the same thing with the App Router.
App Router—dynamic routes
src/
└── app/
├── article/
│ └── [slug]/
│ └── page.js
└── user/
└── [id]/
└── page.js
Since every page component needs to be called page
, we need to add brackets in the folder names. The code below shows how we can access the dynamic URL params:
App Router—accessing URL params
const User = props => {
const { params, searchParams } = props;
return <div>User ID: {params.id}</div>;
};
export default User;
We no longer need to use the useRouter
hook, as params
and searchParams
are passed via props.
A very important thing to note is that every component in the App Router is, by default, a React Server Component (RSC). This means that the components are rendered on the server only. There is no hydration process on the client side, and any JavaScript code related to the server component is skipped and is not sent to the client. If you don’t know what React Server Components are, check out this page.
If you would like to change a component from RSC to a client component, you can do so by adding the "use client"
directive at the top of the file.
"use client"
const User = props => {
const { params, searchParams } = props;
return <div>User ID: {params.id}</div>;
};
export default User;
Next.js can generate static pages at build time or dynamic pages at runtime on the server. In the pages directory, there were three main approaches to fetching data before generating pages—getStaticProps
with or without revalidation and getServerSideProps
. getStaticProps
is called when the application is built with the next build
command, while the latter is called at runtime when a user makes a request to see a page.
However, it’s possible to return the revalidate
property from getStaticProps
to turn a page from static site generation (SSG) to incremental static regeneration (ISR). The difference is that instead of generating the page at build time, the page is generated at runtime, just like with getServerSideProps
. The page is regenerated based on the revalidate
property. Let’s have a look at how we can migrate these to the App Router.
The example below shows how to use getStaticProps
to fetch and display a list of posts.
Pages—getStaticProps (SSG)
export const getStaticProps = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return {
props: {
posts: await response.json(),
},
/*
With the revalidate property, the component will use ISR; without it, it will be just SSG.
*/
revalidate: 60
};
};
const Articles = props => {
const { posts } = props;
return (
<div>
<h1>Articles</h1>
<ul>
{posts.map(post => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
);
};
export default Articles;
Pages—getServerSideProps
export const getServerSideProps = async context => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${context.params.id}`
);
return {
props: {
user: await response.json(),
},
};
};
const UserProfile = props => {
const { user } = props;
return (
<div>
<div>User ID: {user.id}</div>
<div> Name: {user.name}</div>
</div>
);
};
export default UserProfile;
In the App Router, we don’t use getStaticProps
and getServerSideProps
. Instead, all data fetching needs to be done in Server Components.
Normal React components have to be synchronous, but React Server Components can be asynchronous. Therefore, we use async/await
to perform API requests inside of components and then use the fetched data to return JSX markup.
Below, we have an example of how to implement an equivalent of getStaticProps
.
App Router—getStaticProps equivalent
const Articles = async props => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
cache: "force-cache",
});
const posts = await response.json();
return (
<div>
<h1>Articles</h1>
<ul>
{posts.map(post => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
);
};
export default Articles;
Inside of the Articles
component, we make an API request using fetch
. The key part here is the cache: "force-cache"
value. Next.js extends the native Fetch
API to enhance the functionality of server components. In this scenario, the data returned from the API will be cached.
It’s worth noting that all requests performed using fetch
that do not specify the cache
value will be cached by default. Therefore, cache: "force-cache"
, can actually be omitted.
In a situation when data should be revalidated, we can specify the next.revalidate
option and set it to a number of seconds. The code below shows how to revalidate cached data every 30 seconds.
App Router—getStaticProps with revalidate (ISR)
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
next: {
revalidate: 30
}
});
Last but not least, we have getServerSideProps
equivalent.
App Router—getServerSideProps equivalent
const User = async props => {
const { params, searchParams } = props;
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${params.id}`,
{
cache: "no-store",
}
);
const user = await response.json();
return (
<div>
<div>User ID: {user.id}</div>
<div> Name: {user.name}</div>
</div>
);
};
export default User;
The only difference from the getStaticProps
equivalent example is that instead of passing cache: "force-cache"
to the fetch
, it needs to be set to no-store
.
A Page component can export a function called getStaticPaths
to define dynamic paths for pages that should be generated at build time.
export async function getStaticPaths() {
return {
paths: [
{
params: {
slug: 'hello-app-router'
},
params: {
slug: 'migrating-from-next-pages'
}
}
]
}
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://domain.com/articles/${params.slug}`)
return {
props: {
article: await res.json()
}
}
}
export default function Article(props) {
return <DisplayArticle article={props.article} />
}
When using the App Router, we need to call the function generateStaticParams
instead of getStaticPaths
.
App Router—getStaticPaths equivalent
export async function generateStaticParams() {
return {
paths: [
{
params: {
slug: 'hello-app-router'
},
params: {
slug: 'migrating-from-next-pages'
}
}
]
}
}
export default function Article(props) {
const res = await fetch(`https://domain.com/articles/${props.params.slug}`)
const article = await res.json()
return <DisplayArticle article={article} />
}
Creating API endpoints is a breeze in Next.js, as it uses the file system to generate API endpoints. To create an API endpoint in the Pages directory, we need to create a file that exports a handler in the src/pages/api
directory.
Pages—API route structure
src/
└── pages/
└── api/
└── articles.js
Pages—API route example
export default async function handler(req, res) {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
res.status(200).json(await response.json());
}
Creating API endpoints in App Router is quite similar but with a few differences. All API route files need to be named route.js
. Therefore, to achieve the same API Route as we just did with the Pages, we would need to create the route.js
file in the app/api/articles
folder.
App Router—route handler structure
src/
└── app/
└── api/
└── articles/
└── route.js
The route.js
file also needs to export a handler function, but there is an important difference. Instead of using the default
export to provide a handler function, the App Router API Routes need to export a function that are named like HTTP methods.
App Router—route handler example
import { NextResponse } from "next/server";
export async function GET(request) {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return NextResponse.json({
posts: await response.json(),
});
}
Next.js supports the following HTTP Methods: GET
, POST
, PATCH
, DELETE
, HEAD
and OPTIONS
, and actually an API Route handler can export multiple handlers.
export async function GET(request) {}
export async function POST(request) {}
export async function PATCH(request) {}
export async function DELETE(request) {}
Migrating from the Pages directory to the App Router in Next.js offers enhanced route control, dynamic routes, API routes, flexible middleware handling and improved rendering strategies with SSG and SSR. By adopting React Server Components, we can optimize performance and interactivity. Upgrade your Next.js project today and enjoy the benefits of the App Router and React Server Components.
Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “React - The Road To Enterprise” and “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.