The App Router in NextJS: From the root page to parallel routes
Many of us have worked with React, some of us have even worked with the page router in NextJS, but what is the app router? And how is it different from React routing?
Most of us have likely built a React application that requires naviagating between multiple pages.
A common way to solve this was building a file based routing solution and whether we built our own or used a library the end result was similar: We’d be able to create directories and files that’d be mapped to URLs when the application runs.
How those directories and files are mapped depended on the approach we took.
The page router in NextJS does something very similar. These days however NextJS is pushing their App Router, and have been for a couple of years now.
So what is it? How does it differ from what we know?
These are some of the questions we will answer in the coming sections. This post does assume knowledge of React and some basic NextJS.
The App Router
To understand the app router we first need to adjust our mental model.
The old model: Page routing
When using page routing a common mental model was:
- Every file in
pages/is an entry point. - Navigating to a new URL meant unmounting the current page and then mounting the next page.
- Layouts were bolted on:
- using
_app.tsxwrappers - Creating custom
getLayout()per-page patterns, etc.
- using
Diagram – Old mental model
If we take the following URLs:
/dashboard/dashboard/settings/dashboard/invoices
In page routing they’re basically:
[ DashboardPage ] /dashboard
[ SettingsPage ] /dashboard/settings
[ InvoicesPage ] /dashboard/invoices
So why did we move away? Well while this model worked and was simple to understand it made persistent layouts and partial updates difficult.
The new model: App routing
Now one of the biggest mental model shifts we need to take here is when using an app router the URL doesn’t map to a single component like it does in the page router.
The URL maps to a chain of layouts which ends with a page.tsx.
RootLayout
↓
DashboardLayout
↓
InvoicesPage
Each layout wraps around the next, kind of like russian dolls:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Header />
{children}
</body>
</html>
);
}
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-shell">
<Sidebar />
<main>{children}</main>
</div>
);
}
So really we now need to think of a URL mapping to a path through a layout tree, not to a ‘page’ like it did in the page router.
Commonly a single folder is referred to as a route segment and in NextJS this can contain quite a few different files.
The Route Segment - What lives here
If we take the following single folder, app/dashboard, as an example it can contain:
page.tsx– The leaf UI for /dashboard.layout.tsx– A shell that wraps all children of dashboard.template.tsx– Like layout, but recreated on navigation (no state persistence).loading.tsx– Suspense fallback while this segment loads.error.tsx– Error boundary for this segment.not-found.tsx– “404” scoped to this segment.
These file names, such as not-found.tsx, are not optional. They’re specific file names that NextJS looks for and works with.
### Template vs Layout: When to use each
As discussed above we would use layout.tsx for elements we want to keep mounted. Elements like headers, sidebars etc etc.
We would reach for template.tsx when we needed elements to be recreated on navigation. Such as animations, scroll reset etc etc.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// This state persists as you navigate within /dashboard/*
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
return (
<div className="dashboard-shell">
<button onClick={() => setIsSidebarOpen((v) => !v)}>Toggle</button>
{isSidebarOpen && <aside>Sidebar</aside>}
<main>{children}</main>
</div>
);
}
// app/dashboard/template.tsx
export default function DashboardTemplate({
children,
}: {
children: React.ReactNode;
}) {
return <PageTransition>{children}</PageTransition>;
}
But what about nested routes? Say we wanted the following URL dashboard/invoices how would we do that with the app router?
Well lets find out.
Nested routes and route groups
app/
layout.tsx → wraps everything
dashboard/
layout.tsx → wraps /dashboard/*
page.tsx → /dashboard
invoices/
page.tsx → /dashboard/invoices
To create a page at dashboard/invoices we create a new invoice directory within dashboard and add a pages.tsx file to that invoices directory.
This feels very similar to the page router we discussed before but behaves very differently. When a user navigates to dashboard/invoices the following steps happen:
- First it walks the
app/layout.tsxsegment - Then it walks the
app/dashboard/layout.tsxsegment - Finally it walks the
app/dashboard/invoices/page.tsxsegment
The invoice directory can contain all the same files we mentioned before, template.tsx, not-found.tsx, error.tsx etc etc.
This gives us the flexibility to customise each URL segment as we need.
In some cases though we may want to use a different layout for a particular section of our application but we don’t want the URL to change.
For example, imagine we have a marketing page for our product where before signing up users can see what our product is and read more about it but when they sign in they’ll have access to a dashboard.
The marketing and the dashboard would need their own layouts but we don’t want the URL to be /marketing.
Well we can do this:
app/
(marketing)/
layout.tsx → marketing shell
page.tsx → /
about/
page.tsx → /about
(app)/
layout.tsx → app shell
dashboard/
page.tsx → /dashboard
But why can’t we just remove (marketing) and have that segment just be the root layout? Well, if we did that the root layout would also be applied to all pages within (app). By using groups here we ensure that the (marketing)/layout.tsx does not affect the (app)/dashboard and vice versa.
In NextJS any directory name that is wrapped in parentheses, (), will not be mapped to a URL. So if we were to run this application and open / we’d walk through the marketing segment:
/ → RootLayout → (marketing)/layout → (marketing)/page
but the URL would still be /
Then we could navigate to /about which would look like:
/about → RootLayout → (marketing)/layout → (marketing)/about/page
The layouts in NextJS are not only for styling, in this case we’d also configure authentication in the (app)/layout.tsx to ensure only authorized users can access this segment.
What if we wanted to render multiple segments at the same URL? Well that is what parallel routes are for.
Parallel Routes
Parallel routes allow us to render multiple sibling route trees at the same time for a single URL.
Great… But what does that mean? And how do we do it? Well, lets take a look.
First we need to define a slot:
app/
dashboard/
layout.tsx
page.tsx → default slot (/dashboard)
@analytics/
page.tsx → analytics slot (same URL)
@invoices/
page.tsx → invoices slot (same URL)
Placing the @ symbol at the start of the directory name tells Next that this is a slot.
We can then use those slots in our code:
// app/dashboard/layout.tsx
type DashboardLayoutProps = {
children: React.ReactNode; // default slot
analytics: React.ReactNode; // @analytics slot
invoices: React.ReactNode; // @invoices slot
};
export default function DashboardLayout({
children,
analytics,
invoices,
}: DashboardLayoutProps) {
return (
<div>
<aside>{analytics}</aside>
<main>{children}</main>
<section>{invoices}</section>
</div>
);
}
The power here is we can actually render those slots however we want within this component.
In a future post we will discuss why this is powerful, and explore scenarios where this is more powerful than just using components.
Parallel routes aren’t the only special routing type in NextJS, we also have intercepting routes and it is fairly common for both of these to be used together.
Intercepting Routes
Intercepting routes allow us to render a route tree within the current layout instead of navigating away. Imagine a modal, in those cases we want to render some UI without navigating to a new url.
Similar to how we used @ for parallel routes NextJS provides the following for intercepting routes:
(.)- Same segment level(..)- One segment level up(..)(..)- Two segment levels up(...)- From the app route
Lets take a look at an example, imagine we have the following directory structure:
app/
photos/
page.tsx → /photos (grid)
[id]/
page.tsx → /photos/:id (standalone)
But a new feature request is that we now want to open /photos/[id] in a modal without leaving /photos. Well this is where we can combine a parallel route with an intercepting route to create a modal:
app/
photos/
layout.tsx
page.tsx
@modal/
(..)photos/
[id]/
page.tsx
In NextJS terms, the above directory structure says: While we’re at /photos if a user tries to navigate to /photos/[id] then we should intercept that route and render that UI within the @modal slot.
A simple code example of the photos layout:
// app/photos/layout.tsx
type PhotosLayoutProps = {
children: React.ReactNode;
modal: React.ReactNode; // @modal slot
};
export default function PhotosLayout({ children, modal }: PhotosLayoutProps) {
return (
<>
{children}
{modal}
</>
);
}
So when we click a link to /photos/123 from within /photos the following occurs:
- The modal slot renders the intercepted
[id]/page.tsx. - The URL updates, allowing users to copy/paste and share it.
An important note, if the user refreshes their browser at /photos/123 then the standalone /photos/[id]/page.tsx will be rendered instead of the modal.
Conclusion
This was a quick overview of some of the powerful features available to us when using the App router in NextJS.
In a future post we will dive deeper into some of these topics, in particular Parallel and Intercepting routes, to see some real world use cases.