Next.js Discord

Discord Forum

<Link> or useRouter events

Answered
African Slender-snouted Crocodil… posted this in #help-forum
Open in Discord
African Slender-snouted CrocodileOP
Hi,

I have a quiz component with "Next", "Back", "Cancel", "Show Summary", "Pause Quiz" button controls.

Now there is a very hight likelyhood that the user clicks on a breadcrumb, taps on a menu item, or navigates someplace else by clicking on the right sidebar with a secondary menu.

I want to do is listen for events on my Quiz component whenever the route is about to change. How to achieve this in Next.js 14 using the App Router?
Answered by African Slender-snouted Crocodile
Apparently useRouter events are not available in App Router (https://github.com/vercel/next.js/discussions/41934)
So I had to come up with an alternative solution:
"use client";

import { useLeavingQuiz } from "@/contexts/leaving-quiz-context";
import { useQuiz } from "@/contexts/quiz-context";
import { usePathname } from "next/navigation";
import { useEffect } from "react";

export function NavigationEvents() {
  const pathname = usePathname();
  const { setSessions } = useQuiz();
  const { setIsLeavingQuiz } = useLeavingQuiz();

  useEffect(() => {
    console.log("pathname: ", pathname);
    if (!pathname.includes("/quiz/step")) {
      setSessions((sessions) =>
        sessions.map((session) =>
          session.isCurrent
            ? {
                ...session,
                updatedDate: new Date(),
                isPaused: true,
              }
            : session,
        ),
      );

      setIsLeavingQuiz(true);
    } else {
      setIsLeavingQuiz(false);
    }
  }, [pathname, setIsLeavingQuiz, setSessions]);

  return null;
}


I'm basically just pausing the user's quiz progress automatically for them whenever they leave the quiz. It's not the best solution but it'll do for now until Vercel adds useRouter events to App Router.
View full answer

48 Replies

Chinese Alligator
Are you trying to make something like a pop up? Saying "are you sure you want to quit the quiz?"
African Slender-snouted CrocodileOP
yes
but I can't find anything useful on that page
it says:

router.events has been replaced. See below.
it point to https://nextjs.org/docs/app/api-reference/functions/use-router#router-events

but I don't understand how that would help me?
You can listen for page changes by composing other Client Component hooks like usePathname and useSearchParams.

Eeeh, what!?
🤔
@African Slender-snouted Crocodile > You can listen for page changes by composing other Client Component hooks like usePathname and useSearchParams. Eeeh, what!?
In the example they show, they give you the variable you need usePathname() & useSearchParams to listen for changes on those variables via a useEffect
instead of set functions like in the pages router, it's more customizable in the app router since you have full control over the event and can do whatever you please
African Slender-snouted CrocodileOP
Okay, I'm looking at that example right now.... but I don't really understand how I would write my useEffect
The useEffect with the pathname and searchParams in the dependency array, will run on component mount and then run after any changes to the pathname and/or searchParams, so whatever you want to do when those change you can do inside that useEffect so just write whatever logic you were planning to do when the change event happened
Chinese Alligator
it think you make an instance of useRouter like this inside the root layout component

const router = useRouter();

then in the useEffect it should listen to changes on that instance

useEffect(function, [router])
in the function of the useEffect you can listen to router.events.on('routeChangeStart', callbackFunction)
where callbackFunction is the function that executes the logic of what you want to do when any route is about to change
African Slender-snouted CrocodileOP
@Chinese Alligator I think router.events.on('routeChangeStart', callbackFunction)is for the Pages Router. I'm using the App Router.
So I should do something like this?
'use client'
 
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
 
export function NavigationEvents() {
  const router = useRouter()
 
  useEffect(() => {
    // Do something when the route changes
  }, [router])
 
  return '...'
}
And wrap the NavigationEvents component in Suspense when you render it to contain the client-side rendering
African Slender-snouted CrocodileOP
So I should wrap my entire Quiz component in a Suspense?
No, just the NavigationEvents, unless your Quiz component also calls useSearchParams
African Slender-snouted CrocodileOP
Okay.

The docs says: "Which can be imported into a layout."

And then they put the Suspense at the top of the component tree. So I should wrap all my providers etc and put the Suspense as far up in the tree as possible?
@African Slender-snouted Crocodile Okay. The docs says: "Which can be imported into a layout." And then they put the `Suspense` at the top of the component tree. So I should wrap all my providers etc and put the `Suspense` as far up in the tree as possible?
No, they don't put it at the top of the component tree, they render the NavigationEvents component in the root layout so that it is only mounted once and can listen to events for all routes, they wrap only that component in Suspense because it calls useSearchParams which causes client-side rendering up to the closest Suspense boundary.

By wrapping it in Suspense you ensure that only the NavigationEvents component is client-side rendered and not you entire layout in this case.
Suspense should only ever wrap the component that needs it, so ideally you want it to be as low in the tree as possible. (aka as close to the component that needs it)
African Slender-snouted CrocodileOP
Okay. So in the layout.tsx where my Quiz lives then?
Where are you rendering NavigationEvents ?
African Slender-snouted CrocodileOP
And not like this:
import { Providers } from "@/components/providers/providers";
import { inter } from "@/styles/fonts";
import type { Metadata } from "next";
import "@/styles/globals.css";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${inter.className} h-full antialiased`}>
        <Providers>{children} </Providers>
        <Suspense fallback={null}>
          <NavigationEvents />
        </Suspense>
      </body>
    </html>
  );
}
This is fine, it's rendered in the layout and the Suspense is only wrapping the NavigationEvents which is the component that needs it
@Plague Where are you rendering `NavigationEvents` ?
African Slender-snouted CrocodileOP
What do you mean?
That is perfectly fine
@African Slender-snouted Crocodile What do you mean?
In the example you just sent, you're rendering NavigationEvents inside your RootLayout component, which is perfectly fine
I was just wondering where you rendered it but then you sent code so it answered my question
@Plague In the example you just sent, you're rendering `NavigationEvents` inside your `RootLayout` component, which is perfectly fine
African Slender-snouted CrocodileOP
Okay, but my Quizcomponent lives way further down the tree
So I should probably put the Suspense in the nearest layout.tsx?
@African Slender-snouted Crocodile Okay, but my `Quiz`component lives way further down the tree
That's fine, NavigationEvents need to be above the page where you Quiz component lives so that it can not be re-rendered when the Quiz component re-renders
@Plague I was just wondering where you rendered it but then you sent code so it answered my question
African Slender-snouted CrocodileOP
This was just an example for what I was about to ask something. I've actually not put it anywhere because I'm still trying to understand this whole navigation-events.jsthing from the docs.
@African Slender-snouted Crocodile This was just an example for what I was about to ask something. I've actually not put it anywhere because I'm still trying to understand this whole `navigation-events.js`thing from the docs.
I'll try to explain what is happening.

The NavigationEvents component is calling usePathname() to read the path of the current url for example if the pathname is /quiz then pathname will equal quiz and useSearchParams is being called to read the searchParams of the current url for example /quiz?param=somesearchparam then searchParams.get('param') will equal somesearchparam.

In the useEffect you are creating a url variable using the pathname and searchParamsand based off that url variable you can do some custom logic whenever the url pathname or searchParams change.

You are rendering this in the RootLayout because it needs to be above all pages in your application to handle route change events without re-rendering and since you are calling a dynamic function useSearchParams() you must wrap the NavigationEvents component in Suspense to prevent the RootLayout from being client-side rendered since useSearchParams() cause client-side rendering up to the closest Suspense boundary.

So all that is happening is you are reading the URL and executing some logic in the useEffect when that URL changes.
African Slender-snouted CrocodileOP
Okay so I think the following is the minimum amount of code that I need in my navigation-events.ts:
"use client";

import { usePathname } from "next/navigation";
import { useEffect } from "react";

export function NavigationEvents() {
  const pathname = usePathname();

  useEffect(() => {
    console.log("NavigationEvents");
  }, [pathname]);

  return null;
}
And the reason I need to put my Suspensein the RootLayout in my case is because the user can click on any link in my app (sidebar left menu, sidebar right menu, app logo, etc) when she is on the Quiz component. Is that correct?
Yeah
The Suspense in this case is specifically used to prevent leaking CSR to the layout via useSearchParams
African Slender-snouted CrocodileOP
@Plague Got it, don't need to use Suspense! 👍

I still need to use <NavigationEvents />.

Where should I put it? In the layout.tsx closest to my <Quiz /> component? Or in the root layout.tsx (RootLayout)? Or in my <Quiz /> component?
African Slender-snouted CrocodileOP
Apparently useRouter events are not available in App Router (https://github.com/vercel/next.js/discussions/41934)
So I had to come up with an alternative solution:
"use client";

import { useLeavingQuiz } from "@/contexts/leaving-quiz-context";
import { useQuiz } from "@/contexts/quiz-context";
import { usePathname } from "next/navigation";
import { useEffect } from "react";

export function NavigationEvents() {
  const pathname = usePathname();
  const { setSessions } = useQuiz();
  const { setIsLeavingQuiz } = useLeavingQuiz();

  useEffect(() => {
    console.log("pathname: ", pathname);
    if (!pathname.includes("/quiz/step")) {
      setSessions((sessions) =>
        sessions.map((session) =>
          session.isCurrent
            ? {
                ...session,
                updatedDate: new Date(),
                isPaused: true,
              }
            : session,
        ),
      );

      setIsLeavingQuiz(true);
    } else {
      setIsLeavingQuiz(false);
    }
  }, [pathname, setIsLeavingQuiz, setSessions]);

  return null;
}


I'm basically just pausing the user's quiz progress automatically for them whenever they leave the quiz. It's not the best solution but it'll do for now until Vercel adds useRouter events to App Router.
Answer
African Slender-snouted CrocodileOP
Thanks for all the help, @Plague