[next/navigation] Next 13: useRouter events? #41934
-
It seems the new Is support for events planned? https://beta.nextjs.org/docs/app-directory-roadmap#planned-features |
Beta Was this translation helpful? Give feedback.
Replies: 65 comments 178 replies
-
Not sure it means events though... #41745 (reply in thread)
|
Beta Was this translation helpful? Give feedback.
-
looking for event functionality aswell. |
Beta Was this translation helpful? Give feedback.
-
How is "pathname" and "searchParams" being used in the statement below. Just trying to understand the code. Returning those variables? const pathname = usePathname() useEffect(() => { I've also seen this: useEffect(() => { Trying to understand the differences between the two statements. |
Beta Was this translation helpful? Give feedback.
-
I spent a few hours of scouring the internet for a way to do this. Then a few to build this. Very annoying. Here's my best attempt at a solution. No idea if it's the right approach. It's probably not right for everyone, but if it's right for you, great! You can instead of the below just use https://www.npmjs.com/package/nextjs13-router-events I published for this reason and follow those instructions. Link.tsx "use client";
import NextLink from "next/link";
import { forwardRef, useContext } from "react";
import { useRouteChangeContext } from "./RouteChangeProvider";
// https://github.com/vercel/next.js/blob/400ccf7b1c802c94127d8d8e0d5e9bdf9aab270c/packages/next/src/client/link.tsx#L169
function isModifiedEvent(event: React.MouseEvent): boolean {
const eventTarget = event.currentTarget as HTMLAnchorElement | SVGAElement;
const target = eventTarget.getAttribute("target");
return (
(target && target !== "_self") ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey || // triggers resource download
(event.nativeEvent && event.nativeEvent.button === 2)
);
}
const Link = forwardRef<HTMLAnchorElement, React.ComponentProps<"a">>(function Link(
{ href, onClick, ...rest },
ref,
) {
const useLink = href && href.startsWith("/");
if (!useLink) return <a href={href} onClick={onClick} {...rest} />;
const { onRouteChangeStart } = useRouteChangeContext();
return (
<NextLink
href={href}
onClick={(event) => {
if (!isModifiedEvent(event)) {
const { pathname, search, hash } = window.location;
const hrefCurrent = `${pathname}${search}${hash}`;
const hrefTarget = href as string;
if (hrefTarget !== hrefCurrent) {
onRouteChangeStart();
}
}
if (onClick) onClick(event);
}}
{...rest}
ref={ref}
/>
);
});
export default Link; RouteChangeProvider.tsx import { usePathname, useSearchParams } from 'next/navigation';
import { createContext, useContext, useState, useCallback, Suspense, useEffect } from 'react';
/** Types */
type RouteChangeContextProps = {
routeChangeStartCallbacks: Function[];
routeChangeCompleteCallbacks: Function[];
onRouteChangeStart: () => void;
onRouteChangeComplete: () => void;
};
type RouteChangeProviderProps = {
children: React.ReactNode
};
/** Logic */
const RouteChangeContext = createContext<RouteChangeContextProps>(
{} as RouteChangeContextProps
);
export const useRouteChangeContext = () => useContext(RouteChangeContext);
function RouteChangeComplete() {
const { onRouteChangeComplete } = useRouteChangeContext();
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => onRouteChangeComplete(), [pathname, searchParams]);
return null;
}
export const RouteChangeProvider: React.FC<RouteChangeProviderProps> = ({ children }: RouteChangeProviderProps) => {
const [routeChangeStartCallbacks, setRouteChangeStartCallbacks] = useState<Function[]>([]);
const [routeChangeCompleteCallbacks, setRouteChangeCompleteCallbacks] = useState<Function[]>([]);
const onRouteChangeStart = useCallback(() => {
routeChangeStartCallbacks.forEach((callback) => callback());
}, [routeChangeStartCallbacks]);
const onRouteChangeComplete = useCallback(() => {
routeChangeCompleteCallbacks.forEach((callback) => callback());
}, [routeChangeCompleteCallbacks]);
return (
<RouteChangeContext.Provider
value={{
routeChangeStartCallbacks,
routeChangeCompleteCallbacks,
onRouteChangeStart,
onRouteChangeComplete
}}
>
{children}
<Suspense>
<RouteChangeComplete />
</Suspense>
</RouteChangeContext.Provider>
);
}; useRouteChange.tsx import { useEffect } from 'react';
import { useRouteChangeContext } from './RouteChangeProvider';
type CallbackOptions = {
onRouteChangeStart?: Function;
onRouteChangeComplete?: Function;
}
const useRouteChange = (options: CallbackOptions) => {
const { routeChangeStartCallbacks, routeChangeCompleteCallbacks } = useRouteChangeContext();
useEffect(() => {
// add callback to the list of callbacks and persist it
if (options.onRouteChangeStart) {
routeChangeStartCallbacks.push(options.onRouteChangeStart);
}
if (options.onRouteChangeComplete) {
routeChangeCompleteCallbacks.push(options.onRouteChangeComplete);
}
return () => {
// Find the callback in the array and remove it.
if (options.onRouteChangeStart) {
const index = routeChangeStartCallbacks.indexOf(options.onRouteChangeStart);
if (index > -1) {
routeChangeStartCallbacks.splice(index, 1);
}
}
if (options.onRouteChangeComplete) {
const index = routeChangeCompleteCallbacks.indexOf(options.onRouteChangeComplete);
if (index > -1) {
routeChangeCompleteCallbacks.splice(index, 1);
}
}
};
}, [options, routeChangeStartCallbacks, routeChangeCompleteCallbacks]);
};
export default useRouteChange; Then use like this: Replace regular NextJS import { Link } from './Link'; That Link component should be compatible with your setup. Your layout.tsx: import { RouteChangeProvider } from './RouteChangeProvider';
...
return (
<RouteChangeProvider>
{children}
</RouteChangeProvider>
) Your component, where you want to monitor the onRouteChangeStart and onRouteChangeComplete events: import useRouteChange from './useRouteChange';
...
export default function Component(props: any) {
...
useRouteChange({
onRouteChangeStart: () => {
console.log('onStart 3');
},
onRouteChangeComplete: () => {
console.log('onComplete 3');
}
});
...
} |
Beta Was this translation helpful? Give feedback.
-
Thank you. I appreciate it. I am gonna follow up on the code implementation.
Yours truly,
Augustino M.
…On Sun, Jul 16, 2023, 1:08 PM Steven Linn ***@***.***> wrote:
I spent a few hours of scouring the internet for a way to do this. Then a
few to build this. Very annoying. Here's my best attempt at a solution. No
idea if it's the right approach. It's probably not right for everyone, but
if it's right for you, great!
You can instead of the below just use
https://www.npmjs.com/package/nextjs13-router-events I published for this
reason and follow those instructions.
------------------------------
*Link.tsx*
"use client";
import NextLink from "next/link";import { forwardRef, useContext } from "react";import { useRouteChangeContext } from "./RouteChangeProvider";
// https://github.com/vercel/next.js/blob/400ccf7b1c802c94127d8d8e0d5e9bdf9aab270c/packages/next/src/client/link.tsx#L169function isModifiedEvent(event: React.MouseEvent): boolean {
const eventTarget = event.currentTarget as HTMLAnchorElement | SVGAElement;
const target = eventTarget.getAttribute("target");
return (
(target && target !== "_self") ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey || // triggers resource download
(event.nativeEvent && event.nativeEvent.button === 2)
);}
const Link = forwardRef<HTMLAnchorElement, React.ComponentProps<"a">>(function Link(
{ href, onClick, ...rest },
ref,) {
const useLink = href && href.startsWith("/");
if (!useLink) return <a href={href} onClick={onClick} {...rest} />;
const { onRouteChangeStart } = useRouteChangeContext();
return (
<NextLink
href={href}
onClick={(event) => {
if (!isModifiedEvent(event)) {
const { pathname, search, hash } = window.location;
const hrefCurrent = `${pathname}${search}${hash}`;
const hrefTarget = href as string;
if (hrefTarget !== hrefCurrent) {
onRouteChangeStart();
}
}
if (onClick) onClick(event);
}}
{...rest}
ref={ref}
/>
);});
export default Link;
*RouteChangeProvider.tsx*
import { usePathname, useSearchParams } from 'next/navigation';import { createContext, useContext, useState, useCallback, Suspense, useEffect } from 'react';
/** Types */type RouteChangeContextProps = {
routeChangeStartCallbacks: Function[];
routeChangeCompleteCallbacks: Function[];
onRouteChangeStart: () => void;
onRouteChangeComplete: () => void;};
type RouteChangeProviderProps = {
children: React.ReactNode};
/** Logic */
const RouteChangeContext = createContext<RouteChangeContextProps>(
{} as RouteChangeContextProps);
export const useRouteChangeContext = () => useContext(RouteChangeContext);
function RouteChangeComplete() {
const { onRouteChangeComplete } = useRouteChangeContext();
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => onRouteChangeComplete(), [pathname, searchParams]);
return null;}
export const RouteChangeProvider: React.FC<RouteChangeProviderProps> = ({ children }: RouteChangeProviderProps) => {
const [routeChangeStartCallbacks, setRouteChangeStartCallbacks] = useState<Function[]>([]);
const [routeChangeCompleteCallbacks, setRouteChangeCompleteCallbacks] = useState<Function[]>([]);
const onRouteChangeStart = useCallback(() => {
routeChangeStartCallbacks.forEach((callback) => callback());
}, [routeChangeStartCallbacks]);
const onRouteChangeComplete = useCallback(() => {
routeChangeCompleteCallbacks.forEach((callback) => callback());
}, [routeChangeCompleteCallbacks]);
return (
<RouteChangeContext.Provider
value={{
routeChangeStartCallbacks,
routeChangeCompleteCallbacks,
onRouteChangeStart,
onRouteChangeComplete
}}
>
{children}
<Suspense>
<RouteChangeComplete />
</Suspense>
</RouteChangeContext.Provider>
);};
*useRouteChange.tsx*
import { useEffect } from 'react';import { useRouteChangeContext } from './RouteChangeProvider';
type CallbackOptions = {
onRouteChangeStart?: Function;
onRouteChangeComplete?: Function;}
const useRouteChange = (options: CallbackOptions) => {
const { routeChangeStartCallbacks, routeChangeCompleteCallbacks } = useRouteChangeContext();
useEffect(() => {
// add callback to the list of callbacks and persist it
if (options.onRouteChangeStart) {
routeChangeStartCallbacks.push(options.onRouteChangeStart);
}
if (options.onRouteChangeComplete) {
routeChangeCompleteCallbacks.push(options.onRouteChangeComplete);
}
return () => {
// Find the callback in the array and remove it.
if (options.onRouteChangeStart) {
const index = routeChangeStartCallbacks.indexOf(options.onRouteChangeStart);
if (index > -1) {
routeChangeStartCallbacks.splice(index, 1);
}
}
if (options.onRouteChangeComplete) {
const index = routeChangeCompleteCallbacks.indexOf(options.onRouteChangeComplete);
if (index > -1) {
routeChangeCompleteCallbacks.splice(index, 1);
}
}
};
}, [options, routeChangeStartCallbacks, routeChangeCompleteCallbacks]);};
export default useRouteChange;
Then use like this:
Replace regular NextJS Link components with this one:
import { Link } from './Link';
That Link component should be compatible with your setup.
Your layout.tsx:
import { RouteChangeProvider } from './RouteChangeProvider';
...return (
<RouteChangeProvider>
{children}
</RouteChangeProvider>)
Your component, where you want to monitor the onRouteChangeStart and
onRouteChangeComplete events:
import useRouteChange from './useRouteChange';
...export default function Component(props: any) {
...
useRouteChange({
onRouteChangeStart: () => {
console.log('onStart 3');
},
onRouteChangeComplete: () => {
console.log('onComplete 3');
}
});
...}
—
Reply to this email directly, view it on GitHub
<#41934 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AK7QDGWZFJSYXGE3OBZRS4DXQO4S5ANCNFSM6AAAAAARPUBUTE>
.
You are receiving this because you commented.Message ID: <vercel/next.
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
also, there is still no possibility to stop a route change event as there was with the page router, correct? or does anybody have a solution to this use case with nextjs app router? i tried everything possible already. |
Beta Was this translation helpful? Give feedback.
-
Any updates about possibility to block route change? |
Beta Was this translation helpful? Give feedback.
-
next13 is definitely not production ready. |
Beta Was this translation helpful? Give feedback.
-
Hopefully Next14 will have some improvements? 🤔 |
Beta Was this translation helpful? Give feedback.
-
I am also confused when it comes to the events or documentation changes between function versions (I have a legacy app to maintain) |
Beta Was this translation helpful? Give feedback.
-
Are there any alternative solutions? |
Beta Was this translation helpful? Give feedback.
-
Needed router.events.on() for a loading progress bar. |
Beta Was this translation helpful? Give feedback.
-
For everyone who want to make loading animation when change route, i am working in a package able to replace the useRouter() WITHOUT LOSING THE PREFETCH NEXT FEATURE |
Beta Was this translation helpful? Give feedback.
-
Is it possible to implement only those screens where router events are required in the pages directory? |
Beta Was this translation helpful? Give feedback.
-
I think router.events is needed to prevent users leaving while typing, why did the team remove it? Does anyone know why? |
Beta Was this translation helpful? Give feedback.
-
Hi everyone! If you want to show a loading indicator upon route transitions, here is "use client";
import { useRouter } from "next/navigation";
import { useEffect, useOptimistic } from "react";
export function LoadingIndicator() {
const router = useRouter();
const [loading, setLoading] = useOptimistic(false);
useEffect(() => {
if (router.push.name === "patched") return;
const push = router.push;
router.push = function patched(...args) {
setLoading(true);
push.apply(history, args);
};
}, []);
return loading && <div class="bg-red-500 h-3 fixed inset-0 animate-pulse"></div>;
} It works by monkey patching the |
Beta Was this translation helpful? Give feedback.
-
how about doing this
everytime the route changes this useEffect hook will be triggered |
Beta Was this translation helpful? Give feedback.
-
I'm wrote this hacky hook to detect when an anchor element is clicked on the page, and shows a progress bar until the path changes. import { usePathname } from "next/navigation";
import { useRef, useState, useEffect } from "react";
/**
* Hook that listens for click on an anchor element, when that happens return true
* In the case of a soft navigation, once the new path is set, clear the loading status
*/
export default function usePageLoading() {
const path = usePathname();
const currentPath = useRef(path);
const [pageLoading, setPageLoading] = useState(false);
useEffect(() => {
document.addEventListener("click", handleGlobalClick);
// Cleanup function to remove the event listener
return () => {
document.removeEventListener("click", handleGlobalClick);
};
}, []);
// When path changes, clear the loading status
useEffect(() => {
if (currentPath.current !== path) {
setPageLoading(false);
currentPath.current = path;
}
}, [path]);
function handleGlobalClick(event: MouseEvent) {
if (event.target instanceof Element) {
const anchor = event.target.closest("a");
if (anchor) {
setPageLoading(true);
}
}
}
return pageLoading;
} |
Beta Was this translation helpful? Give feedback.
-
It's been almost 2 years since this question was first posted. One might think that there would at least be some sort of official communication that says if route blocking is on the roadmap or not. |
Beta Was this translation helpful? Give feedback.
-
Just adding another voice to the fire. @leerob I read through all of your "solutions" and will be spinning up a branch to try them out today - but I am still utterly disappointed in this regression. I chose to push my organization to standardize on Next so we could deliver world class UX - but here I am, struggling to reliably display page transition state the user without having to jump through hoops that are frail and unreliable at best. Before I am told Pages router is stable and these nav events exist there... I committed to app router. I can't just "use Pages router". This really needs to be prioritized. |
Beta Was this translation helpful? Give feedback.
-
Put back the router.events in app router please (and don't ask me if I saw your message, I don't wan't to apply a dirty solution 😅), thank you! |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
@leerob This discussion is not solved yet. |
Beta Was this translation helpful? Give feedback.
-
Look up how easy this is in SvelteKit - just import |
Beta Was this translation helpful? Give feedback.
-
Just to be sure: there is no way to warn users when they leave a form with unsaved changes, by whichever way (closing the tab, back button, clicking a link)? It looks like there is only a quite complicated workaround that does not support the back button. For complex forms it would be more than helpful to have a complete and official solution 🙏 |
Beta Was this translation helpful? Give feedback.
-
After reading through all discussions I still can't find a new solution for the |
Beta Was this translation helpful? Give feedback.
-
For anyone who needs to prevent navigation away from forms, i stumbled across this article: https://medium.com/@joaojbs199/page-exit-prevention-in-nextjs-14-7f42add43297, and the solution works like a charm - handles back button clicks, any navigations away, and also if you try close the tab the browser steps in to ask if you want to leave. I had to make a few tweaks, the crucial one being wrapping the pushState statement on lines 25-27 in a
I also removed the backHref parameter and made line 41 just trigger a Huge credit to João who came up with the solution, this has saved me alot of headache |
Beta Was this translation helpful? Give feedback.
-
any update on it? |
Beta Was this translation helpful? Give feedback.
-
I just wish I could use: Router?.events.on( Again in nextjs13. |
Beta Was this translation helpful? Give feedback.
-
Has this been added again in Next.js 14? I see it listed in the documentation. https://nextjs.org/docs/14/pages/api-reference/functions/use-router#routerevents |
Beta Was this translation helpful? Give feedback.
Hey everyone, I appreciate your patience here while we worked on a reply. Thanks for answering some of the questions I asked as it helped us collect a list of current solutions. Please let us know if this helps!
Current Solutions
Displaying a progress indicator while a route transition is happening
All navigations in the Next.js App Router are built on React Transitions. This means you can use the
useTransition
hook and use theisPending
flag to understand if a transition is currently in-flight. For example: