[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: 49 comments 128 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.
-
For us we needed to persist a specific query param across all route changes and instead of sending it via each router push or Link navigation this is what worked for us. Then you can import it in the layouts
|
Beta Was this translation helpful? Give feedback.
-
Can we reintroduce router events, please? I need to display a top-loader during router changes, so we need the route change start and end 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.
-
The page no longer redirects to the road map, and it has been a long time to see such interceptors... |
Beta Was this translation helpful? Give feedback.
-
Hi everyone, if you're looking to add a progress bar in the app dir, this blog post is helpful: https://buildui.com/posts/global-progress-in-nextjs Just to be clear - creating a wrapper around the Link component that reimplements router navigation is a hacky solution in my book. I believe Next.js should expose a boolean for pending navigations. More in my previous comment. |
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 SolutionsDisplaying a progress indicator while a route transition is happeningAll navigations in the Next.js App Router are built on React Transitions. This means you can use the 'use client';
import { useTransition } from 'react';
import NextLink from 'next/link';
import { useRouter } from 'next/navigation';
/**
* A custom Link component that wraps Next.js's next/link component.
*/
export function Link({
href,
children,
replace,
...rest
}: Parameters<typeof NextLink>[0]) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
if (isPending) {
return <>Pending navigation</>;
}
return (
<NextLink
href={href}
onClick={(e) => {
e.preventDefault();
startTransition(() => {
const url = href.toString();
if (replace) {
router.replace(url);
} else {
router.push(url);
}
});
}}
{...rest}
>
{children}
</NextLink>
);
} We’re also going to work on a proposal for a There is also a new library called CleanShot.2024-04-03.at.07.46.09.mp4It can also be used to show a progress bar when calling other React Transitions like Server Actions and the Shallow routingIn Next.js 14.1 we’ve introduced support for navigating similar to “shallow routing” in Pages Router by using Integrating with analytics libraries or similar tools that need to listen for different routing changes and report back to the serviceFor analytics, we’re investigating if a proposal similar to It would look similar to the following example, which you can start using today: // lib/use-report-analytics.ts
import 'client-only';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
export function useReportAnalytics(reportAnalyticsFn: (url: string) => void) {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const url = `${pathname}?${searchParams}`;
reportAnalyticsFn(url);
}, [pathname, searchParams]);
} Preventing navigation away with unsaved changes and show a confirmation modelFor blocking the navigations, we’re going to investigate if there is a built-in approach that works for most use-cases. For example, you don’t want to prevent all React Transitions, because that means you can’t submit the form anymore (as that is a React Transition, too). If you only want to block navigations using e.g. // components/navigation-block.tsx
'use client';
import {
Dispatch,
SetStateAction,
createContext,
useContext,
useEffect,
useState,
} from 'react';
const NavigationBlockerContext = createContext<
[isBlocked: boolean, setBlocked: Dispatch<SetStateAction<boolean>>]
>([false, () => {}]);
export function NavigationBlockerProvider({
children,
}: {
children: React.ReactNode;
}) {
// [isBlocked, setBlocked]
const state = useState(false);
return (
<NavigationBlockerContext.Provider value={state}>
{children}
</NavigationBlockerContext.Provider>
);
}
export function useIsBlocked() {
const [isBlocked] = useContext(NavigationBlockerContext);
return isBlocked;
}
export function Blocker() {
const [isBlocked, setBlocked] = useContext(NavigationBlockerContext);
useEffect(() => {
setBlocked(() => {
return true;
});
return () => {
setBlocked(() => {
return false;
});
};
}, [isBlocked, setBlocked]);
return null;
}
export function BlockBrowserNavigation() {
const isBlocked = useIsBlocked();
useEffect(() => {
console.log({ isBlocked });
if (isBlocked) {
const showModal = (event: BeforeUnloadEvent) => {
event.preventDefault();
};
window.addEventListener('beforeunload', showModal);
return () => {
window.removeEventListener('beforeunload', showModal);
};
}
}, [isBlocked]);
return null;
} // components/link.tsx
'use client';
import { startTransition } from 'react';
import NextLink from 'next/link';
import { useRouter } from 'next/navigation';
import { useIsBlocked } from './navigation-block';
/**
* A custom Link component that wraps Next.js's next/link component.
*/
export function Link({
href,
children,
replace,
...rest
}: Parameters<typeof NextLink>[0]) {
const router = useRouter();
const isBlocked = useIsBlocked();
return (
<NextLink
href={href}
onClick={(e) => {
e.preventDefault();
// Cancel navigation
if (isBlocked && !window.confirm('Do you really want to leave?')) {
return;
}
startTransition(() => {
const url = href.toString();
if (replace) {
router.replace(url);
} else {
router.push(url);
}
});
}}
{...rest}
>
{children}
</NextLink>
);
} // components/form.tsx
import { Blocker } from './navigation-block';
export default function Form() {
return (
<>
<Blocker />
<input type="text" name="firstname" />
<input type="text" name="lastname" />
</>
);
} // app/layout.tsx
import {
BlockBrowserNavigation,
NavigationBlockerProvider,
} from '@/components/navigation-block';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<NavigationBlockerProvider>
{/* Blocks reloading the page / navigating away to other urls */}
{/* <BlockBrowserNavigation /> */}
{children}
</NavigationBlockerProvider>
</body>
</html>
);
} Then use it when the form has been edited, for example with // components/form.tsx
import { useForm, useFormState } from 'react-hook-form';
import { Blocker } from './navigation-block';
function BlockerWhenDirty({
control,
}: {
control: ReturnType<typeof useForm>['control'];
}) {
const { dirtyFields } = useFormState({
control,
});
return Object.keys(dirtyFields).length > 0 ? <Blocker /> : null;
} It’s worth mentioning there’s no browser behavior to prevent popstate (back or forward navigations), so there isn’t a way to add that to the router. ConclusionWe know the the transition from the Pages Router to the App Router, even when taken incrementally, can create questions about how to use features or APIs the previous router supported, but look different in the new world. Thank you all for the feedback and patience here. We encourage you all to share solutions you’ve found for your own applications. And if the provided solutions above are working well for you all, we’ll work on moving these to the official documentation. |
Beta Was this translation helpful? Give feedback.
-
I'd like to propose a different solution, other than taking every interaction point of application code -> App Router and manually tracking the state of the router transition. The router already uses I agree that we can use the pathname/params to know when the router change has completed, but we still need a realistic way to know when a router change has started. I can't imagine the change is quite as simple as the commit I linked above, but isn't this a more reasonable direction to go in? |
Beta Was this translation helpful? Give feedback.
-
If there an ETA solution for the popState when user goes back in the broswer button? |
Beta Was this translation helpful? Give feedback.
-
Just, WHY? It was working as well with pages router, why it is removed from app router? There are a ton of message here and other topics. With app router we have a lot of problems like that, should we roll back to pages router? |
Beta Was this translation helpful? Give feedback.
-
I have nothing to add but Ive been getting emails about this issue for months by now. @leerob in the trenches on this one, love to see it. |
Beta Was this translation helpful? Give feedback.
-
@leerob why can't |
Beta Was this translation helpful? Give feedback.
-
For the use case of showing a global progress indicator during route transitions, we came up with this hack that hooks directly into the router state. This builds on the idea of wrapping calls to the router in Major caveats:
import { useContext, useEffect, useTransition } from "react";
import { ActionQueueContext } from "next/dist/shared/lib/router/action-queue";
const useIsRouterPending = () => {
const actionQueue = useContext(ActionQueueContext);
const [isPending, startTransition] = useTransition();
useEffect(() => {
if (!actionQueue) {
return undefined;
}
const originalDispatch = actionQueue.dispatch;
actionQueue.dispatch = (...args) => {
startTransition(() => {
originalDispatch.apply(actionQueue, args);
});
};
return () => {
actionQueue.dispatch = originalDispatch;
};
}, [actionQueue]);
return isPending;
}; Of course this could break at any time since it's patching an internal API. |
Beta Was this translation helpful? Give feedback.
-
In my use case:
This makes the update visually slow. I would prefer to update my state asap, not when With events, I could subscribe to them and update my atoms directly, refreshing my components immediately with new data. A solution I see is subscribing to |
Beta Was this translation helpful? Give feedback.
-
I've been following this issue for a year, and it's clear that the Next.js people are not concerned about the lack of this functionality in the app router model. So I guess we won't be switching to it. For my own sanity I'll be unfollowing this thread. |
Beta Was this translation helpful? Give feedback.
-
What worked for me was use the provided solution of navigation-block with the addition of a custom hook that makes use of the new shallow routing feature:
usage:
based on this old stackoverflow answer and react-beforeunload lib. nextjs version: 14.2.2 |
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: