NextJs 13 Experimental App Dir Hash in routes not directing to the hash id
Asked Answered
G

3

8

I am using Nextjs 13 with the experimental App Dir but am not sure if this problem I am facing has anything to do with the issue I am facing. I have an id in my home page of "faqs" and when I click on the link, I can see it successfully goes to that link but does nothing in the browser. If I am on another page, I click the link and it takes me to the home page with the correct url but still stays on the top of the page and does not scroll to the indicated id. I did implement scroll={false} as suggested in the documentation but it makes no difference.

Here is a snippet of the relevant code parts:

"use client"
import React, { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"

const Navigation = () => {
  const router = useRouter()
 ...

In the return:

 <Link scroll={false} href="/#faqs">FAQS</Link>

I Even tried:

<button type="button" onClick={() => router.push("/#faqs")}>FAQS</button>

In React the hash works fairly well but in next js, even only in client rendering it seems convoluted. If anyone knows what I am doing wrong or if there is a viable work around, I would sure appreciate it. Thank you in advance. If I am missing anything, please let me know.

Garda answered 18/2, 2023 at 0:3 Comment(0)
B
5

I use hashtags a lot and I plan to start using the app directory in future projects, so I dug into this and it's not pretty. Apparently, NextJS uses a different package for app directory components client-side called "next/navigation". It's very different from "next/router". Also, when using "next/link" elements, NextJS does not trigger the onRouteChangeComplete event when location.hash changes but location.pathname does not.

So, in order to detect a hash change and scroll to the associated element, I finally had to implement this hack:

"use client"
import { Inter } from '@next/font/google'
import paragraph from './paragraph'
import Link from 'next/link'
import { useEffect, useState } from 'react'

const inter = Inter({ subsets: ['latin'] })

export default function Home() {
  const [navClick, setNavClick] = useState(false);

  useEffect(() => {
    setTimeout(() => {
      const hash = window.location.hash;
      if (hash) document.querySelector(hash).scrollIntoView();
    }, 0);
  }, [navClick])

  const toggleNavClick = () => setNavClick((oldVal) => !oldVal);

  return (
    <main>
      <nav>
        <ul>
          <li>
            <Link href="/#one" onClick={toggleNavClick}>Section One</Link>
          </li>
          <li>
            <Link href="/#two" onClick={toggleNavClick}>Section Two</Link>
          </li>
          <li>
            <Link href="/#three" onClick={toggleNavClick}>Section Three</Link>
          </li>
        </ul>
      </nav>
      <div className="container">
        <section id="one">
          <h1>Section One</h1>
          <div dangerouslySetInnerHTML={{ __html: paragraph }} />
        </section>
        <section id="two">
          <h1>Section Two</h1>
          <div dangerouslySetInnerHTML={{ __html: paragraph }} />
        </section>
        <section id="three">
          <h1>Section Three</h1>
          <div dangerouslySetInnerHTML={{ __html: paragraph }} />
        </section>
      </div>
    </main>
  )
}

Since the hash change cannot be detected because no event is triggered, I basically created an event by toggling navClick each time a link is clicked. The navigation logic is enclosed in setTimeout() function because it triggers after window.location is updated.

Repo: https://github.com/designly1/next-hash-test Demo: https://next-hash-test.vercel.app/

Bourges answered 18/2, 2023 at 1:40 Comment(6)
Wonderful. Thank you so much @Designly. That did the trick. How convoluted when NextJS is supposed to make things easier but oh well. You saved the day.Garda
Yeah no problem. I'll leave the demo and repo up for posterity. Keep in mind, NextJS 13 is still in beta, and there is an issue tracked for this, so hopefully it will be fixed in the stable release. Overall, though, I'm excited about the new app directory, but I'm holding off for just these reasons.Bourges
Agreed. Yeah if it weren't for that, I really have no complaints so all is good. Thanks againGarda
Yes, I am not happy with next/navigationBourges
Thank you, I've been pulling my hair out.Glynis
You could also listen for the 'hashchange' event. developer.mozilla.org/en-US/docs/Web/API/Window/… While this solution looks like it would work, there is a less "hacky" solution here: github.com/vercel/next.js/discussions/…Buber
M
0

My workaround is to not use the Link component:

<a href="#faqs">FAQS</a>
Multicolor answered 7/3, 2023 at 16:56 Comment(1)
NB - This partially works, but being undocumented, does cause edge case bugs. For example, navigate to /a, then click an anchor to /a#section (any number of times), then go to /b (using Link as normal), then press back. For me this is a valid workaround until they release a solution to this problem (as is planned in their roadmap).Lorenelorens
S
0

My solution for scroll to particular header is this.

  const scrollToHeader = (anchorId: string) => {
    const headerElement = document.getElementById(anchorId);
    if (headerElement) {
      const offset = 100;
      const scrollPosition = headerElement.getBoundingClientRect().top + window.scrollY - offset;
  
      window.scrollTo({
        top: scrollPosition,
        behavior: "smooth",
      });
    }
  };
<Link
            key={headerId}
            className="text-sm font-semibold text-primary hover:underline"
            href={`#${headerId}`}
            scroll={false}
            onClick={(e) => {
              scrollToHeader(headerId);
              onLinkClick?.();
            }}>
            {parser(String(formattedHeaderText))}
</Link>
Syllabogram answered 13/9, 2023 at 22:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.