DEV Community

Cover image for Exploring advanced Next.js routing conventions
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Exploring advanced Next.js routing conventions

Written by David Omotayo✏️

One of Next.js' competitive advantages when it was released in 2016 was its built-in routing system. It supported both client-side and server-side rendering, so developers didn't have to configure a third-party routing library like React Router DOM. Next.js' router was also file-system based, which meant that the routes in an application were determined by how the files and folders were organized. This made it more appealing to most developers.

The team at Vercel has been improving the routing system with each new release. Next.js 9 introduced API routes, which let developers create serverless functions that handle specific API endpoints. Next.js 13 introduced the App Router, a new routing convention that lets you render client and server-side React components in the same application.

The App Router has many features, including layouts, dynamic routes, nested routes, and a new set of routing conventions called parallel and intersecting routes. These features can be used to create advanced routing patterns.

In this article, we'll explore what parallel and intersecting routes are, compare them to the existing routing options, understand their conventions, and demonstrate how to use them.

Prerequisite

Having prior knowledge of Next.js will be helpful to follow along with this article, but it's not required if you have a solid understanding of React.

What are parallel routes?

Parallel routes are a new and advanced routing convention in Next.js. According to the documentation:

“Parallel routes are a Next.js route paradigm that allows you to simultaneously or conditionally render one or more pages in the same layout that can be navigated independently.”

In other words, parallel routes allow you to render multiple pages in the same view.

Parallel routes are most useful when rendering complex, dynamic sections of an application, such as in a dashboard with multiple independent sections or a modal.

The image below is an illustration of a dashboard page from the Next documentation that demonstrates the intricacies of parallel routes: Parallel Routes Diagram In this context, the @team and @analytics routes are simultaneously rendered as sections of the dashboard layout using parallel routes.

How to use parallel routes

Parallel routes are defined using the @folder convention, which is also known as a "slot,” which is essentially a folder with the @ symbol prefixed to its name: Parallel Routes With The Folder Convention

Slots are defined within a route segment and serve as a container for diverse types of dynamic contents. Once defined, they become readily accessible as props in the layout.tsx file within the corresponding route segment.

For example, say we have a dashboard page and want to organize its contents modularly using parallel routes. The first step would be to define named slots within the app/dashboard directory. Considering it is a dashboard, we could define parallel routes for team, analytics, and revenue sections: The Contents Of The Page.tsx File Rendering Within The Dashboard

For simplicity, we will include placeholder contents within the slots as follows:

// app/dashboard/@team/page.tsx

export default function Team() {
  return (
    <h2>Team slot</h2>
    <svg>...</svg>
  )
}
// app/dashboard/@revenue/page.tsx

export default function Revenue() {
  return (
    <h2>Revenue slot</h2>
    <svg>...</svg>
  )
}
// app/dashboard/@analytics/page.tsx

export default function Analytics() {
  return (
    <h2>Analytics slot</h2>
    <svg>...</svg>
  )
}
Enter fullscreen mode Exit fullscreen mode

With the slots defined, the layout.tsx file within the dashboard route segment now accepts the @analytics, @revenue, and @team slots as props. This replaces the conventional approach of importing them.

Therefore if we go to the layout.tsx file and log the props object to the console, we will get the following result:

{
  analytics: {
      ...
    },
  },
  revenue: {
      ...
    },
  },
  teams: {
      ...
    },
  },
  children: {
      ...
  }
}
Enter fullscreen mode Exit fullscreen mode

The next step involves accessing the slots as properties of the props object and dynamically rendering them within the layout, as demonstrated below:

import React from "react";

interface ISlots {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
  revenue: React.ReactNode;
}

export default function DashboardLayout(props: ISlots) {

  return (
    <div>
      <h1>{props.children}</h1>
      <div>{props.analytics}</div>
      <div>{props.team}</div>
      <div >{props.revenue}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When you navigate to localhost:3000/dashboard, you should see the dashboard layout rendered with the parallel routes: Dashboard Layout Rendered With The Parallel Routes Note: To simplify the example, we have omitted the inline styles. You can find the source code in my GitHub repo. There are a few additional details to note from this example.

First, the children slot defined alongside the team, analytics, and revenue slots, serves as an implicit slot specifically designed to render the contents of the page.tsx file within the /dashboard route segment. As such, it does not need to be mapped to a folder: The Contents Of The Page.tsx File Rendering Within The Dashboard This means that dashboard/page.tsx is equivalent to dashboard/@children/page.tsx.

Secondly, it might be tempting to assume that the analytics, team, and revenue slots function as route segments because they are structured similarly. However, they do not impact the URL structure, and a file path like app/dashboard/@team/members remains accessible at localhost:3000/dashboard/members.

Why use parallel routes?

The clear advantage of parallel routes over the traditional approach is the ability to render completely independent code on the same URL and within the same view, using slots.

Conventionally, developers faced limitations in dynamically rendering contents on a page, as traditional routing mechanisms only supported linear rendering—meaning one URL per view.

This is why component composition development has been embraced over the years. It enables the rendering of modular and reusable components that can be combined and composed to construct intricate user interfaces.

If we were to use the component composition method in our dashboard example, the @analytics, @team, and @revenue slots would be defined as components and arranged in the dashboard layout like so:

import UserAnalytics from "@/components/Team";
import RevenueMetrics from "@/components/Analytics";
import Notifications from "@/components/Revenue";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      <div>{children}</div>
      <UserAnalytics />
      <Revenue />
      <Team />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

While this approach is effective and helps to make your code more manageable, especially when multiple teams are working on the project, it is possible to achieve the same result using parallel routes, with the added benefit of independent streaming and sub-navigation.

Independent route stream

Each parallel route is streamed independently to the layout, allowing for individual loading and error states, completely isolated from other parts of the layout.

For instance, if a section like analytics takes longer to load than other dashboard sections, a loading indicator can be displayed for that section alone, while the remaining sections remain fully interactive.

We can achieve this by defining loading.tsx and error.tsx files within each slot, as demonstrated in the illustration below: Defining Loading.tsx And Error.tsx Files In Our Dashboard

Then, we add the corresponding contents for the states. For example, we can add a loading spinner for the loading state and a custom interface for the error state. But for the sake of simplicity, we can simply add a text:

export default function Loading() {
  return <div>Loading...</div>;
}
Enter fullscreen mode Exit fullscreen mode

We can observe this feature in action if we add varying delays to the slot’s load time as follows:

 // wait function to add varying load time

 export function wait(time: number) {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}

// app/dashboard/@team/page.tsx

export default async function Team() {
  Await wait(1000) 
  return (
    <h2>Team slot</h2>
    <svg>...</svg>
  )
}
// app/dashboard/@revenue/page.tsx

export default async function Revenue() {
  Await wait(2000)
  return (
    <h2>Revenue slot</h2>
    <svg>...</svg>
  )
}
// app/dashboard/@analytics/page.tsx

export default async function Analytics() {
  Await wait(3000)
  return (
    <h2>Analytics slot</h2>
    <svg>...</svg>
  )
}
Enter fullscreen mode Exit fullscreen mode

We'll get the following result:

Dashboard With Varying Delayed Implemented

Note that for this feature to work properly, you have to also define a loading.tsx file for the children slot, i.e., within the root of the /dashboard path: Resulting Dashboard

Sub-navigation

The independent attribute of a slot extends beyond the loading and error state. Each route operates as a standalone entity, complete with its own state management and navigation, thus making each section of the user interface (in this context, the dashboard) operate as a standalone application.

This implies that we can create sub-folders that are associated with the dashboard/@folder/sub-folder file path within the slots and navigate back and forth without altering the state or rendition of the other sections on the dashboard.

For example, if we wish to implement sub-navigation within the @team slot, we can create a subfolder as follows: Implementing Sub-Navigation Within Our Dashboard Dropdown

Then, we feature a link in the @team slot: localhost:3000/dashboard/members, which navigates to the members subfolder, and another link in the members subfolder: localhost:3000/dashboard, which navigates back to the team default view:

import React from "react";
import Card from "@/components/card/card";
import { wait } from "@/lib/wait/page";
import Link from "next/link";

// app/dashboard/@team

export default async function Team() {
  return (
    <>
      <h2>Teams slot</h2>
      <svg>...</svg>
      <Link href="/dashboard/members">
        <p>
          Got to `/members` page </p> </Link> </> ); } // app/dashboard/@team/members export default function Members() { return ( <> <h1>Members page</h1> <Link href="/dashboard"> <p> Got back to `/teams` page </p> </Link> </> ); }
Enter fullscreen mode Exit fullscreen mode

  Navigating To The Default View Of Our Dashboard Note that in certain cases, you may encounter a black screen when attempting to navigate back to the default view, i.e., /dashboard. This is only a problem in development mode; if you build your project and run the production version of the project, everything should function as expected.

On the topic of default views, it is important to consider the behavior of other parts of the dashboard during this process. We will explore this in the following section.

Unmatched routes

An unmatched route occurs when the contents within a slot don’t match the current URL. This occurs when sub-navigating, as demonstrated in the previous section, and only one section of the dashboard or a layout matches the new route.

To put it more simply, by default, every slot aligns with the file path for the route segment they are defined in. In the case of our dashboard, this is /dashboard.

However, during client-side navigation, similar to what we did in the previous section, the file path changes to dashboard/members, matching only the @teams slot. Consequently, the @analytics and @revenue slots become unmatched.

This occurs because, during a page reload, Next.js attempts to render a default.tsx file within the unmatched slots. If the file is absent, Next.js throws a 404 error; otherwise, it renders the file's contents.

The default.tsx file serves as a fallback for unmatched slots, allowing us to render alternate contents when Next.js cannot retrieve the active state of a slot.

To prevent Next.js from throwing a 404 error when accessing the /dashboard/members route within the @team slot, we simply need to add a default.tsx file for each slot within the route segment, including the children slot: Adding A Default.tsx File For Each Slot Within The Route Segment

Now, when we engage in a hard navigation to the dashboard/members route, the page will load correctly and render the default view for unmatched routes: Rendering The Default Dashboard For Unmatched Routes

Conditional routes

Parallel routes can also be conditionally rendered based on certain conditions. For example, if we want only authenticated users to have access to the dashboard, we can use the authentication state to render the dashboard if the user is authenticated or render a login slot if otherwise:

interface ISlots {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
  revenue: React.ReactNode;
  login: React.ReactNode
}

export default function DashboardLayout(props: ISlots) {
  const isLoggedIn = true; // Simulates auth state

  if (!isLoggedIn) return props.login;

  return(
    <>
      {children}
      {users}
      {revenue}
      {notifications}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

What are intercepting routes?

The intercepting route is a Next.js route paradigm that allows us to load a route from another part of our application within the current context or layout.

The concept of an intercepting route is simple; it essentially functions as middleware, which enables us to intercept routing requests before actual navigation takes place.

Consider a login modal or a photo feed. Traditionally, clicking on a login link in a navigation bar or an image in a photo feed would direct you to a dedicated page where the login component or image is fully rendered.

However, with intercepting routes, we can alter this behavior. By intercepting the route, masking it, and overlaying it on the current URL, we can render it as a modal that overlays the layout without switching context: Visual Demonstration Of A Modal That Overlays The Layout Without Switching Context

Once a route has been intercepted, Next.js preserves the intercepted route, making it shareable. However, in the event of a hard navigation (e.g., browser refresh) or access via the shareable URL, Next.js renders the entire page instead of a modal. In this case, no route interception takes place.

How to create intercepting routes

Intercepting routes follow a convention similar to parallel routes, using a (.)folder convention. This convention involves adding a (.) prefix to a folder's name to match an existing route segment on the same level.

For instance, say we have a app/products route segment with a nested dynamic route: /[item], accessible at the localhost:3000/products/itemId: Products Route Segment We can intercept navigation from localhost:3000/products to localhost:3000/products/itemId by creating a (.)[item] directory within the products segment, as depicted in the image below: Intercepting Our Navigation Using An Item Directory Then, we define the contents we intend to render when the route is intercepted, such as the following:

interface IimageProps {
  params: {
    item: string;
  };
}

export default async function Page({ params: { item } }: IimageProps) {
  const res = await getImage(item);
  const image = await res.json();

  return (
    <>
      <div>
        <div>
          <div>
            <Image
              src={image.urls.regular}
              alt={image.alt_description}
              priority
              fill
              style={{ borderRadius: "10px" }}
            />
          </div>
        </div>
        <p>{image.alt_description}</p>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Presently, if an attempt is made to access the individual page of any of the items via the /products route, the URL is updated to localhost:3000/products/itemId, and the contents of the /products/(.)[item] intercepted route are rendered, replacing the intended item's contents: Item Page Rendered With Page Reload   There are two things to note from the example above. Firstly, the item's page is rendered following a page reload, and secondly, the intercepting route is rendered as a standalone page rather than a modal.

Intercepting routes are partially rendered by default. So, in the event of a page reload or direct access to the localhost:3000/products/itemId URL, the contents of the /products/[item] get rendered.

Although it may appear as though the intersecting route is being rendered as a standalone page, it is not the case, as the context remains unchanged; it only changes after a page reload, as previously explained.

To ensure the route renders properly as a modal, complete with a backdrop and the necessary characteristics, we need to define the intercepting route in a parallel route. To do this, we'll begin by creating a slot within the /products route and move the (.)[item] intercepting route into it: Defining The Intercepting Route In A Parallel Route

Next, we will proceed to add a layout.tsx file to the /products directory and a default.tsx file within the @modal slot with the following code:

// app/products/layout.tsx

import React from "react";

export default function layout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div>
      {children}
      {modal}
    </div>
  );
}

// app/products/@modal/default.tsx

Export const Default = () => {
  return null;
};
Enter fullscreen mode Exit fullscreen mode

We defined the default.tsx file to prevent Next.js from throwing a 404 error when the modal is not active, and because we don't want to display anything when it is not active, we're returning null.

Now with proper styling, the modal should render properly after interception: Modal Rendering After Interception Find the source code, including a link to the live site, in my GitHub repo.

Backward navigation closes the modal by default, but if you wish to add an icon or a button that does that to the modal, you can use the router.back() as shown in the code below:

'use client'
import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()
  return (
    <div>
      <span onClick={() => router.back()}>Close modal</span>
      ...
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Intercepting patterns

The intercepting route convention works similarly to the relative path convention ../, which means we can define intercepting routes using different levels:

  • (..) matches segments on the same level
  • (..)(..) matches segments two levels above
  • (...) matches segments on the root level

With these patterns, we can intercept routes anywhere in our application.

Conclusion

Parallel and intercepting routes are advanced routing mechanisms in Next.js that individually provide enhanced flexibility and an improved user experience when building web applications. However, when combined, they offer even more advanced features, as demonstrated in this article.

While their conventions may initially require some dedicated effort to fully understand and there may be occasional minor bugs, these routing mechanisms have the potential to significantly change how developers manage routing functionalities within their applications.


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)