Skip to content

Instantly share code, notes, and snippets.

@ellemedit
Last active April 5, 2025 13:54
Show Gist options
  • Save ellemedit/e27574d2e03aca5899e01947009f5a71 to your computer and use it in GitHub Desktop.
Save ellemedit/e27574d2e03aca5899e01947009f5a71 to your computer and use it in GitHub Desktop.
Next.js Navigation API intercepter
"use client";
// Next.js version of React ViewTransition fixture
// ref: https://github.com/facebook/react/blob/main/fixtures/view-transition/src/components/App.js
//
// Currently, it has a infinite rendering bug in React when you navigate for go or back
// wait for merging following PRs
// PR 1: https://github.com/facebook/react/pull/32821
// PR 2: https://github.com/vercel/next.js/pull/77856
//
// You can use this like:
//
// export default function RootLayout({
// children,
// }: Readonly<{
// children: React.ReactNode;
// }>) {
// return (
// <html>
// <body>
// <NavigationIntercepter>{children}</NavigationIntercepter>
// </body>
// </html>
// );
// }
//
// example:
// <a
// onClick={(event) => {
// event.preventDefault();
// window.navigation.navigate("/b");
// }}
// href="/b"
// >
// Link
// </a>
import { useRouter } from "next/navigation";
import {
startTransition,
useEffect,
useInsertionEffect,
useState,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
} from "react";
export function NavigationIntercepter({
children,
}: {
children: React.ReactNode;
}) {
const [resolveNavigation, setWorkInProgressNavigation] = useState<
null | (() => void)
>(null);
const router = useRouter();
useEffect(() => {
function handleNavigate(event: NavigateEvent) {
if (!event.canIntercept) {
return;
}
const navigationType = event.navigationType;
if (!event.userInitiated && navigationType !== "traverse") {
return;
}
const previousIndex =
window.navigation.currentEntry == null
? 0
: window.navigation.currentEntry.index;
const navigationDirection =
navigationType === "traverse"
? event.destination.index > previousIndex
? "forward"
: "back"
: null;
const newUrl = new URL(event.destination.url);
const nextUrl = newUrl.pathname + newUrl.search;
event.intercept({
handler() {
const { promise, resolve } = Promise.withResolvers<void>();
startTransition(() => {
addTransitionType("navigation-" + navigationType);
if (navigationDirection != null) {
addTransitionType("navigation-" + navigationDirection);
}
switch (navigationType) {
case "push":
router.push(nextUrl);
break;
case "replace":
router.replace(nextUrl);
break;
case "reload":
router.refresh();
break;
case "traverse":
router.replace(nextUrl);
break;
}
setWorkInProgressNavigation(resolve);
});
return promise;
},
});
}
window.navigation.addEventListener("navigate", handleNavigate);
return () => {
window.navigation.removeEventListener("navigate", handleNavigate);
};
}, [router]);
useInsertionEffect(() => {
if (!resolveNavigation) {
return;
}
resolveNavigation();
}, [resolveNavigation]);
return <ViewTransition>{children}</ViewTransition>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment