Telerik blogs

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.

Migrating from Pages to the App 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:

  • src/pages/index.js -> /
  • src/pages/about.js -> /about
  • src/pages/contact/index.js -> /contact

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.

Dynamic Routes

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;

Static Site Generation (SSG), Server Side Rendering (SSR) and Incremental Static Regeneration (ISR)

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.

getStaticPaths

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} />
}

API Routes

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) {}

Conclusion

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-2
About the Author

Thomas Findlay

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.

Related Posts

Comments

Comments are disabled in preview mode.