skip to Main Content

In nextjs . I want to block navigation and appear a confirmation window popup when user trying to navigate to another page. the navigation should continue if the user click yes in the confirmation popup. If the user clicks the "no" in confirmation window , the user should stay in the current page.

There is no way in the official nextjs documentation about blocking a router.

2

Answers


  1. You can use JavaScript’s window.onbeforeunload event to do that.

    See an example code below

    const ExamplePage = () => {
      useEffect(() => {
        window.onbeforeunload = () => true;
    
        return () => {
          window.onbeforeunload = null;
        };
      }, []);
    
      return (
        <div>
          <h1>Example Page</h1>
          <p>This page will display a confirmation dialog when navigating away.</p>
        </div>
      );
    };
    
    export default ExamplePage;
    
    Login or Signup to reply.
  2. If you are using NextJS 12 or less you can have the following:

    Stack

    • NextJS 12.x
    • Typescript
    • Tailwind
    • Some custom components, replace on your own.

    The idea behind the components below is to get when the router fires the routeChangeStart and stop it immediately, then we have two ways of getting user confirmation, you can use the window.confirm() which cannot be styled, so you probably don’t want that or you can trigger an async function (which is used on the example), pass a callback so you can continue the action in case the user press "Yes" and throw immediately after, if you don’t do that, it will continue the route change.

    BeforePopState is when you fire the go back / go forward on the router/browser.
    BeforeUnload is when the user refreshes or clicks to exit the page (in this case you can only show the browser default window.

    To help you understand better and implement, here is the logic I have created.

    Code examples

    /hooks/useWarnIfUnsaved

    import getUserConfirmation, {
      IConfirmationBody,
      IConfirmationDialogConfig,
    } from "@/utils/ui/confirmationDialog"
    import Router from "next/router"
    import { useEffect, useRef } from "react"
    import useWarnBeforeUnload from "./useWarnBeforeUnload"
    
    export interface TConfirmationDialog {
      body: IConfirmationBody
      config?: IConfirmationDialogConfig
    }
    
    /**
     * Asks for confirmation to leave/reload if there are unsaved changes.
     * @param {boolean} unsavedChanges Whether there are unsaved changes. Use a ref to store the value or make a comparison with current and previous values.
     * @param {TDialog} confirmationDialog [Optional] The dialog to show.
     * @returns {void}
     */
    export default function useWarnIfUnsaved(
      unsavedChanges: boolean,
      confirmationDialog: TConfirmationDialog
    ) {
      const unsavedRef = useRef(unsavedChanges)
      // * Keep unsaved in sync with the value passed in.
      // * At the same time we want to be able to reset
      // * the trigger locally so we can move forward on the redirection
      useEffect(() => {
        unsavedRef.current = unsavedChanges
      }, [unsavedChanges])
    
      useWarnBeforeUnload(unsavedRef.current)
    
      useEffect(() => {
        const handleRouteChange = (url: string) => {
          if (unsavedRef.current && Router.pathname !== url) {
            Router.events.emit("routeChangeError")
            getUserConfirmation(confirmationDialog.body, {
              ...confirmationDialog.config,
              callbackAfterConfirmation: () => {
                unsavedRef.current = false
                Router.replace(url)
              },
            })
            throw "Route change aborted. Ignore this error."
          }
        }
    
        Router.beforePopState(({ url }) => {
          if (unsavedRef.current) {
            if (Router.pathname !== url) {
              getUserConfirmation(confirmationDialog.body, {
                ...confirmationDialog.config,
                callbackAfterConfirmation: () => {
                  unsavedRef.current = false
                  window.history.pushState("", "", url)
                },
              })
              return false
            }
          }
          return true
        })
    
        // For changing in-app route.
        if (unsavedRef.current) {
          Router.events.on("routeChangeStart", handleRouteChange)
        } else {
          Router.events.off("routeChangeStart", handleRouteChange)
        }
        return () => {
          Router.beforePopState(() => true)
          Router.events.off("routeChangeStart", handleRouteChange)
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [unsavedRef.current])
    }
    

    @/utils/ui/confirmationDialog

    import Button from "@/components/Buttons/Button.primitive"
    import DynamicIcon from "@/components/Icons/DynamicIcon"
    import Text from "@/components/Typography/Text.primitive"
    import { PolarisIcons } from "@/types/polarisIcons.types"
    import { createRoot } from "react-dom/client"
    import { cn } from "../className"
    
    export interface IConfirmationDialogConfig {
      overrideBody?: JSX.Element
      cancelButtonText?: string
      okButtonText?: string
      title?: string
      hideOkButton?: boolean
      confirmationInput?: string
      className?: string
      bodyClassName?: string
      isDeleteConfirmation?: boolean
      callbackAfterConfirmation?: () => void
      callbackAfterCancel?: () => void
    }
    
    export interface IConfirmationBody {
      icon?: PolarisIcons | JSX.Element
      title: string
      message: string
    }
    
    interface IRenderDialogProps {
      resolve: (value: { value: string } | PromiseLike<{ value: string }>) => void
      body: IConfirmationBody
      config?: IConfirmationDialogConfig
    }
    
    type TPromiseResolve = (
      value:
        | {
            value: string
          }
        | PromiseLike<{
            value: string
          }>
    ) => void
    
    const renderDialog = (
      body: IConfirmationBody,
      resolve: TPromiseResolve,
      config?: IConfirmationDialogConfig
    ) => {
      const root = document.querySelector("body")
      if (!root) {
        console.error("No root element found.")
        return
      }
      const div = document.createElement("div")
      div.setAttribute("id", "confirmationDialogContainer")
      div.setAttribute(
        "class",
        "h-screen w-screen fixed z-50 inset-0 grid place-items-center globalConfirmationModalContainer"
      )
      root.appendChild(div)
      const container = document.getElementById("confirmationDialogContainer")
      if (container === null) {
        console.error("Container was not found.")
      }
      const dialog = createRoot(container as HTMLElement)
      return dialog.render(
        <ConfirmationDialog body={body} resolve={resolve} config={config} />
      )
    }
    
    const removeDialog = () => {
      const root = document.querySelector("body")
      if (!root) {
        console.error("No root element found.")
        return
      }
      const divs = root.querySelectorAll(".globalConfirmationModalContainer")
      divs && divs.forEach((div) => root.removeChild(div))
    }
    
    const ConfirmationDialog = ({ resolve, body, config }: IRenderDialogProps) => {
      const clickOK = () => {
        removeDialog()
        config?.callbackAfterConfirmation?.()
        resolve({ value: "true" })
      }
    
      const clickCancel = () => {
        removeDialog()
        config?.callbackAfterCancel?.()
        resolve({ value: "" })
      }
    
      return (
        <div
          className="fixed inset-0 grid h-screen w-screen place-items-center"
          style={{ zIndex: 9999 }}
        >
          <div
            className="fixed inset-0 z-0 h-screen w-screen bg-black opacity-60"
            onClick={clickCancel}
            onKeyDown={() => undefined}
            role="button"
            tabIndex={0}
          />
          <div className="dark:bg-blue-dark relative z-10 max-w-[95vw] rounded-lg bg-white shadow-lg">
            <div className="p-14">
              {config?.overrideBody ? (
                <></>
              ) : (
                <>
                  <div className="flex items-center justify-between border-b border-ds-gray-300 p-6">
                    <Text
                      as="p"
                      className="text-sm font-extrabold"
                      color="text-ds-primary-700 dark:text-ds-primary-200"
                    >
                      {config?.title || "Confirmation"}
                    </Text>
                    <Button
                      variant="icon"
                      subvariant="solo"
                      onClick={clickCancel}
                      iconName={PolarisIcons.CancelMajor}
                      className="border-none !p-0"
                    />
                  </div>
                  <div
                    className={cn(
                      "flex flex-col items-center justify-center px-7 py-12",
                      config?.bodyClassName
                    )}
                  >
                    {body.icon && (
                      <div className="mb-4 flex items-center justify-center rounded-full">
                        {typeof body.icon === "string" ? (
                          <DynamicIcon
                            iconName={body.icon as PolarisIcons}
                            width={120}
                            className="max-w-[25%] bg-ds-gray-400 dark:bg-ds-gray-700"
                          />
                        ) : (
                          body.icon
                        )}
                      </div>
                    )}
                    <div className="px-8 text-center">
                      <Text
                        as="h3"
                        className="mb-2 text-lg font-extrabold leading-[1.44]"
                        color="text-ds-primary-700 dark:text-ds-primary-200"
                      >
                        {body.title}
                      </Text>
                      <Text
                        as="p"
                        className="text-sm leading-[1.57] text-ds-gray-600 dark:text-ds-gray-400"
                        font="inter"
                      >
                        {body.message}
                      </Text>
                    </div>
                  </div>
                  <div className="flex items-center justify-end gap-3 border-t border-ds-gray-300 p-6">
                    <Button
                      variant="main"
                      subvariant="outlinePrimary"
                      size="md"
                      onClick={clickCancel}
                      className="text-ds-gray-600 dark:text-ds-gray-400"
                    >
                      {config?.cancelButtonText || "Cancel"}
                    </Button>
                    {!config?.hideOkButton && (
                      <Button
                        variant="main"
                        subvariant={
                          config?.isDeleteConfirmation ? "error" : "primary"
                        }
                        onClick={clickOK}
                        size="md"
                      >
                        {config?.okButtonText || "Ok"}
                      </Button>
                    )}
                  </div>
                  {/* <Text
                      as="h1"
                      label="Modal Title"
                      color="text-mirage dark:text-link-water"
                    >
                      {config?.title || "Confirmation"}
                    </Text>
                    <Text as="p">{body}</Text>
                  </div>
                  <div className="mt-10 flex items-center justify-center gap-3">
                    <Button
                      variant="main"
                      subvariant={
                        config?.isDeleteConfirmation ? "outlinePrimary" : "primary"
                      }
                      onClick={clickCancel}
                    >
                      {config?.cancelButtonText || "Cancel"}
                    </Button>
                    {!config?.hideOkButton && (
                      <Button
                        variant={config?.isDeleteConfirmation ? "5" : "2"}
                        onClick={clickOK}
                        className="px-8"
                      >
                        {config?.okButtonText || "Ok"}
                      </Button>
                    )} 
                    </div>*/}
                </>
              )}
            </div>
          </div>
        </div>
      )
    }
    
    /**
     *
     * @param {IConfirmationBody} body
     * @param {IConfirmationDialogConfig} config
     * @returns
     */
    const getUserConfirmation = (
      body: IConfirmationBody,
      config?: IConfirmationDialogConfig
    ): Promise<{ value: string }> =>
      new Promise((resolve) => {
        renderDialog(body, resolve, config)
      })
    
    export default getUserConfirmation
    

    Usage

    // only one simple example
    const UnsavedChangesDialog: TConfirmationDialog = {
      body: {
        icon: <svg>ICON CODE OPTIONAL</svg>,
        title: "Your title",
        message:
          "Your message",
      },
      config: {
        okButtonText: "Yes",
        cancelButtonText: "No",
        title: "Unsaved changes",
        isDeleteConfirmation: true,
        bodyClassName: "px-[15vw]",
      },
    }
    
    const hasChanges = useRef(false)
    useWarnIfUnsaved(hasChanges.current, UnsavedChangesDialog)
    
    const handleFormChange = (e: ChangeEvent<HTMLInputElement>) => {
        hasChanges.current = true
        ...
    }
    
    const submitForm = (data) => {
        ...
        if (success) {
            // important to set as false when you actually submit so you can stop showing the message
            hasChanges.current = false
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search