NextJs Parallel Routes

NextJs Parallel Routing allows you to render multiple pages in the same layout view. You can utilize this functionality for List - Detail View pattern, to show list of items and selected item detail in the same view.

Parallel Routes

Content

Setup

Parallel routing makes it easy to display list and detail UI in the same view using two separate pages, one page for list of items and another page for selected item detail.

Parallel routes are created using named slots. Slots are defined with the “@folder” convention, and are passed as props to layout. For showing list of children, we use the default “children” slot and display them as a column List in left column. When user selects an item from the list, we want to display item details in the right view. To display item detail, we will define a “@detail” slot. With this setup, default children shows item list page and @detail slot displays the selected item detail page.

Parallel Routes

Parallel Routes App

Lets code above UI using NextJs parallel routes.

Stater Project

Create a NextJS app using

npx create-next-app@latest
npx create-next-app@latest

Name your project "parallel-two-col" and accept rest of the defaults.

Seed

Create a new top level directory called “data” and create a file inventory.json. Copy following inventory items, which will be used to display list.

[
  {
    "tool_name": "Hammer",
    "manufacturer": "Stanley",
    "model": "STHT51304",
    "barcode": "0123456789012",
    "photo": "hammer.jpg"
  },
  {
    "tool_name": "Screwdriver",
    "manufacturer": "Craftsman",
    "model": "CMHT65045",
    "barcode": "1122334455667",
    "photo": "screwdriver.jpg"
  },
  {
    "tool_name": "Pliers",
    "manufacturer": "Klein Tools",
    "model": "D2000-28",
    "barcode": "9876543210987",
    "photo": "pliers.jpg"
  },
  {
    "tool_name": "Level",
    "manufacturer": "DeWalt",
    "model": "DWHT43003",
    "barcode": "5432109876543",
    "photo": "level.jpg"
  },
  {
    "tool_name": "Tape measure",
    "manufacturer": "Milwaukee",
    "model": "48-22-6616",
    "barcode": "8765432109876",
    "photo": "tape_measure.jpg"
  },
  {
    "tool_name": "Circular saw",
    "manufacturer": "Makita",
    "model": "XSH03Z",
    "barcode": "6543210987654",
    "photo": "circular_saw.jpg"
  },
  {
    "tool_name": "Drill",
    "manufacturer": "Ryobi",
    "model": "P208B",
    "barcode": "3210987654321",
    "photo": "drill.jpg"
  }
]
[
  {
    "tool_name": "Hammer",
    "manufacturer": "Stanley",
    "model": "STHT51304",
    "barcode": "0123456789012",
    "photo": "hammer.jpg"
  },
  {
    "tool_name": "Screwdriver",
    "manufacturer": "Craftsman",
    "model": "CMHT65045",
    "barcode": "1122334455667",
    "photo": "screwdriver.jpg"
  },
  {
    "tool_name": "Pliers",
    "manufacturer": "Klein Tools",
    "model": "D2000-28",
    "barcode": "9876543210987",
    "photo": "pliers.jpg"
  },
  {
    "tool_name": "Level",
    "manufacturer": "DeWalt",
    "model": "DWHT43003",
    "barcode": "5432109876543",
    "photo": "level.jpg"
  },
  {
    "tool_name": "Tape measure",
    "manufacturer": "Milwaukee",
    "model": "48-22-6616",
    "barcode": "8765432109876",
    "photo": "tape_measure.jpg"
  },
  {
    "tool_name": "Circular saw",
    "manufacturer": "Makita",
    "model": "XSH03Z",
    "barcode": "6543210987654",
    "photo": "circular_saw.jpg"
  },
  {
    "tool_name": "Drill",
    "manufacturer": "Ryobi",
    "model": "P208B",
    "barcode": "3210987654321",
    "photo": "drill.jpg"
  }
]

Display Item List

Let first tackle page to display tools. Create a new folder called “tools” under "app" dir (app/tools). Add a page.tsx file and copy following

import data from '@/data/inventory.json';
import Link from 'next/link';
 
async function getData() {
  return data;
}
 
export default async function ListPage() {
  const data = await getData();
  return (
    <div className=" w-full overflow-y-auto bg-slate-200 dark:bg-slate-800">
      {data.map((item, i) => (
        <Link
          href={`/tools/${item.barcode}`}
          key={`${i}`}
          className="bg-slate-100 dark:bg-slate-900 flex flex-row flex-nowrap gap-2 m-4 p-4 rounded-md"
        >
          <div className="bg-slate-200 dark:bg-slate-800 text-2xl font-bold border-2 rounded-full w-12 h-12 flex justify-center items-center">
            {item.tool_name.substring(0, 1)}
          </div>
          <div>
            <h1 className="text-base ">{item.tool_name}</h1>
            <h2 className="text-sm ">{item.manufacturer}</h2>
          </div>
        </Link>
      ))}
    </div>
  );
}
import data from '@/data/inventory.json';
import Link from 'next/link';
 
async function getData() {
  return data;
}
 
export default async function ListPage() {
  const data = await getData();
  return (
    <div className=" w-full overflow-y-auto bg-slate-200 dark:bg-slate-800">
      {data.map((item, i) => (
        <Link
          href={`/tools/${item.barcode}`}
          key={`${i}`}
          className="bg-slate-100 dark:bg-slate-900 flex flex-row flex-nowrap gap-2 m-4 p-4 rounded-md"
        >
          <div className="bg-slate-200 dark:bg-slate-800 text-2xl font-bold border-2 rounded-full w-12 h-12 flex justify-center items-center">
            {item.tool_name.substring(0, 1)}
          </div>
          <div>
            <h1 className="text-base ">{item.tool_name}</h1>
            <h2 className="text-sm ">{item.manufacturer}</h2>
          </div>
        </Link>
      ))}
    </div>
  );
}

We are using getData to load list of tools and then iterate over returned data to display the list.

Start project with npm run dev and go to http://localhost:3000/tools

You should see list of tools

Two Column Layout

We want design layout so as to display tools in left column and tool detail in right column. Add new file layout.tsx to app/tools folder and copy following code

export default function TwoColumnLayout({
  children,
  detail = null,
}: {
  children: React.ReactNode,
  detail?: React.ReactNode,
}) {
  return (
    <div className="grid grid-cols-[320px_auto] overflow-hidden">
      <div className="flex h-screen overflow-hidden bg-slate-200 dark:bg-slate-800">
        {children}
      </div>
      <div className=" bg-slate-100 dark:bg-slate-900">{detail}</div>
    </div>
  );
}
export default function TwoColumnLayout({
  children,
  detail = null,
}: {
  children: React.ReactNode,
  detail?: React.ReactNode,
}) {
  return (
    <div className="grid grid-cols-[320px_auto] overflow-hidden">
      <div className="flex h-screen overflow-hidden bg-slate-200 dark:bg-slate-800">
        {children}
      </div>
      <div className=" bg-slate-100 dark:bg-slate-900">{detail}</div>
    </div>
  );
}

Above shows UI with two columns, left one for list of items and right one for item detail. Notice layout takes two props, the default children prop and an additional detail prop. Children node is shown in left col and detail node in right.

Start project with npm run dev and go to http://localhost:3000/tools

You should see list of tools display in left column.

Display Item Detail

When user selects an item from the widget list, we want to display the selected item details. Add new folder called @detail below app\tools. Next add folder [barcode] below @detail. [barcode] defines dynamic segment and aallows us to capture the selected tool barcode. Add page.tsx to [barcode] and copy following code

import data from '@/data/inventory.json';
 
async function getData(barcode: string) {
  const item = data.find((item) => item.barcode === barcode);
  return item;
}
 
export default async function DetailPage({
  params,
}: {
  params: { barcode: string },
}) {
  const data = await getData(params.barcode);
  return (
    <div className="flex flex-row flex-nowrap p-12 gap-4">
      <div className="text-4xl font-bold border-2 rounded-md w-20 h-20 flex justify-center items-center bg-slate-400 dark:bg-slate-600">
        {data?.tool_name.substring(0, 1)}
      </div>
      <div>
        <h1 className="text-2xl ">{data?.tool_name}</h1>
        <h2 className="text-base ">
          {data?.manufacturer} - {data?.model}
        </h2>
        <div className="text-sm ">{data?.barcode}</div>
      </div>
    </div>
  );
}
import data from '@/data/inventory.json';
 
async function getData(barcode: string) {
  const item = data.find((item) => item.barcode === barcode);
  return item;
}
 
export default async function DetailPage({
  params,
}: {
  params: { barcode: string },
}) {
  const data = await getData(params.barcode);
  return (
    <div className="flex flex-row flex-nowrap p-12 gap-4">
      <div className="text-4xl font-bold border-2 rounded-md w-20 h-20 flex justify-center items-center bg-slate-400 dark:bg-slate-600">
        {data?.tool_name.substring(0, 1)}
      </div>
      <div>
        <h1 className="text-2xl ">{data?.tool_name}</h1>
        <h2 className="text-base ">
          {data?.manufacturer} - {data?.model}
        </h2>
        <div className="text-sm ">{data?.barcode}</div>
      </div>
    </div>
  );
}

We read barcode of user selected tool from params and load tool details.

One last detail, add page.tsx to @detail folder and copy following code

export default function Page() {
  return null;
}
export default function Page() {
  return null;
}

Above will be displayed when no tool is selected

Start project with npm run dev and go to http://localhost:3000/tools

Select a tool from list, selected tool details will be shown in right column

Default on Refresh

If you try to refresh page when an item is selected, you will get 404. This 404 due to unmatched route, and to fix it add default.tsx to tools and copy following code

import ListPage from './page';
 
export default ListPage;
import ListPage from './page';
 
export default ListPage;

Now when you refresh the browser with a tool selected, it works!

Next Steps

We explored one way to utilize Parallel routing to display list and selected item detail in a side-by-side two column layout. Hopefully you now have a good idea of how to display multiple pages in same layout view using parallel route slot mechanism. You can show multiple slots in same layout, like for dashboard, feed views and other UI views.

Find Source code with a ready to run project @ GitHub

CreateAppAI

CreateAppAI auto generates List-Detail parallel route and supports host of other route patterns.