DEV Community

Cover image for Implementing SSR in Next.js: Dynamic routing and prefetching
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Implementing SSR in Next.js: Dynamic routing and prefetching

Written by Elijah Agbonze✏️

Since its inception in 2016, Next.js has seen massive growth and adoption across the dev community. It is one of the most popular and, arguably, the most efficient React framework available. It has key features like a built-in routing system, an ability to write server logic in the same project as your client.

With frameworks like Next.js, you can build complex, multiple-page applications without the need for external libraries. When using just React, you'd have to write your server code elsewhere and then make requests from your React application to your server to fetch data. But with Next.js, you can write server logic inside of Next.js.

In this article, we will be creating a travel blog to learn how to use dynamic routing on both the client and server side in Next.js. We will also look at how pages are and should be rendered in Next.js. You can follow along in the GitHub repository for this tutorial.

Jump ahead:

Next.js: The React framework with built-in SSR

One cool thing about Next.js is that you can control how pages are rendered to the client individually. All of your Next.js pages are server-rendered, but when you make use of the newly stable App Router feature, you can render pages from the client or server. We will be focusing on server-rendered pages in this article.

By default, Next.js pre-renders all your pages (server-rendered pages). This means that Next.js generates the HTML of each page ahead of time. There are two different ways of pre-rendering your pages: static site generation (SSG), and server-side rendering (SSR).

What makes these methods different is when the pages are generated. For example, when using SSG, the pages are generated at build time while the ones using SSR are generated at runtime. When we say runtime, it means the pages are generated on each request of a page.

This is very different from SSG, which only generates a page once and reuses it for each request. The reason SSR pages are regenerated on each request is that it was built for pages where the data will always change. So when you have a page where the data changes almost every time a user makes a request to that page, then you can make use of SSR.

Setting up Agility with Next.js

In our tutorial, we will create a simple blog to demonstrate dynamic routing and SSR using Next.js and Agility, a headless content management system (CMS). If you don’t already have Agility, the first step of this tutorial will be for you to create an account.

We're just going to set up the contents we need on Agility and later on, we will fetch these contents using the Agility Content Fetch JS SDK.

After signing up, you'll be prompted to create a new instance: Signing Up For The Agility CMS

Select the Next.js Starter option. After this, you'll be required to name the instance; choose whatever name you like.

By default, you'd already have a list of blog posts, so head to Content > Blog Posts from the left sidebar: A List Of Blog Posts On The Agility Account Page You can add more posts or simply use the default for this tutorial. Now that we have our account set up, let's get the API key of our instance and save it somewhere. Head up to Settings > API Keys from the left sidebar. Agility provides two API keys: preview and live. For this tutorial, we will make use of the live API key, so copy that and save it somewhere.

Setting up the Next.js development environment

To set up our development environment, let's first create a new Next.js project. To do this, run the following command, which will trigger the installation of the latest version of Next.js:

npx create-next@latest
Enter fullscreen mode Exit fullscreen mode

This command will trigger a few prompts. This tutorial uses JavaScript, but we will use Tailwind CSS for styling, so select Yes when asked about that. Finally, if you're not familiar with using the src directory in Next.js , select No as the value for the src/ directory prompt.

Then, Next.js will ask if you want to use the regular Pages Router or the new App Router. The Pages Router simply means using the traditional router where all files under the pages directory are a route. The App Router is the new form of router where a special page.js file is used to define a route for any directory. For this tutorial, we will be making use of the Pages Router because the App Router doesn't support SSR. Instead, it uses React Server Components (RSC).

Once installed, open up the project in your preferred editor and create a .env file. This is the file where we will save our API key from Agility, so add it to your .gitignore file. Then, save the API key you copied earlier as AGILITY_API_KEY in your .env file.

Let's proceed by installing the essential dependency required to retrieve content from Agility. Run the following command:

npm install @agility/content-fetch
Enter fullscreen mode Exit fullscreen mode

Once installed, create an agility.lib.js file in the root directory. In this file, we will create the configuration to connect our project to our Agility instance. Paste the following in the file:

// agility.lib.js
import agility from '@agility/content-fetch';
const instanceGuid = '';

export const api = agility.getApi({
  guid: instanceGuid,
  apiKey: process.env.AGILITY_API_KEY,
  isPreview: false,
});
Enter fullscreen mode Exit fullscreen mode

The instanceGuid is the ID of the instance you created. Head back to your Agility dashboard and on the top of the same page where you got your API key, you'll find an Instance GUID section with a series of characters. Copy and paste the characters into the instanceGuid variable.

Introducing Next.js routes

Unlike React, where you have to install an external library to handle routing, Next.js has a built-in routing system that handles all of your routes.

In this section, I’ll provide a brief introduction to routing in Next.js so you can follow along with this tutorial. If you would like more information, check out this in-depth guide to how routing works in Next.js. As I mentioned earlier, this article focuses on the Pages Router. The way that this router works is that any .ts, .js, .tsx, and .jsx file that is under the pages directory becomes a route. In your file structure, you will find a pages directory with an index.js file.

In your terminal, run npm run dev. When you open the app’s local URL, you'll observe that the contents in the index.js file are what's displaying on the browser. To confirm, make some change to index.js and save it. Go back to your browser and you'll see that the change has been made: Next.js Route Changes Applied From the image above, we can deduce that:

  • An index.js file of a directory represents the root route of that directory
  • A directory within the pages directory becomes a nested route. So a file structure like pages/users/profile.js will yield a /users/profile route
  • A file can be used for multiple and dynamic routes using [] and the spread operator (...). The braces indicate a dynamic route, and the spread operator is used to catch all routes of that directory

Creating your first Next.js page

To enhance your understanding of Next.js routing, we'll now explore how to create pages and seamlessly navigate between them.

First, let's update the contents of the / route by heading to pages/index.js and replacing it with the code below:

const Home = () => {
  return (
      <main className={`pb-14 mt-28 max-w-[800px] mx-auto`}>
        <h1 className='font-bold text-3xl mb-4'>Welcome to my blog</h1>
        <p>There are no posts yet</p>
      </main>
  );
};
export default Home;
Enter fullscreen mode Exit fullscreen mode

Now, let's create a new About page. Head to the pages directory and create an about.js file. Paste the code below:

const About = () => {
  return (
      <main className={`pb-14 mt-28 max-w-[800px] mx-auto`}>
        <h1 className='font-bold text-3xl mb-4'>Welcome to my about page</h1>
        <p>Learn more about my blog here</p>
      </main>
  );
};
export default About;
Enter fullscreen mode Exit fullscreen mode

Save the project and go to /about in your browser. This will load the About page.

Next, let's work on creating a small header that navigates from the home page to the About page and vice versa. In the root directory of your project, create a components folder and in it, create a Header.js file. Now, paste the following:

import Link from 'next/link';

const Header = () => {
  return (
    <header
      className={`fixed w-full left-0 top-0 bg-blue-500 text-white py-6 px-10 flex items-center justify-between`}
    >
      <h1 className={`font-bold`}>XenTravels</h1>
      <ul className={'flex'}>
        <li>
          <Link
            href={'/'}
            className={
              'text-xs text-gray-200 hover:underline block ml-7 hover:text-white transition-all'
            }
          >
            Home
          </Link>
        </li>
        <li>
          <Link
            href={'/about'}
            className={
              'text-xs text-gray-200 hover:underline block ml-7 hover:text-white transition-all'
            }
          >
            About
          </Link>
        </li>
      </ul>
    </header>
  );
};
export default Header;
Enter fullscreen mode Exit fullscreen mode

The Link component is used for navigating across different pages in your Next.js application. It is similar to the <a> tag in HTML, but it has more features. For example, when you use the Link component in a page, Next.js automatically prefetches the linked page in the background. This helps with smooth navigation.

In the example above, we can now import and use the Header component in both the pages/index.js and pages/about.js files. Assuming we had several pages, reusing the Header component in every single one of them would simply be redundant. Instead, we can import and use the Header component on a file that is global to all pages in our app. Let's take a look at that next.

Adding a global navigation bar to your Next.js app component

The _app.js file is global, applying to all pages — even those we haven't created yet. Let's utilize this by integrating the Header component within it.

Your _app.js file should now look like this:

import Header from '@/components/Header';
import '@/styles/globals.css';
export default function App({ Component, pageProps }) {
  return (
    <>
      <Header />
      <Component {...pageProps} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Resulting About Page

Understanding dynamic routing in Next.js

Dynamic routing is having an “n” number of different content with similar content structure. For example, take a social network where users can sign up and post content. When you want to display a list of users or a collection of posts by a specific user, it's impractical to create a separate file for each individual. However, despite each user having unique data, the content structure will remain the same — every user page will feature a username, profile picture, and a list of their posts.

This is where dynamic routing comes in. In Next.js, we make use of braces [] to signify a dynamic route in the file structure. So a file structure like pages/users/[username].js could yield a /users/elijah or /users/elonmusk route as the case may be. Having a billion users would not change the file; they are all going to make use of the same file structure, but each of them will have unique details on the profile.

These are the steps to dynamic routing:

  • You create a dynamic route such as pages/users/[username].js for a set of users
  • When a client navigates to /users/elijah, you'll be able to fetch the username as a query parameter from that page
  • With the unique value of username you received (in this case, it is elijah), you can fetch the details of that user:

Changing The URL Path Username

Catch-all routes

The catch-all routes feature is often very useful in scenarios where you want all routes of a particular directory to return a similar page. For example, we saw an image where a file structure like pages/posts/[...slug].js would yield a /posts/2021/1/10/reactjs URL path.

Let's say I want all posts in my blog to be fetched by the year, month, and day: Changing The URL Path Destination Notice that the webpage on the right has an additional route (i.e., /nothing), but didn't throw an error. Instead, it still returned the blog post of the specified day (we will see how this was done later).

Next.js employs the spread operator in [...slug].js to return all the routes. Within that file, you can get your desired values and disregard the rest.

Catch-all routes can be made optional by adding double braces: pages/posts/[[...slug]].js. This means that while [[...slug]].js can catch the posts/2021/1/10/anything route, it can also catch posts. We will see a real-world application of this later.

Nested dynamic segments

Dynamic segments are made when you wrap square braces around a folder within pages. You can think of it as a dynamic folder. For example, pages/[username]/posts.js can yield /elijah/posts or /elonmusk/posts.

You can have multiple dynamic segments within a route. In a travel blog for example, you could have pages/categories/[continent]/[country]/[state].js, which can yield /categories/africa/nigeria/lagos.

Alternatively, you could use a structure like pages/posts/[year]/[month]/[day].js to render paths such as posts/2022/1/11. This approach mirrors the catch-all routes we discussed earlier, but with a more explicit file structure.

Dynamic routes vs. static routes

Static routes take precedence over dynamic routes. Consider a folder, such as pages/posts, containing both [slug].js (dynamic) and my-intro-post.js (static). When navigating to posts/my-intro-post in a browser, it loads the static file. But for any route other than “my-intro-post”, the dynamic file is loaded.

Query parameters and dynamic routing in Next.js

Query parameters are often used to represent query string parameters and dynamic routing parameters. Query string parameters are the parameters of a query string in a URL. For example, in google.com/search?q=somesearch, the q is a query string param and the value is somesearch. When you see query parameters in this article, we are referring to dynamic routing parameters.

Dynamic routing parameters are of a dynamic route. For example, a route like mytravelblog.com/posts/traveling-to-new-york with a file structure like posts/[slug].js will return slug as the dynamic route param with the value of traveling-to-new-york.

In this section, we will see how to access query parameters within a dynamic route. Head back to your project's root directory, create a posts.json file, and paste this:

{
  "posts": [
    {
      "id": 1,
      "title": "Exploring the Serene Beaches of Bali",
      "slug": "exploring-the-serene-beaches-of-bali",
      "body": "<p>Bali, the Island of Gods, is renowned for its picturesque beaches and stunning landscapes</p>"
    },
    {
      "id": 2,
      "title": "A Safari Adventure in the Heart of Africa",
      "slug": "a-safari-adventure-in-the-heart-of-africa",
      "body": "<p>Embark on a once-in-a-lifetime safari adventure in the heart of Africa...</p>"
    },
  ]
}
Enter fullscreen mode Exit fullscreen mode

For now, we'll retrieve posts locally from posts.json rather than from Agility. Start by creating a blog folder inside pages. Within this folder, we'll set up two files: [slug].js and index.js:

// blog/index.js
import postsData from 'posts.json';
export default function Blog() {
  const posts = postsData.posts;
  return (
    <main className={`pb-14 mt-28 max-w-[800px] mx-auto`}>
      <ul className={'grid grid-cols-3 gap-x-4 gap-y-8 items-center'}>
        {posts.map((post) => (
          <li key={post.id}>
            <Link
              href={`/blog/${post.slug}`}
              className={
                'block bg-white rounded-md shadow-md transition-all hover:scale-125'
              }
            >
              <span className={'px-5 pb-6 mt-5 block font-medium capitalize'}>
                {post.title}
              </span>
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we are fetching the posts directly from posts.json and displaying them as a list to our users. Now let's dynamically render each post from [slug].js:

//blog/[slug].js
import { useRouter } from 'next/router';

export default function PostItem() {
  const { query } = useRouter();
  const [post, setPost] = useState({});
  useEffect(() => {
    if (query?.slug) {
      const post = postsData.posts.find((post) => query.slug === post.slug);
      setPost(post);
    }
  }, [query?.slug]);
  return (
    <main className={'pb-14 mt-28 max-w-[800px] mx-auto'}>
      <div
        className={
          'mt-10 border-b-2 pb-3 border-[#ccc] flex justify-between items-center'
        }
      >
        <h1 className={'font-bold text-3xl capitalize leading-relaxed'}>
          {post.title}
        </h1>
      </div>
      <div
        className={'blog-body mt-10'}
        dangerouslySetInnerHTML={{ __html: post.body }}
      />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

The useRouter Hook provides several properties and methods for accessing all the properties of a route and for manually taking control of how you want the Next.js routing system to work.

One of the properties of this Hook is the query parameter that gives you access to the parameter of a dynamic route. In our example above, the parameter is slug because we named the file [slug].js. This is how we access query parameters in dynamic routes — we make use of the useRouter Hook. We’ll look at how this works in SSR in a later section.

Imperative dynamic routing with Next.js

So far, we've only seen how to use the Link component to navigate across different pages in Next.js. There is another way of navigating across different pages, and that is navigating imperatively.

The Link component is the declarative way of navigating to a different page in Next.js while the router.push method is the imperative way. Navigating imperatively means you tell Next.js how it should transit to the next page. For example, the router.push method, which is from the useRouter Hook, is used to programmatically navigate to a different page:

export default function Dashboard() {
  const router = useRouter();

  useEffect(() => {
    if (!token) return router.push('/login');
  }, []);

  return <></>;
}
Enter fullscreen mode Exit fullscreen mode

Note that router.push doesn't generate an <a> tag, similar to window.location. Consequently, crawlers may not detect it, which can be detrimental to Search Engine Optimization (SEO). However, in some cases, this might be the desired outcome.

API routes in Next.js

Next.js has an api folder that allows you to structure all of your server routes. Structuring your server routes within this folder is the same way you'd do it in the client. This means you could have dynamic API routes.

Recall that our posts were contained within the posts.json file. Now, instead of rendering it directly to the client, we can do it through the server. Head to pages/api and create a posts directory. In this new directory, create a new file called index.js:

//pages/api/posts/index.js
import postsData from 'posts.json';

export default function handler(req, res) {
  const posts = postsData.posts;
  res.status(200).json(posts);
}
Enter fullscreen mode Exit fullscreen mode

This works similarly to the way an Express server works. Open your browser or Postman, and make a request to /api/posts. You'll retrieve all your posts from the posts.json file.

This is just a simple example with local data. In a real-world application, you might pull data from a database like MongoDB, complete with models and other features. So it's a fully functional Node.js server.

The next API route we would need is for fetching each post, so create a [slug].js file:

// pages/api/posts/[slug].js
import postsData from 'posts.json';

export default function handler(req, res) {
  const { slug } = req.query;
  const post = postsData.posts.find((post) => post.slug === slug);
  res.status(200).json(post);
}
Enter fullscreen mode Exit fullscreen mode

When making a request to this API route, a unique slug will have to be passed to it, just like we've seen in the client.

Now we can update the files in our blog directory to fetch from our API routes:

// blog/index.js
export default function Blog() {
  useEffect(() => {
    const res = await fetch('http://localhost:3000/api/posts');
    const posts = await res.json()
  }, []);

  return ()
}

// blog/[slug].js
export default function PostItem() {
  useEffect(() => {
    const res = await fetch(`http://localhost:3000/api/posts/${query.slug}`);
    const post = await res.json()
  }, [])

  return ();
}
Enter fullscreen mode Exit fullscreen mode

Client-side data fetching is not great for SEO in Next.js apps. But in cases where you have to fetch from the client, you can make use of client-side data-fetching libraries like SWR.

Nested routing with query parameters

Nested routing covers both multiple dynamic segments and multiple dynamic routes. So far we've only seen an example of a nested route with multiple dynamic segments. Now let’s look at an example in action. For this example, we will be going a little bit beyond the scope of our tutorial to make use of JSONPlaceholder.

We're going to be fetching a list of posts for different users. Create a file structure like pages/users/[userId]/posts.js and paste the code below into the posts.js file:

//pages/users/[userId]/posts.js

const UserPosts = () => {
  const [posts, setPosts] = useState([]);
  const { query } = useRouter();

  useEffect(() => {
    if (query?.userId) {
      (async () => {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/posts?userId=${query.userId}&_limit=3`
        );
        const posts = await res.json();
        posts.length >= 1 && setPosts(posts);
      })();
    }
  }, [query?.userId]);

  return (
    <main className={`pb-14 mt-28 max-w-[800px] mx-auto`}>
      <ul className={'grid grid-cols-3 gap-x-4 gap-y-8 items-center'}>
        {posts.map((post) => (
          <li key={post.id}>
            <span className={'px-5 pb-6 mt-5 block font-medium capitalize'}>
              {post.title}
            </span>
          </li>
        ))}
      </ul>
    </main>
  );
};
Enter fullscreen mode Exit fullscreen mode

Query parameters and server-side rendering (SSR)

Using server-side rendering is very useful when you have data that changes often. This is why Next.js regenerates the HTML of pages using SSR whenever a request is made to that page.

In this section, we will look at how SSR can be used with dynamic routing and how we can access the query parameters inside the getServerSideProps. If you're new to SSR, getServerSideProps is the function you create to indicate that a page should use SSR.

The way it works is that you create a function within a page, name it getServerSideProps, and export it:

export const getServerSideProps = () => {};
Enter fullscreen mode Exit fullscreen mode

Any code you write within the getServerSideProps function runs on the server, and Next.js usually runs the getServerSideProps function before rendering the page. This means you can make all of your fetch requests here, and prepare them before they are displayed to the client.

To see this in action, head back to pages/index.js, create a getServerSideProps function, and paste the following into the function:

const posts = await api.getContentList({
  referenceName: 'posts',
  languageCode: 'en-us',
  sort: 'properties.date',
});
return {
  props: {
    posts: posts.items,
  },
};
Enter fullscreen mode Exit fullscreen mode

api is imported from ../agility.lib.js. The function returns an object of props, which will be passed into our page component as props:

export default function Home({ posts }) {
  return (
    <div>
      <main className={`pb-14 mt-28 max-w-[800px] mx-auto`}>
        <ul className={'grid grid-cols-3 gap-x-4 gap-y-8 items-center'}>
          {posts.map((post, index) => (
            <PostCard postFields={post.fields} key={index} />
          ))}
        </ul>
      </main>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

You can find the PostCard component in the GitHub repo here. The same applies for dynamic routes. If we aim to access each of these posts, initiate a posts directory, and create a [slug].js file within it.

Recall that we already have a blog directory where the posts are fetched locally. Now we want to fetch from our Agility instance:

// pages/posts/[slug].js

export const getServerSideProps = async (context) => {
  const { slug } = context.query;
  const posts = await api.getContentList({
    referenceName: 'posts',
    languageCode: 'en-us',
  });

  const post = posts.items?.find((post) => post.fields.slug === slug);

  return {
    props: {
      post: post.fields,
    },
  };
};

const PostItem = ({ post }) => {
  return (
    <main className={'pb-14 mt-28 max-w-[800px] mx-auto'}>
      <div
        className={
          'mt-10 border-b-2 pb-3 border-[#ccc] flex justify-between items-center'
        }
      >
        <h1
          className={
            'font-bold text-3xl capitalize leading-relaxed max-w-[500px]'
          }
        >
          {post.title}
        </h1>
        <span className={'text-xs text-[#aaa]'}>
          {new Date(post.date).toDateString()}
        </span>
      </div>
      <div
        className={'blog-body mt-10'}
        dangerouslySetInnerHTML={{ __html: post.content }}
      />
    </main>
  );
};
export default PostItem;
Enter fullscreen mode Exit fullscreen mode

Because we're fetching in the server, we can’t use the useRouter Hook. But the getServerSideProps provides a context param that has properties of the page's route, including the query. This method facilitates fetching dynamic data within a dynamic route using SSR.

Handling a common use case for dynamic routing in Next.js

So far, we've seen multiple common examples of using dynamic segments and routes. In this section, we will take a look at a common use case of query string parameters for filtering posts. But instead of using query string parameters, we will use the optional catch-all route.

Head to index.js in the pages directory, and rename it [[...filter]].js:

export const getServerSideProps = async ({ query }) => {
  const posts = await api.getContentList({
    referenceName: 'posts',
    languageCode: 'en-us',
    sort: 'properties.date',
  });

  if (query?.filter) {
    const [year, month] = query.filter;
    const filteredPosts = posts.items.filter(({ fields: post }) => {
      const date = new Date(post.date);
      if (date.getFullYear() === +year) {
        if (month && date.getMonth() + 1 === +month) return post;
        else if (!month) return post;
      }
    });

    return {
      props: {
        posts: filteredPosts,
      },
    };
  }

  return {
    props: {
      posts: posts.items,
    },
  };
};

export default function Home({ posts }) {
  return (
    <main className={`pb-14 mt-28 max-w-[800px] mx-auto`}>
      <ul className={'grid grid-cols-3 gap-x-4 gap-y-8 items-center'}>
        {posts.map((post, index) => (
          <PostCard postFields={post.fields} key={index} />
        ))}
      </ul>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, when you navigate to /2022, you’ll get all the posts from 2022, and /2022/9 will pull up all the posts from September 2022: Final Travel Blog Demo

Conclusion

In this article, we delved deep into dynamic routing, both on the client and server sides, and explored nested routing and query parameters with SSR. As you decide which pages in your app should use SSR and which should use SSG, consider whether you want the page to be pre-rendered during build time or runtime.

Pages using SSR are pre-rendered during runtime, which is not ideal for SEO. On the other hand, pages using SSG are pre-rendered during build time, which is often excellent for SEO.

With this knowledge, you should be well-equipped to create a fully functional full-stack web application, just as we demonstrated in this article using the Agility CMS. Thanks for reading and happy hacking!


LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — start monitoring for free.

Top comments (0)