Parallel Routes are one of the coolest features of the Next.js App Router. They allow you to have multiple routing systems running simultaneously, but then combines them into the layout at the last moment.
Let's put them into use by adding a chat menu sidebar to our chat detail page. This sidebar will display a list of previous chats and will load in parallel with the main chat content.
Setting up the Parallel Route
Parallel routes work on the file system level by creating a folder with the @
symbol followed by the name of the parallel route. In our case, we'll create a folder named @chats
.
Inside of the @chats
folder, we'll create pages that correspond to the pages in the primary routing system:
First up is page.tsx
. This will be an empty page for the top-level route, as we don't want the extra chat menu column on the homepage.
// inside src/app/@chats/page.tsx
export default function BlankPage() {
return <div />;
}
Next we'll create about/page.tsx
. Similarly, we'll leave this page empty since we don't need the chat menu on the about page.
Then we'll create the route for the chat menu sidebar at @chats/[chatId]/page.tsx
. This is where we'll implement the chat menu sidebar for the chat detail page.
Inside the @chats/[chatId]/page.tsx
file, create a placeholder component called ChatMenuColumn
:
// inside of src/app/@chats/chats/[chatId]/page.tsx
export default async function ChatMenuColumn() {
return <div>Chat Menu</div>;
}
With the routes created, we can roll them together in the layout.
Updating the Layout for Parallel Routes
Inside of the layout.tsx
file, we will first update the RootLayout
component to include the chats
prop, which corresponds to the @chats
route:
// inside of src/app/layout.tsx
export default function RootLayout({
children,
chats,
}: Readonly<{
children: React.ReactNode;
chats: React.ReactNode;
}>) {
return (
<SessionProvider>
...
Then we'll render chats
above the children
:
//inside the return statement of RootLayout
<div className="flex">
{chats}
<div className="flex-1">{children}</div>
</div>
Now when refreshing the browser and navigating to a different page than the homepage, you should see the "Chat Menu" text displayed above the children content of the route:
What's happening is when we visit the page, Next is getting the children, which is the primary content for the route, and then it's also getting the parallel route of the chat menu and putting them together in parallel.
Implementing the Chat Menu
Back in VS Code, create a new component called ChatMenu
at src/components/ChatMenu.tsx
. This component will be an async React Server Component that will make calls to the server to get the user session and fetch the chats from the database, which will then be formatted as a set of links that go to the chat detail page for each chat.
Here's what the markup looks like all together:
import { getServerSession } from "next-auth";
import Link from "next/link";
import { Separator } from "@/components/ui/separator";
import { getChats } from "@/db";
export default async function ChatMenu() {
const session = await getServerSession();
const chats = await getChats(session?.user?.email!);
return (
<>
<div className="text-2xl font-bold">Chat Sessions</div>
<Separator className="my-3" />
<div className="flex flex-col gap-2">
{chats.map((chat) => (
<div key={chat.id}>
<Link href={`/chats/${chat.id}`} className="text-lg line-clamp-1">
{chat.name.trim().slice(0, 20)}
{chat.name.length >= 20 ? "..." : ""}
</Link>
</div>
))}
</div>
</>
);
}
With the ChatMenu
component created, we need to update the @chats
parallel route to use it.
Inside of @chats/[chatId]/page.tsx
, replace the ChatMenuColumn
placeholder with the imported ChatMenu
component. We'll also surround it in a conditional check to see if the user is authenticated by using getServerSession
:
// inside of src/app/@chats/chats/[chatId]/page.tsx
import { getServerSession } from "next-auth";
import ChatMenu from "@/app/components/ChatMenu";
export default async function ChatMenuColumn() {
const session = await getServerSession();
const authenticated = !!session?.user?.email;
return authenticated ? (
<div className="md:w-1/3 md:min-w-1/3 pr-5 w-full text-nowrap">
<ChatMenu />
</div>
) : null;
}
After saving, when you navigate to the chat detail page, you should see the list of chats displayed in the sidebar:
Handling Loading States
In order to demonstrate the parallelism of the Parallel Routes, let's simulate a delay in the ChatMenu
component by adding a setTimeout
:
// inside of src/app/@chats/chats/[chatId]/page.tsx
export default async function ChatMenuColumn() {
await new Promise((resolve) => setTimeout(resolve, 3000));
// ...
}
Now when you refresh the page, you'll notice that the entire page is blocked for 3 seconds. There's no implicit Suspense or streaming behavior, but we can handle this.
Inside of @chats/chats/[chatId]
, create a loading.tsx
file and export a Loading
component that displays a message:
// inside of src/app/@chats/chats/[chatId]/loading.tsx
export default function Loading() {
return <div>Loading</div>;
}
Now, when you refresh the page, you'll see the "Loading..." message displayed in the sidebar while the ChatMenu
component is loading.
Next.js automatically detects that the component is blocking and uses the optional loading.tsx
file to display a loading UI!
This is an example of implicit suspense behavior at the route level. If you want more fine-grained control over suspense boundaries and loading states, you can use the Suspense
component as we did in the previous lesson.
We can now remove the setTimeout
from the ChatMenu
component to restore the normal loading behavior, and push the changes to the repository.
Wrapping Up & Next Steps
Parallel Routes in Next.js App Router provide a powerful way to load multiple components simultaneously, improving the performance and user experience of your application. By using Parallel Routes, we were able to add a chat menu sidebar that loads in parallel with the main chat content, without blocking the entire page.
In the next lesson, we'll cover how to implement a streaming server action, another exciting feature exclusive to the App Router.