Dynamic Routing in Next.js

Next.js offers a powerful way to create applications that can seamlessly handle different URL patterns. Its dynamic routing feature lets you create routes that are adaptable and can be generated based on data. This is perfect for websites that have content like blog posts, e-commerce products, or any structure where individual pages follow a similar template but have unique content.

Understanding Dynamic Routing

In Next.js, dynamic routes are defined by using square brackets [] within your pages directory to represent dynamic segments of a URL. Let's say you have a blog, here are some examples of how this might look:

  • pages/blog/[postId].js: Handles URLs like /blog/1, /blog/20, etc.

  • pages/products/[category]/[productId].js: Handles URLs like /products/electronics/laptop-xyz

Handling Dynamic Route Parameters

When a request is made to a dynamic route, Next.js parses the URL and extracts the value of the dynamic segment. This value is then passed as a parameter to the page component or to data-fetching functions like getServerSideProps or getStaticProps. In the page component, you can access these parameters using the useRouter hook or as props, depending on how you've set up your data fetching.

Basic Dynamic Routing

Dynamic routing is perfect for scenarios where you have a large number of similar pages, such as individual blog posts or product pages. For example, in a blog application, you might have a dynamic route for individual posts:

File structure:

pages/
└── posts/
    └── [id].tsx

In pages/posts/[id].tsx, you can fetch the post data based on the id and display it:

import { useRouter } from 'next/router';

const Post = () => {
  const router = useRouter();
  const { id } = router.query;

  // Fetch the post data based on the id
  // const postData = fetchPostData(id);

  return <div>Post ID: {id}</div>;
};

export default Post;

Fetching Data and Navigation

When using dynamic routing in Next.js, you often need to fetch data based on the dynamic segment in the URL and navigate to the appropriate URL based on user interactions. Let's consider a more detailed example using a product page in an e-commerce application.

File Structure

First, let's set up the file structure for our dynamic product page:

pages/
└── products/
    └── [slug].tsx

In this structure, [slug].tsx is a dynamic route that will match any path like /products/some-product-slug.

Product Page Component-CSR

In pages/products/[slug].tsx, we'll fetch the product data based on the slug and display it:

import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { NextPage } from "next";

interface Product {
  id: number;
  name: string;
  description: string;
  slug: string;
  price: number;
}

const ProductPage: NextPage = () => {
  const router = useRouter();
  const { slug } = router.query;
  const [product, setProduct] = useState<Product | null>(null);

  useEffect(() => {
    if (slug) {
      fetchProductData(slug as string).then(setProduct);
    }
  }, [slug]);

  if (!product) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>Price: ${product.price}</p>
    </div>
  );
};

async function fetchProductData(slug: string): Promise<Product> {
  // Simulate fetching product data from an API or database
  const products: Product[] = [
    {
      id: 1,
      name: "Product 1",
      description: "Description of Product 1",
      slug: "product-1",
      price: 100,
    },
    {
      id: 2,
      name: "Product 2",
      description: "Description of Product 2",
      slug: "product-2",
      price: 200,
    },
  ];

  return products.find((product) => product.slug === slug);
}

export default ProductPage;

In the ProductPage component, we're using the useRouter hook to access the slug parameter from the URL. We then use this slug to fetch the product data in a useEffect hook. This approach is useful when you want to fetch data on the client side or when the data needs to be refreshed without a full page reload.

Product Page Component-SSR

import { GetServerSideProps, NextPage } from 'next';

interface Product {
  id: number;
  name: string;
  description: string;
  slug: string;
  price: number;
}

interface ProductPageProps {
  product: Product;
}

const ProductPage: NextPage<ProductPageProps> = ({ product }) => {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>Price: ${product.price}</p>
    </div>
  );
};

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { slug } = context.params;

  // Fetch the product data based on the slug
  const product = await fetchProductData(slug as string);

  return {
    props: {
      product,
    },
  };
};

async function fetchProductData(slug: string): Promise<Product> {
  // Simulate fetching product data from an API or database
  const products: Product[] = [
    { id: 1, name: 'Product 1', description: 'Description of Product 1', slug: 'product-1', price: 100 },
    { id: 2, name: 'Product 2', description: 'Description of Product 2', slug: 'product-2', price: 200 },
  ];

  return products.find((product) => product.slug === slug);
}

export default ProductPage;

In this example:

  • The getServerSideProps function is an async function that runs on the server side for each request. It receives a context parameter that contains information about the request, including the dynamic route parameters.

  • We extract the slug parameter from context.params and use it to fetch the product data. In a real-world scenario, you would fetch the data from an API or a database.

  • The fetched product data is then passed as props to the ProductPage component, which renders the product details.

  • Since this is server-side rendering, the product data is fetched and the page is rendered on the server before being sent to the client. This ensures that the page is fully populated with data when it's delivered to the client, which is beneficial for SEO and initial page load performance.

By using getServerSideProps in combination with dynamic routing, you can fetch and render data based on dynamic route parameters with server-side rendering in Next.js.

Navigation

To navigate to a product page, you can use the Link component from next/link in your product listing or any other component:

import { NextPage } from "next";
import Link from "next/link";

const ProductList: NextPage = () => {
  const products = [
    { id: 1, name: "Product 1", slug: "product-1" },
    { id: 2, name: "Product 2", slug: "product-2" },
  ];

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <Link href={`/products/${product.slug}`}>{product.name}</Link>
        </li>
      ))}
    </ul>
  );
};

export default ProductList;

In the ProductPage component, we're using the useRouter hook to access the slug parameter from the URL. We then use this slug to fetch the product data in a useEffect hook. When a user clicks on a product link, they will be navigated to the corresponding product page, where the product details are displayed.

Catch-all Segments

Catch-all segments in Next.js are a powerful feature that allows you to match routes with multiple path segments using a single dynamic route. This is particularly useful when you want to create a route that can handle a variable number of segments, such as in the case of a documentation page where the URL structure represents the hierarchy of the documentation.

How Catch-all Segments Work

To create a catch-all route, you use the [...slug] syntax in your file name. The ... indicates that it's a catch-all segment, and slug is the name of the parameter that will hold the array of path segments.

For example, if you have the following file structure:

pages/
└── docs/
    └── [...slug].tsx

The file pages/docs/[...slug].tsx will match any route that starts with /docs/, regardless of the number of segments that follow. The segments after /docs/ will be captured in an array and passed to the page component as a query parameter named slug.

Accessing the Segments in Your Component

In your page component, you can access the slug parameter using the useRouter hook from next/router. The slug parameter will be an array containing all the segments of the path.

Here's an example of how you might use this in a documentation page:

import { useRouter } from 'next/router';

const Documentation = () => {
  const router = useRouter();
  const { slug } = router.query;

  // You can use the slug array to fetch the appropriate documentation content
  // const content = fetchDocumentationContent(slug);

  return (
    <div>
      <h1>Documentation</h1>
      <p>Current Path: /docs/{slug.join('/')}</p>
      {/* Render the documentation content here */}
    </div>
  );
};

export default Documentation;

In this example, the slug array is used to reconstruct the current path and could also be used to fetch the appropriate documentation content based on the hierarchy represented in the URL.

Use Cases for Catch-all Segments

Catch-all segments are particularly useful in scenarios where you have a hierarchical structure that needs to be represented in the URL, such as:

  • Documentation: As shown in the example, you can use catch-all segments to create a documentation site where the URL structure represents the hierarchy of the documentation topics.

  • File Browsing: If you're creating a file browser or a similar application, catch-all segments can be used to represent the path to a file or directory.

  • Nested Categories: In an e-commerce site, you might use catch-all segments to handle nested categories, where the URL structure represents the category hierarchy.

Catch-all segments provide a flexible way to handle complex routing scenarios in Next.js, allowing you to create more dynamic and scalable applications.

Optional Catch-all Segments

Optional catch-all segments are similar to catch-all segments, but they also match when there are no additional path segments. This is useful for creating a route that can handle a variable number of segments, including zero. For example, you might have a blog route that can show a list of posts or an individual post:

File structure:

pages/
└── blog/
    └── [[...slug]].tsx

In pages/blog/[[...slug]].tsx, slug will be undefined or an array of segments:

import { useRouter } from 'next/router';

const Blog = () => {
  const router = useRouter();
  const { slug } = router.query;

  // Fetch the blog content based on the slug array or show the blog list if slug is undefined
  // const blogContent = slug ? fetchBlogContent(slug) : fetchBlogList();

  return <div>Path: /blog{slug ? '/' + slug.join('/') : ''}</div>;
};

export default Blog;

Conclusion

Dynamic routing in Next.js provides a flexible and powerful way to handle a variety of URL patterns in your application. By understanding how to use dynamic routing effectively, you can create more scalable and maintainable applications. Whether you need basic dynamic routes, nested routes, catch-all segments, or optional catch-all segments, Next.js has you covered.