Next.js Discord

Discord Forum

Route interception

Answered
English Springer Spaniel posted this in #help-forum
Open in Discord
English Springer SpanielOP
How the duck do we prevent an intercepted route from showing when you update the search params from the page that was intercepted?
Answered by English Springer Spaniel
I had to create this silly component to handle whether or not the route should be blocked:

//RouteInterceptBlocker.tsx
"use client";
import { usePathname } from "next/navigation";
import { useRef, createContext, useContext, type ReactNode } from "react";

type RouteInterceptBlockerContextValue = {
  shouldBlock: (path: string) => boolean;
};

const RouteInterceptBlockerContext =
  createContext<RouteInterceptBlockerContextValue | null>(null);

export const useRouteInterceptBlocker = () => {
  const context = useContext(RouteInterceptBlockerContext);
  if (!context) {
    throw new Error(
      "RouteInterceptBlockerProvider must be used within a RouteInterceptBlockerContext",
    );
  }
  return context;
};

export const RouteInterceptBlockerProvider = ({
  children,
}: { children?: ReactNode }) => {
  const pathnameRef = useRef(usePathname());
  const shouldBlock = (path: string) => {
    const currentPathname = pathnameRef.current;
    return currentPathname.startsWith(path);
  };
  return (
    <RouteInterceptBlockerContext.Provider value={{ shouldBlock }}>
      {children}
    </RouteInterceptBlockerContext.Provider>
  );
};

//app/@aside/(.)search/components.tsx
const _Drawer = ({ children }: { children: ReactNode }) => {
  const [isOpen, setIsOpen] = useState(false);

  const router = useRouter();

  useLayoutEffect(() => {
    setIsOpen(true);
  }, []);

  const handleClose = useEffectEvent(() => {
    if (!isOpen) {
      router.back();
    }
  });

  return (
    <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
      <AnimatePresence onExitComplete={handleClose}>
        {isOpen && <Dialog.Portal forceMount>{children}</Dialog.Portal>}
      </AnimatePresence>
    </Dialog.Root>
  );
};

export const Drawer = ({ children }: { children: ReactNode }) => {
  const pathname = usePathname();
  const { shouldBlock } = useRouteInterceptBlocker();
  const block = shouldBlock("/search");

  return !block && pathname === "/search" && <_Drawer>{children}</_Drawer>;
};

// app/@aside/(.)search/layout.tsx
import * as Search from "./components";

export default function SearchLayout({ children }: { children: ReactNode }) {
  return (
    <Search.Drawer>
      {/* other layout stuff /*}
      {children}
    </Search.Drawer>
  )
}
View full answer

6 Replies

English Springer SpanielOP
I had to create this silly component to handle whether or not the route should be blocked:

//RouteInterceptBlocker.tsx
"use client";
import { usePathname } from "next/navigation";
import { useRef, createContext, useContext, type ReactNode } from "react";

type RouteInterceptBlockerContextValue = {
  shouldBlock: (path: string) => boolean;
};

const RouteInterceptBlockerContext =
  createContext<RouteInterceptBlockerContextValue | null>(null);

export const useRouteInterceptBlocker = () => {
  const context = useContext(RouteInterceptBlockerContext);
  if (!context) {
    throw new Error(
      "RouteInterceptBlockerProvider must be used within a RouteInterceptBlockerContext",
    );
  }
  return context;
};

export const RouteInterceptBlockerProvider = ({
  children,
}: { children?: ReactNode }) => {
  const pathnameRef = useRef(usePathname());
  const shouldBlock = (path: string) => {
    const currentPathname = pathnameRef.current;
    return currentPathname.startsWith(path);
  };
  return (
    <RouteInterceptBlockerContext.Provider value={{ shouldBlock }}>
      {children}
    </RouteInterceptBlockerContext.Provider>
  );
};

//app/@aside/(.)search/components.tsx
const _Drawer = ({ children }: { children: ReactNode }) => {
  const [isOpen, setIsOpen] = useState(false);

  const router = useRouter();

  useLayoutEffect(() => {
    setIsOpen(true);
  }, []);

  const handleClose = useEffectEvent(() => {
    if (!isOpen) {
      router.back();
    }
  });

  return (
    <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
      <AnimatePresence onExitComplete={handleClose}>
        {isOpen && <Dialog.Portal forceMount>{children}</Dialog.Portal>}
      </AnimatePresence>
    </Dialog.Root>
  );
};

export const Drawer = ({ children }: { children: ReactNode }) => {
  const pathname = usePathname();
  const { shouldBlock } = useRouteInterceptBlocker();
  const block = shouldBlock("/search");

  return !block && pathname === "/search" && <_Drawer>{children}</_Drawer>;
};

// app/@aside/(.)search/layout.tsx
import * as Search from "./components";

export default function SearchLayout({ children }: { children: ReactNode }) {
  return (
    <Search.Drawer>
      {/* other layout stuff /*}
      {children}
    </Search.Drawer>
  )
}
Answer
English Springer SpanielOP
The above was the only way I could figure this one out. It's important to note that the apps RootLayout should be wrapped with RouteInterceptBlockerProvider:
// app/layout.tsx
import { RouteInterceptBlockerProvider } from "@/components/RouteInterceptBlocker";

export default function RootLayout({
  children,
  aside,
}: {
  children: ReactNode;
  aside: React.ReactNode;
}) {
  return (
    <RouteInterceptBlockerProvider>
  <html>
    <body>
      {children}
      {aside}
    </body>
  </html>
</RouteInterceptBlockerProvider>
  )
}
English Springer SpanielOP
I'm sorry the above actually doesn't seem to work when updating the url.
The intercepted route prevents the actual page from receiving updates.
English Springer SpanielOP
This solution did not work, I put in issue up on nextjs -> https://github.com/vercel/next.js/issues/86362
My solution rn is to intercept the route that I want and then create the other "interceptable" page on a whole other route. To handle hard refreshes I have a GET route for the route which redirects you to the route I want to act as the hard route.