[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: 71 comments 190 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.
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.
-
I tried the proposed solution but it didn't work for me. I hpe, this helps. |
Beta Was this translation helpful? Give feedback.
-
@LuisanSuarez have a nice implementation here to listen on 'on navigation start' and 'on navigation done (full loaded)' events: #42016 (comment) Edit: Because the indicator not works when press back button on the browserwe can fix it by listen on 'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import nProgress from 'nprogress';
import { useEffect } from 'react';
export default function NavigationEvents() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
// Override router.push for start nprogress
const _push = router.push.bind(router);
router.push = (href, options) => {
nProgress.start();
_push(href, options);
};
const handlePopState = () => {
nProgress.start();
};
// Popstate listener to handle back/forward navigation
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, []);
useEffect(() => {
nProgress.done();
}, [pathname, searchParams]);
return null;
} |
Beta Was this translation helpful? Give feedback.
-
we need router events start, end events for transition. Please bring it back. Thanks |
Beta Was this translation helpful? Give feedback.
-
Scroll related issues are plenty and not prioritized. Scroll (restoration) bugs have major impact on the end-user. Next/navigation router introduced the bugs and also removed the possibility to implement a workaround using events. We need router events to make things work that Next.js team does not fix. |
Beta Was this translation helpful? Give feedback.
-
Hey everyone, I wanted to share one solution that might be relevant for you all. I saw a few comments in here related to handing popstate and the browser back button, so figured I'd write this up. Here was my use case:
I can definitely empathize with this being confusing, and apologize we haven't done enough to create examples or show how this could be possible. For example, trying to animate across routes using The solution I landed on is this:
This isn't perfect – for example, if you manually swipe back in iOS Safari. The most ideal solution here is View Transitions, which you can use https://next-view-transitions.vercel.app for in some cases. However, I couldn't get it working perfectly for the animation I was going for in this case, and also there isn't Firefox support yet for View Transitions. I do believe this is the best solution in the future though. CleanShot.2024-12-29.at.14.56.21.mp4Hope this helps! 🙏 |
Beta Was this translation helpful? Give feedback.
-
@leerob please give me a simple solution of how to detect page change is |
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: