Skip to content

Instantly share code, notes, and snippets.

@MarksCode
Last active October 31, 2024 11:03
Show Gist options
  • Save MarksCode/64e438c82b0b2a1161e01c88ca0d0355 to your computer and use it in GitHub Desktop.
Save MarksCode/64e438c82b0b2a1161e01c88ca0d0355 to your computer and use it in GitHub Desktop.
return `usePrompt` capabilities from react-router
/**
* Prompts a user when they exit the page
*/
import { useCallback, useContext, useEffect } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
function useConfirmExit(confirmExit: () => boolean, when = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) {
return;
}
const push = navigator.push;
navigator.push = (...args: Parameters<typeof push>) => {
const result = confirmExit();
if (result !== false) {
push(...args);
}
};
return () => {
navigator.push = push;
};
}, [navigator, confirmExit, when]);
}
export function usePrompt(message: string, when = true) {
useEffect(() => {
if (when) {
window.onbeforeunload = function () {
return message;
};
}
return () => {
window.onbeforeunload = null;
};
}, [message, when]);
const confirmExit = useCallback(() => {
const confirm = window.confirm(message);
return confirm;
}, [message]);
useConfirmExit(confirmExit, when);
}
@L2Develop96
Copy link

Thanks it's working perfectly ! But is there anyway to make a custom dialog instead of the standard confirm dialog?

@Niyatihd
Copy link

Hello, was anyone able to make this work with custom dialog box??

@Niyatihd
Copy link

Thanks it's working perfectly ! But is there anyway to make a custom dialog instead of the standard confirm dialog?

Did you happen to find a way???

@Niyatihd
Copy link

Niyatihd commented Jun 12, 2023

The implementation for custom dialog that worked for me,

import { useCallback, useContext, useEffect } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
import { useConfirmModal } from 'src/components';

const useConfirmExit = (confirmExit: (confirmNavigation: () => void) => void, when = true) => {
  const { navigator } = useContext(NavigationContext);

  useEffect(() => {
    if (!when) return;

    const push = navigator.push;

    navigator.push = (...args: Parameters<typeof push>) => {
      const confirm = () => push(...args);
      confirmExit(confirm);
    };

    return () => {
      navigator.push = push;
    };
  }, [navigator, confirmExit, when]);
};

export const usePrompt = (message: string, when = true, onContinue?: () => void) => {
  const { confirm } = useConfirmModal();

  // This is to ensure that work is saved on page accidental exit or refresh
  useEffect(() => {
    if (when) {
      window.onbeforeunload = function () {
        return message;
      };
    }

    return () => { window.onbeforeunload = null };
  }, [message, when]);

  const confirmExit = useCallback((confirmNavigation: () => void) => {
    const onConfirm = () => {
      if (onContinue) onContinue();
      confirmNavigation();
    }

    confirm.warning({
      title: '',
      description: message,
      confirmText: 'Continue',
      onConfirm,
    });
  }, [message, onContinue]);

  useConfirmExit(confirmExit, when);
};

@BustamanteMelkia
Copy link

Great job!, it works

@mayafelipe
Copy link

mayafelipe commented Sep 1, 2023

Hey @Niyatihd. How did you use the useConfirmModal, is it a context ? Could you share this implementataion please?

@CleverAtBen
Copy link

@Niyatihd

The implementation for custom dialog that worked for me,

I would be keen to see the useConfirmModal implementation too please !!

@denchiklut
Copy link

@mayafelipe and @CleverAtBen I added an example with useConfirm.

export const usePrompt = () => {
    const [isDirty, setDirty] = useState(false)
    const blocker = useBlocker(isDirty)
    const { show } = useConfirm()

    const confirm = useCallback(() => {
        if (!isDirty) return Promise.resolve(true)

        return new Promise<boolean>(resolve => {
          show({
                title: 'You have unsaved changes.',
                subtitle: 'Changes you made may not be saved.',
                confirmText: 'Confirm',
                cancelText: 'Cancel',
                onConfirm: () => resolve(true),
                onCancel: () => resolve(false)
            })
        })
    }, [isDirty, show])

    useEffect(() => {
        if (blocker.state === 'blocked') {
            confirm().then(result => {
                if (result) blocker.proceed()
                else blocker.reset()
            })
        }
    }, [blocker, confirm])

    useEffect(() => {
        if (isDirty) window.onbeforeunload = () => 'Changes you made may not be saved.'

        return () => {
            window.onbeforeunload = null
        }
    }, [isDirty])

    return { setDirty }
}

Run yarn dev or yarn spa to run the app.
Hope this will be helpfull

@dev2-piniada
Copy link

Hey @Niyatihd. can you show me how is the implementation of useConfirmModal

@denchiklut
Copy link

@dev2-piniada you can check the example from comment above. You can find useConfirm implementation there

@yara-tle
Copy link

yara-tle commented May 28, 2024

Hey @Niyatihd, would like to know how u made the useConfirmModal as well? @denchiklut your example wont work because can't use useBlocker with current router setup. Do you have any idea of using a custom modal with this example?

@denchiklut
Copy link

your example wont work because cant import unstable_useBlocker from the router.

@yara-tle yeah this example was written while it was unstable API. Now you can import useBlocker without unstable prefix

@dkovah
Copy link

dkovah commented Jul 16, 2024

@denchiklut useBlocker and usePrompt don't work with routers that don't have access to the new data API, like BrowserRouter component, this gist does

@saomartinho
Copy link

hey, do you manage to block when the user clicks the back browser button?

@kanavi57
Copy link

kanavi57 commented Oct 17, 2024

for anyone having issue with the back button, check this code

import { useCallback, useContext, useEffect } from "react";
import { useDispatch } from "react-redux";
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";

function useConfirmExit(confirmExit: () => boolean, when = true) {
  const { navigator } = useContext(NavigationContext);

  useEffect(() => {
    if (!when) {
      return;
    }

    const push = navigator.push;

    navigator.push = (...args: Parameters<typeof push>) => {
      const result = confirmExit();

      if (result !== false) {
        push(...args);
      }
    };

    return () => {
      navigator.push = push;
    };
  }, [navigator, confirmExit, when]);
}

export function usePrompt(message: string, when = true) {
  useEffect(() => {
    if (!when) return;
    window.onbeforeunload = function () {
      return message;
    };

    window.history.pushState(null, "", window.location.href);
    window.onpopstate = function () {
      const confirm = window.confirm(message);

      if (confirm) {
        dispatch(setIsEditing(false));
        window.onpopstate = null;
        window.history.back();
      } else {
        window.history.pushState(null, "", window.location.href);
      }
    };
    return () => {
      window.onbeforeunload = null;
      window.onpopstate = null;
      }, [message, when]);

  const confirmExit = useCallback(() => {
    const confirm = window.confirm(message);

    if (confirm) {
      dispatch(setIsEditing(false));
    }

    return confirm;
  }, [message]);
  useConfirmExit(confirmExit, when);
}

@saomartinho
Copy link

for anyone having issue with the back button, check this code

import { useCallback, useContext, useEffect } from "react";
import { useDispatch } from "react-redux";
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";

function useConfirmExit(confirmExit: () => boolean, when = true) {
  const { navigator } = useContext(NavigationContext);

  useEffect(() => {
    if (!when) {
      return;
    }

    const push = navigator.push;

    navigator.push = (...args: Parameters<typeof push>) => {
      const result = confirmExit();

      if (result !== false) {
        push(...args);
      }
    };

    return () => {
      navigator.push = push;
    };
  }, [navigator, confirmExit, when]);
}

export function usePrompt(message: string, when = true) {
  useEffect(() => {
    if (!when) return;
    window.onbeforeunload = function () {
      return message;
    };

    window.history.pushState(null, "", window.location.href);
    window.onpopstate = function () {
      const confirm = window.confirm(message);

      if (confirm) {
        dispatch(setIsEditing(false));
        window.onpopstate = null;
        window.history.back();
      } else {
        window.history.pushState(null, "", window.location.href);
      }
    };
    return () => {
      window.onbeforeunload = null;
      window.onpopstate = null;
      }, [message, when]);

  const confirmExit = useCallback(() => {
    const confirm = window.confirm(message);

    if (confirm) {
      dispatch(setIsEditing(false));
    }

    return confirm;
  }, [message]);
  useConfirmExit(confirmExit, when);
}

Thanks for the suggestion, @kanavi57

This code works when the user clicks directly on the back button, but it won't work if the user long-presses the back button and chooses a specific page to return to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment