Global Shareable Modals - Route Intercepting in Next 14
January 12, 2024 (9 months ago)
I started a quick toy project over the weekend to try out some things. It's an expense tracker to split trip expenses with friends. For now it's just generically called Split
. I might write about it later but for now this was the most interesting part.
My goal was to showcase the expense details component as both a modal and a page. In the past this would have been a pain to do but this time it was only a mild pain. JK, once you get the hang of it; route intercepting in Next is quite straightforward. Let's get to it!
What is route intercepting and parallel routes?
Route interception in Next lets you tweak or pause the usual way pages change in your app, letting you show a different view or part while navigating, but still keeps the route you first wanted – handy for times like when you reload a page. It's great for showing a different route without losing track of where you are on the current page.
The goal
Now that we know what router intercepting is, our current goal is to show a modal that we can display anywhere in our app that will show the expense details and should be able to be opened from a shareable URL.
Code and folder structure
I'll share my current use case as it's a little more opinionated and resembles something that you can encounter in real life.
I have a app/(app)
(yes, I know) route group to differentiate between the app and the landing /app/(marketing)
so this is what my current folder structure looks like:
- app
- (marketing)
- page.tsx
- (app)
- expenses
- [id]
- page.tsx // This is the page that we want to intercept
// ...layout.tsx and other stuff
The slot
We'll start by creating a slot
. Slots are part of the paralell routes feature and are a way to tell Next where to render the intercepted route.
- app
// Adding our slot
- @modal
- default.tsx
- (...expenses)
- [id]
- page.tsx
- (marketing)
- page.tsx
- (app)
- expenses
- [id]
- page.tsx // This is the page that we want to intercept
// ...layout.tsx and other stuff
The slot is called @modal
contains a default.tsx
file. You can define a default file to render as a fallback when Next cannot recover a slot's active state based on the current URL. Ours just returns null since our "fallback" is the expenses/[id]
page.
The (...)expense
convention is a way to tell Next that we want to intercept all routes that start with expenses
. This is useful if you want to have your modal called from different places in your app. Read more about it here.
Adding the slot modal to our layout
I spent quite a few minutes trying to figure out why my slot wasn't working. Turns out I forgot to add it to my root layout. 🤦♂️
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
{modal}
</body>
</html>
);
}
The actual modal
Now that we have our slot and route intercepted we can have our modal working as expected. For this particular project (and all my projects from now on) I'm using shadcn/ui and this is a perfect use case for the Dialog component.
This is a client component that is opened by default (so the dialog is opened when the page loads) and we simulate the closing of the dialog by going back in the router history.
// app/@modal/(...)expenses/[id]/page.tsx
"use client";
export default function ExpenseDetailsResponsiveDialog({
params: { id },
}: {
params: { id: string };
}) {
const router = useRouter();
const handleOpenChange = () => router.back();
return (
<Dialog
open={true}
onOpenChange={(val: boolean) => !val && handleOpenChange()}
>
<DialogContent>
<ExpenseDetails expenseId={id} />
</DialogContent>
</Dialog>
);
}
Conclusion
That's it! Congratulations! You've just implemented a global modal with route intercepting and paralell routes on Next 14. I hope you found this useful and if you have any questions or comments feel free to reach out on Twitter.