<Link> or useRouter events
Answered
African Slender-snouted Crocodil… posted this in #help-forum
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?
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:
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.
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.
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
I found this:
https://nextjs.org/docs/pages/api-reference/functions/use-router#routerevents
but it's for the Pages Router
If I switch to App Router I end up here:
https://nextjs.org/docs/app/api-reference/functions/use-router
https://nextjs.org/docs/pages/api-reference/functions/use-router#routerevents
but it's for the Pages Router
If I switch to App Router I end up here:
https://nextjs.org/docs/app/api-reference/functions/use-router
but I can't find anything useful on that page
it says:
router.events has been replaced. See below.
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?
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 useEffectinstead 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 happenedChinese 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])
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 '...'
}@African Slender-snouted Crocodile 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 '...'
}
No, you shouldn't import the entire router, just use the
usePathname() and useSearchParams() hooks provided by Next.js like they use in the documentationAnd wrap the
NavigationEvents component in Suspense when you render it to contain the client-side renderingAfrican Slender-snouted CrocodileOP
So I should wrap my entire
Quiz component in a Suspense?No, just the
NavigationEvents, unless your Quiz component also calls useSearchParamsAfrican Slender-snouted CrocodileOP
Okay.
The docs says: "Which can be imported into a layout."
And then they put the
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
By wrapping it in
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 fineI 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 treeSo 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
In the
You are rendering this in the
So all that is happening is you are reading the URL and executing some logic in the
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
@African Slender-snouted Crocodile And the reason I need to put my `Suspense`in 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?
You don't even need the
Suspense if you aren't going to call useSearchParamsThe
Suspense in this case is specifically used to prevent leaking CSR to the layout via useSearchParamsAfrican Slender-snouted CrocodileOP
@Plague Got it, don't need to use
I still need to use
Where should I put it? In the
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 Crocodile <@683517071749021779> 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?
Always in a layout, which layout depends on your usecase, but overall id just put it in the root layout
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:
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.
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