Modal Routing in Next.js with Pages Router

Image ...

Introduction

In this article, we'll dive into implementing Modal Routing in an already existent project with Next.js 12 and pages router. For those using Next.js 13 and the App Router, you can find relevant information at Next.js Documentation.

The concept behind Modal Routing is to open links in a modal dialog instead of redirecting users to a separate page. However, we still want to ensure the URL remains SEO-friendly. This means that whether the user types the URL directly in the browser's address bar or arrives via external links, the specific page behaves as expected. Additionally, if the user refreshes the modal, it should take them directly to the specific detail page.

Diagram

Here's an overview to improve your comprehension of the different components and their interactions in this example. Detailed explanations for each component will be provided in the following sections.

AllProjects:

  • The "AllProjects" component serves as a gallery, responsible for displaying a collection of projects.

ProjectItem:

  • The "ProjectItem" component is used within the "AllProjects" component. It contains the "Link" for each specific project.
  • The "Link" is used to navigate to individual project details or open them in a modal.
  • It has parameters such as href, as, and shallow to control how the navigation and URL presentation work.

ProjectContent:

  • The "ProjectContent" component is responsible for displaying the detailed content of a selected project. It is designed to show the specifics of a project, such as its title, description, and other relevant information.

Modal:

  • The "Modal" component is an essential part of the setup, serving as a parent for the "ProjectContent" component.
  • It controls the visibility and behavior of the modal dialog, allowing users to view project details without leaving the current page. It ensures a seamless user experience when interacting with project content.

Detailed explanations with code

Pages

Index (pages/projects/index.js)

Receives data for all projects with getStaticProps and passes it as props to AllProjects component.

const Projects = (props) => {
  const { projects } = props;
  const router = useRouter();

  return (
    <>
      <AllProjects projects={projects} />
      <Modal
        isOpen={!!router.query.slug}
        onRequestClose={() =>
          router.push('/projects', null, { shallow: true })
        }>
        <ProjectContent
          project={projects.find(
            (project) => project.slug === router.query.slug
          )}
          pathname={router.query.slug}
        />
      </Modal>
    </>
  );
};

//export const getStaticProps = (context) => {...};

The central element of the Modal component resides within the projects index page, where it functions as the parent to the ProjectContent component. Within this context, two crucial parameters are associated with the Modal component:

  1. isOpen is set to 'true' when a Link is clicked and router.query.slug is defined. The '!!' double negation operator makes sure that any value is converted to a boolean.
  2. onRequestClose uses 'router.push' to close the modal and change the URL to '/projects'. shallow prevents full page reloads and is useful for modal dialogs to maintain the current page position when navigating.

 <Modal
  isOpen={!!router.query.slug}
  onRequestClose={() =>
    router.push('/projects', null, { shallow: true })
  }>

To ensure that we can access the specific project data on the index page, which is typically retrieved in the slug page, we employ a method of comparison. This comparison involves matching the project's slug with the router query defined by the Link and passing it as props to the ProjectContent component. It's worth noting that the "projects" object contains the data for all the projects.

<ProjectContent
  project={projects.find((project) => project.slug === router.query.slug)}
  pathname={router.query.slug}
/>

Slug (pages/projects/[slug].js)

This page is responsible for rendering the detailed content view when users access the route directly by entering a URL in the browser's address bar or when they refresh the modal. getStaticProps retrieves data specific to the project and passes it as props to the "ProjectContent" component. getStaticPaths generates the dynamic paths for projects.

const ProjectDetailPage = (props) => {
  const { project } = props;

  return (
    <>
      <Head>
        <title>Project - {project.title}</title>
        <meta name='description' content={project.description} />
      </Head>

      <ProjectContent project={project} />
    </>
  );
};

//export const getStaticProps = (context) => {...};
//export const getStaticPaths = () => {...};

Components

Modal

The Modal component is an essential part of implementing modal routing. It controls the visibility of the modal and prevents scrolling when the modal is open.

const ProjectModal = (props) => {
  const { isOpen, onRequestClose } = props;

  useEffect(() => {
    if (isOpen) document.body.style.overflow = 'hidden';
    if (!isOpen) document.body.style.overflow = 'unset';
  }, [isOpen]);

  return (
    <>
      {isOpen && (
        <div className={classes.backdrop} onClick={onRequestClose}>
          <div className={classes.content}>{props.children}</div>
        </div>
      )}
    </>
  );
};

export default ProjectModal;

ProjectItem (Link)

The ProjectItem component is a child of AllProjects component, shown at pages/projects/index.js, which serves as a gallery for displaying a collection of projects. In the Link we have following parameters:

?slug= is used to pass query parameters to the route. It's a way to send data to the dynamic route, typically used to specify which project or content to display. The ?slug= format is used for query parameters and is often associated with dynamic routes. You can include query parameters in the href to send data to the dynamic page.

as used when you want to show a different URL in the browser's address bar than the one used for routing. This can be useful for creating more user-friendly or SEO-friendly URLs.

shallow prevents full page reloads and is useful for modal dialogs to maintain the current page position when navigating.

<div className={classes.card}>
  <Link
    href={`/projects/?slug=${project.slug}`}
    as={`/projects/${project.slug}`}
    shallow={true}>
{/* ... */}

ProjectItem (router.push)

While Link is the preferred method for modal routing, you can achieve similar results using router.push. However, this approach may have a negative impact on SEO.

<div className={classes.card}>
  <button
    onClick={router.push(
      `/projects/?slug=${project.slug}`,
      `/projects/${project.slug}`,
      {
        shallow: true,
      }
    )}>
{/* ... */}

Bonus

Conditionally open modal with query parameters

If you want to implement Modal Routing on your root index page, which features both Projects and Posts, you'll need to ensure that only one instance of the Modal is opened at a time. To achieve this, you can pass ProjectContent and PostContent as children to the Modal. But how does the Modal know which one to render? This is where passing additional query parameters becomes crucial. Next.js Documentation - Linking and Navigating

When constructing the Link's href, you should use an URL object that includes the pathname, which is the root ('/'), and within the query, specify the slug and the type. These query parameters will help identify whether the link corresponds to a project or a post and guide the rendering of the Modal content accordingly.

<Link
  href={{
    pathname: '/',
    query: {
      slug: project.slug,
      type: 'project',
    },
  }}
  as={`/projects/${project.slug}`}
  shallow={true}>

Here is how your root index page should look like:

<Modal
  isOpen={!!router.query.slug}
  onRequestClose={() => router.push('/', null, { shallow: true })}>
  {!!router.query.slug && router.query.type == 'project' && (
    <ProjectContent
      project={featuredProjects.find(
        (project) => project.slug === router.query.slug
      )}
      currentTheme={currentTheme}
      pathname={router.query.slug}
    />
  )}

  {!!router.query.slug && router.query.type === 'post' && (
    <PostContent
      post={featuredPosts.find((post) => post.slug === router.query.slug)}
      currentTheme={currentTheme}
      pathname={router.query.slug}
    />
  )}
</Modal>

Thanks for following along, and I hope this guide helps you implement Modal Routing with Pages Router effectively!