Implementing Modals using parallel routes & intercepting routes provided in Next JS app router offers the following benefits:
1.Ability to share the modal content using a url
2.Preserving context when the page is refreshed, instead of closing the modal.
3.Closing the Modal on backward navigation & re-opening the modal on forward navigation
Why use with Parallel Routes?
Parallel Routes in Next JS are created by defining ‘slots’ which is a folder created by prepending an ‘@’ before the folder name e.g @modal or @photogrid
Paralle Routes work same way as Componentization where you can have different UI blocks in a single page with their own routes and logic. Using parallel routes however have the following benefits:-
Parallel Routes can be streamed independently, allowing you to define independent error and loading states for each route.
Next.js will perform a partial render, changing the subpage within the slot, while maintaining the other slot’s active subpages, even if they don’t match the current URL.
These 2 Routing techniques would therefore help us to implement a modal in Next JS and make sure that:
We can share the Modal through a url (Intercepting Routes)
On page refresh or shareable link navigation, modal context is preserved. (Intercepting Routes)
Keeping the context of the photo grid when navigating a modal (Parallel Routes)
In your app directory, create a blogs folder with the following logic to create a photo grid in its page.jsx file
import Link from "next/link";
import blogs from "../../blogs";
import Image from "next/image"; const Blogs = () => { return ( <div className="grid grid-cols-2 place-items-center place-content-center gap-5 sm:grid-cols-4"> {blogs.map((blog) => { return ( <article key={blog.id}> <Link href={`/blogs/${blog.title}`}> <Image alt={blog.title} src={blog.url} width={250} height={250} /> </Link> </article> ); })} </div> );
}; export default Blogs;
Then create a dynamic route for each photo from the grid,
import React from "react";
import blogs from "../../../blogs";
const Blog = ({ params }) => { const find = blogs.find((blog) => blog.title == params.title); return <div>{find.title}</div>;
}; export default Blog;
Then create a parallel route by creating a folder in the app directory starting with an @ as in the screenshot below @modal.
Every parallel route must have a default.js file for when the page gets refreshed and only one of the routes is matched by Next JS, the content in the default.jsx file gets displayed for the other slots. On our case we return a null as we only want to show a modal when the user clicks on a photo.
Inside the @modal slot create an intercepting route which intercepts the blog route we create earlier. If the slot is in the same segment as the intercepted route, you use only a single dot i.e (.) followed by the folder name of the intercepted route folder name.
(.) to match segments on the same level
(..) to match segments one level above
(..)(..) to match segments two levels above
(…) to match segments from the root app directory
You might say since the (.)blogs is inside the @modal folder which is inturn inside the app folder then it is 1 segment above but parallel routes/slots do not affect routing, you can’t navigate to /slots.
Inside the intercepted route dynamic route i.e page.jsx file, paste the following code
import React from "react";
import blogs from "../../../../blogs";
import Modal from "@/components/Modal";
import Image from "next/image";
const InterceptedRoute = ({ params }) => { const finder = blogs.find((blog) => blog.title == params.title); return ( <Modal> <article> <Image src={finder.url} alt={finder.title} width={400} height={400} /> </article> </Modal> );
}; export default InterceptedRoute;
The code to the modal component is
"use client";
import { useCallback, useRef, useEffect, MouseEventHandler } from "react";
import { useRouter } from "next/navigation"; export default function Modal({ children }: { children: React.ReactNode }) { const overlay = useRef(null); const wrapper = useRef(null); const router = useRouter(); const onDismiss = useCallback(() => { router.back(); }, [router]); const onClick: MouseEventHandler = useCallback( (e) => { if (e.target === overlay.current || e.target === wrapper.current) { if (onDismiss) onDismiss(); } }, [onDismiss, overlay, wrapper] ); const onKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === "Escape") onDismiss(); }, [onDismiss] ); useEffect(() => { document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }, [onKeyDown]); return ( <div ref={overlay} className="fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/60 " onClick={onClick} > <div ref={wrapper} className="w-[100%] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 sm:w-10/12 md:w-8/12 lg:w-2/5 p-6" > {children} </div> </div> );
}
The slot can now be accessed as a prop in the root layout:-
import React from "react";
import "./globals.css"; const RootLayout = ({ children, modal }) => { return ( <html lang="en"> <body className="p-5 sm:p-[80px] h-[100vh] "> {children} {modal} </body> </html> );
}; export default RootLayout;
You may need to restart your development server and then THAT’S IT!