[Next 13] router.events removed? #42016
-
router.events doesn't exist anymore on Next.js 13 and there doesn't seem to be a replacement, is this a permanent change? |
Beta Was this translation helpful? Give feedback.
Replies: 22 comments 41 replies
-
I think it's on their roadmap (interceptors). I asked the same here: #41934 |
Beta Was this translation helpful? Give feedback.
-
I think I have
|
Beta Was this translation helpful? Give feedback.
-
Previously I used |
Beta Was this translation helpful? Give feedback.
-
Learn more here: https://nextjs.org/docs/app/api-reference/functions/use-router#router-events |
Beta Was this translation helpful? Give feedback.
-
useEffect(() => {
router.events.on('routeChangeComplete', () => setShowMobileNavbar(false));
router.events.on('routeChangeError', () => setShowMobileNavbar(false));
return () => {
router.events.off('routeChangeComplete', () => setShowMobileNavbar(false));
router.events.off('routeChangeError', () => setShowMobileNavbar(false));
};
}) I can't listen to router events. What is an alternative way to write code? |
Beta Was this translation helpful? Give feedback.
-
If anybody was using it to show loading between page transitions then you can use the following |
Beta Was this translation helpful? Give feedback.
-
The loading.(js|tsx) file will replace all the current content with the loading component which is not what i want to. I hope the router events back, or equally for Next 13 within app router |
Beta Was this translation helpful? Give feedback.
-
I am using something like this, basically not using
|
Beta Was this translation helpful? Give feedback.
-
Anyone have a working example of |
Beta Was this translation helpful? Give feedback.
-
I create a NextJs Context and Hook to help you handle with the code that will be run when user leave some application route. import React from 'react';
import { usePathname } from 'next/navigation';
type TargetsRouteToHandle = {
[x: string]: (() => void) | undefined;
};
type NavigationEventContextValue = {
addTargetRoute: (targetRoute: AddTargetRouteProps) => void;
};
type AddTargetRouteProps = {
pathName: string;
hookFunction: TargetsRouteToHandle['x'];
};
const NavigationEventContext = React.createContext<NavigationEventContextValue>(
{} as NavigationEventContextValue
);
export const useNavigationEvent = () =>
React.useContext(NavigationEventContext);
function NavigationEventProvider({ children }: React.PropsWithChildren) {
const pathName = usePathname();
const pathHistory = React.useRef<[string, string]>(['', pathName]);
const [targetsRouteToHandle, setTargetsRouteToHandle] =
React.useState<TargetsRouteToHandle>({});
const addTargetRoute = ({ pathName, hookFunction }: AddTargetRouteProps) => {
setTargetsRouteToHandle((prev) => ({ ...prev, [pathName]: hookFunction }));
};
React.useEffect(() => {
const prev = pathHistory.current.at(0) as string;
const isLeavingATargetRoute = targetsRouteToHandle[prev];
if (isLeavingATargetRoute) isLeavingATargetRoute();
pathHistory.current.push(pathHistory.current.shift() as string);
pathHistory.current.unshift(pathName);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathName]);
return (
<NavigationEventContext.Provider value={{ addTargetRoute }}>
{children}
</NavigationEventContext.Provider>
);
}
export default NavigationEventProvider; Example of usethis way every time the user leave the route '/chat' the code from hookFunction will be called, you can create how many addTargetRoute you want. React.useEffect(() => {
addTargetRoute({
pathName: '/chat',
hookFunction: () => {
console.log('Trocou a pagina');
}
});
}, []); Dot forget to wrap your application in the NavigationEventProvider providerimport NavigationEventProvider from '@contexts/NavigationEventContext';
function Providers({ children }: PropsWithChildren) {
return (
<NavigationEventProvider>{children}</NavigationEventProvider>
)
} |
Beta Was this translation helpful? Give feedback.
-
For anyone who needs a means to intercept the old 'routeChangeStart' events; this is a hook I came up with. import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { v4 as uuid } from 'uuid';
const CALLBACK_BY_LISTENER: { [id: string]: (acceptAction: () => void) => void } = {};
function acceptActionWhenReady(acceptAction: () => void) {
const listeners = Object.values(CALLBACK_BY_LISTENER);
if (listeners.length == 0) {
acceptAction();
} else {
listeners.forEach((listener) => {
listener(acceptAction);
});
}
}
function addListener(callback: (acceptAction: () => void) => void): string {
const id = uuid();
CALLBACK_BY_LISTENER[id] = callback;
return id;
}
function removeListener(id: string): void {
delete CALLBACK_BY_LISTENER[id];
}
interface AppRouterResponse {
push: (route: string) => void;
acceptAction: (() => void) | undefined;
}
/**
* @param shouldWarn if true, the component will be subscribed to the router push event before navigation.
* @returns a push function (like next's useRouter().push) and an acceptAction function that the subscribing function can call when ready to navigate.
*/
export default function useAppRouter(shouldWarn?: boolean): AppRouterResponse {
const [{ acceptAction }, setAcceptAction] = useState<{ acceptAction?: () => void }>({ acceptAction: undefined });
const { push } = useRouter();
useEffect(() => {
if (shouldWarn) {
const id = addListener((acceptAction) => {
setAcceptAction({ acceptAction });
});
return () => {
removeListener(id);
};
}
}, [shouldWarn]);
return {
push: (route: string) => {
acceptActionWhenReady(() => {
push(route);
});
},
acceptAction,
};
} It can be used as such: export function HomeButton() {
const {push} = useAppRouter();
return <button onClick={() => {push('/home')}}>Home</button>
}
export function PreventNavModal(preventNav: boolean) {
const {acceptAction} = useAppRouter(preventNav);
return <Modal open={acceptAction != undefined}><button onClick={() => acceptAction()}>Continue</button></Modal>
} |
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.
-
For me it does. Without events we can not call the router fully functional and production ready. Regardless of the number of chunks, each view has URL and it makes sense to fire It is absolutely critical functionality to prevent customer data loss and significantly improve user experience. And for now there is literally no good way to do this. I don't have much time for this at the moment, and that is why it is even more annoying. That was working and become broken. This hook let's control import { useTranslations } from 'next-intl';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
// TODO: Implement this hook properly if we have any other option like router events
// or interceptors
const clickType = typeof document !== 'undefined' && document.ontouchstart ? 'touchstart' : 'click';
interface GuardEventListener {
onBeforeUnload: () => boolean;
}
export default function usePageUnloadGuard() {
const router = useRouter();
const t = useTranslations('studio');
const pathname = usePathname();
const searchParams = useSearchParams();
const listener: GuardEventListener = {
onBeforeUnload: () => false,
};
const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
listener.onBeforeUnload() && event.preventDefault();
};
const clickHandler = (event: MouseEvent | TouchEvent) => {
if ((event as MouseEvent).button || event.which !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const target = event.target as HTMLElement;
if (target.tagName !== 'A') {
return;
}
const newPath = target.getAttribute('href');
if (newPath && newPath !== window.location.pathname && listener.onBeforeUnload()) {
event.preventDefault();
// NOTE: There is an option to show standard beforeunload dialog here by
// actually assigning window.location.href to newPath. That will be more
// consistent, but will cause full page reload in case of the user decides to
// go back.
// window.location.href = newPath; // This will show the standard dialog
if (confirm(t('dialogs.unsaved.title'))) {
router.push(newPath);
}
}
};
const popStateHandler = (event: PopStateEvent) => {
if (event.state !== null) {
return;
}
if (listener.onBeforeUnload() && confirm(t('dialogs.unsaved.title'))) {
window.removeEventListener('popstate', popStateHandler);
history.back();
return;
}
// Returning to the fake history state
history.go(1);
};
useEffect(() => {
// Since 'popstate' fires at the end of the page swap, there is no option to
// cancel it. So we're adding artificial state and in case we got there,
// it means a user pressed back button.
// TODO: Check if it is safe to add these to deps so the effect will re-run with pathname and params change
history.pushState(null, '', pathname + searchParams.toString());
window.addEventListener('beforeunload', beforeUnloadHandler);
window.addEventListener('popstate', popStateHandler);
window.document.addEventListener(clickType, clickHandler, { capture: true });
return () => {
window.removeEventListener('popstate', popStateHandler);
window.removeEventListener('beforeunload', beforeUnloadHandler);
window.document.removeEventListener(clickType, clickHandler, { capture: true });
};
});
return listener;
} it can be used like this const listener = usePageUnloadGuard();
listener.onBeforeUnload = () => !!someCondition; [UPDATE]: Added a code to handle back button |
Beta Was this translation helpful? Give feedback.
-
Intercept route changes at the NextJS app router modeDemo:[codeSondbox] cf6e2e9c42a4f29b1dacadffb58c9a1f_723815601830_v_1702122801840414.mp4source code: https://github.com/cgfeel/next.v2/tree/master/routing-file/src/app/leaving/proxy Use this Provider in your layout at the app root directory:https://github.com/cgfeel/next.v2/blob/master/routing-file/src/components/proxyProvider/index.tsx 'use client'
import { usePathname, useSearchParams } from "next/navigation";
import Script from "next/script";
import { FC, PropsWithChildren, createContext, useEffect, useState } from "react";
const ProxyContext = createContext<ProxyInstance>([undefined, () => {}]);
const ProxyProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
const [tips, setTips] = useState<string|undefined>();
const msg = tips === undefined ? tips : (tips||'Are you sure want to leave this page?');
const pathname = usePathname();
const searchParams = useSearchParams();
const url = [pathname, searchParams].filter(i => i).join('?');
useEffect(() => {
setTips(undefined);
}, [url, setTips]);
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (msg === undefined) return msg;
event.preventDefault();
event.returnValue = msg;
return msg;
};
const script = document.getElementById('proxy-script');
if (script) {
script.dataset.msg = msg||'';
script.dataset.href = location.href;
}
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
}
}, [msg]);
return (
<ProxyContext.Provider
value={[msg, setTips]}
>
<Script
strategy="afterInteractive"
id="proxy-script"
dangerouslySetInnerHTML={{
__html: `(() => {
const originalPushState = history.pushState.bind(history);
let currentPoint = 0;
let point = 0;
window.history.pushState = function(state, title, url) {
state.point = ++point;
currentPoint = point;
originalPushState(state, title, url);
};
const originalReplaceState = history.replaceState.bind(history);
window.history.replaceState = function(state, title, url) {
state.point = currentPoint;
originalReplaceState(state, title, url);
};
window.addEventListener('popstate', function (event) {
const { state: nextState } = event;
const isback = currentPoint > nextState.point;
currentPoint = nextState.point;
const script = document.getElementById('proxy-script');
if (!script || location.href === script.dataset.href) return;
const msg = script.dataset.msg||'';
const confirm = msg == '' ? true : window.confirm(msg);
if (!confirm) {
event.stopImmediatePropagation();
isback ? history.forward() : history.back();
}
});
})()`,
}}
></Script>
{children}
</ProxyContext.Provider>
);
};
export type ProxyInstance = [
string|undefined, (tips?: string) => void
]
export { ProxyContext };
export default ProxyProvider; |
Beta Was this translation helpful? Give feedback.
-
"use client"; type CustomLinkProps = ComponentProps; const CustomLink: React.FC = ({ children, href, ...rest }) => { useEffect(() => { const handleClick = () => {
}; return ( export default CustomLink; |
Beta Was this translation helpful? Give feedback.
-
any solution for this? i want trigger function before ,close, refresh,
it not fire when click to another page, any solution without router.events ? |
Beta Was this translation helpful? Give feedback.
-
@leerob Please can you add router events for NextJS if this package is able to use Router events please can you add to core NextJS @leerob I can assure you that We need NextJS router events any way. There is no other suitable alternative for that. Please add this to native NextJS Library |
Beta Was this translation helpful? Give feedback.
-
I'm struggling with this too at the moment, In pages router I could simply use events and on navigating away I was able to save my order to session storage. Now this seems to be impossible to do. Anyone has any suggestions? Should I instead save it to 'global state'? What are my options without
|
Beta Was this translation helpful? Give feedback.
-
For those looking to add a progress bar during navigation, I added a helpful resource to the other events discussion: #41934 (comment)
|
Beta Was this translation helpful? Give feedback.
-
For those still looking for a solution, you can edit the NavigationEvents component shown in the docs to modify the router.push method. "use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import nProgress from "nprogress";
import "nprogress/nprogress.css";
import { useEffect } from "react";
export function NavigationEvents() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
const _push = router.push.bind(router);
router.push = (href, options) => {
nProgress.start();
_push(href, options);
};
}, [])
useEffect(() => {
nProgress.done();
}, [pathname, searchParams]);
return null;
}
|
Beta Was this translation helpful? Give feedback.
-
I did this . It worked in most of the link tags. I created a LoadingBar Component that I imported in layout.js Here I am calling a loader
}, []); ` I hope this works. |
Beta Was this translation helpful? Give feedback.
-
I'm going to combine this Discussion with a duplicate here: #41934 (comment) We just posted a longer message there with some solutions! |
Beta Was this translation helpful? Give feedback.
I'm going to combine this Discussion with a duplicate here: #41934 (comment)
We just posted a longer message there with some solutions!