skip to content
Andrei Calazans

Next.js - Handling Linkable Tabs

/ 3 min read

Do you need to have tabs in your Next.js app and these tabs need to have links to them?

What is the wrong way of doing this?

You know you can use URL query parameters to store which tab is active. Then you think you can just modify the current URL query parameters as the user switches tab. Lastly, you say to yourself you need to store the current query parameters in your local React state so you can decide what data to request and which tab to display.

The above is possible to achieve, and you end up with the following problems:

  • Since you duplicate the URL parameters state, you need to write logic to keep your local state in sync with the URL parameters.

  • By not using Link + anchor tags to write to the URL parameters, you also lose all of the functionalities anchor tags have. Thus you either ignore them or reimplement them.

  • If you do decide to reimplement the anchor tag functionalities, that is also more reinventing the wheel.

What’s the right way?

Use the platform.

This means. Use anchor tags to link to your tabs. And use Next.js’ Link component to write new query parameters to the URL.

Code Example:


import { useEffect } from "react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import styles from "../styles/Home.module.css";

const Tab = ({ href, isSelected, title }) => (
    <Link href={href}>
      <a
        style={{
          padding: 5,
          margin: 5,
          backgroundColor: isSelected ? "blue" : "transparent",
        }}
      >
        {title}
      </a>
    </Link>
)

export default function Home() {
  const { query } = useRouter();

  const isTabOneSelected = !!query.tabOne;
  const isTabTwoSelected = !!query.tabTwo;
  const isTabThreeSelected = !!query.tabThree;

  return (
    <div className={styles.container}>
      <Head>
        <title>Your Site</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1>Title</h1>
        <Link href="/about">About</Link>

        <nav>
          <Tab href="/?tabOne=true" title="Tab One" isSelected={isTabOneSelected} />
          <Tab href="/?tabTwo=true" title="Tab Two" isSelected={isTabTwoSelected} />
          <Tab href="/?tabThree=true" title="Tab Three" isSelected={isTabThreeSelected} />
        </nav>

        <section>
          <p>{JSON.stringify(query)}</p>
        </section>
      </main>
    </div>
  );
}

What is going on?

We use the useRouter’s query object to read the current URL query parameters.

const { query } = useRouter();

Then we use Next.js’ <Link href={href}> component to set the new query parameters /?tabOne=true when a tab is selected

Lastly, we check if the current tab is selected by doing const isTabOneSelected = !!query.tabOne;. The query object will have the tabOne key defined only when it is present as a query parameter.

Result

Video of tabs changing URL query parameters

But, I don’t want to push a new route for every tab selection

I noticed some people prefer for every new tab selection for it to not add a new history element.

Video of going back on selected tabs

Just use the replace prop on the Link component

<Link replace href={href}>

Now changing tabs won’t push to the history stack:

Video of tabs not pushed to history

This isn’t the only way

Case you don’t want to use the Link component you can still use the router’s push method. In this Tweet thread there are some examples of this https://twitter.com/Andrei_Calazans/status/1368976174861455360.