Custom React Router Prompt: A history supports only one prompt at a time

Viewed 2460

I have created a custom React Router Prompt to show when the user wants to navigate to other page and have not saved the data yet.

Everything seems to be working as it should, but I have an annoying warning:

Warning: A history supports only one prompt at a time

The component looks like this:

export function RouterPrompt(props) {
    const {when, onOK, onCancel, title} = props;

    const history = useHistory();

    const [showPrompt, setShowPrompt] = useState(false);
    const [currentPath, setCurrentPath] = useState("");

    useEffect(() => {
        if (when) {
            history.block((prompt) => {
                setCurrentPath(prompt.pathname);
                setShowPrompt(true);
                return "true";
            });
        } else {
            history.block(() => {
            });
        }
    }, [when]);

    const handleOK = useCallback(async () => {
        if (onOK) {
            const canRoute = await Promise.resolve(onOK());
            if (canRoute) {
                history.block(() => {
                });
                history.push(currentPath);
            }
        }
    }, [currentPath, history, onOK]);

    const handleCancel = useCallback(async () => {
        if (onCancel) {
            const canRoute = await Promise.resolve(onCancel());
            if (canRoute) {
                history.block(() => {
                });
                history.push(currentPath);
            }
        }
        setShowPrompt(false);
    }, [currentPath, history, onCancel]);

    return showPrompt ? (
        <Dialog
            open={showPrompt}
            onClose={handleCancel}
            aria-labelledby="alert-dialog-title"
            aria-describedby="alert-dialog-description"
        >
            <DialogTitle id="alert-dialog-title">
                {title}
            </DialogTitle>
            <DialogContent>
                <DialogContentText id="alert-dialog-description">
                    There are unsaved changes. Are you sure want to leave this page?
                </DialogContentText>
            </DialogContent>
            <DialogActions>
                <Button onClick={handleCancel}>Stay on page</Button>
                <Button onClick={handleOK} autoFocus>
                    Leave
                </Button>
            </DialogActions>
        </Dialog>
    ) : null;
}

As far as I was able to do some research on this, this comes from the class lifecycle methods (useEffect runs before unmouting the current history.block), but in reality only one Prompt is rendered in the DOM.

Edit

I have discovered another bug, that this will change the path regardless of the actual push or not. So in the URL the path changes but the view don't, so next time when you press back and Confirm, instaed of staying on the page, you will be 2 steps back in history.

1 Answers

I have completely restructured/refactored my code (original code was from a blog post), this seems to be working perfectly (without warnings) and as expected:

export function RouterPrompt(props) {
    const {when, title} = props;

    const history = useHistory();

    const [showPrompt, setShowPrompt] = useState(false);
    const [currentPath, setCurrentPath] = useState("");

    const unblockRef = useRef();

    function handleShowModal() {
        setShowPrompt(true);
    }

    function onCancel() {
        setShowPrompt(false);
    }

    useEffect(() => {
        unblockRef.current = history.block((location) => {
            if (when) {
                setCurrentPath(location.pathname);
                handleShowModal();
                return false;
            }
            return true;
        });
        return () => {
            unblockRef.current && unblockRef.current();
        };
    }, [when]);

    function handleConfirm() {
        if (unblockRef) {
            unblockRef.current();
        }
        setShowPrompt(false);
        history.push(currentPath);
    }

    return (
        <Dialog
            open={showPrompt}
            onClose={onCancel}
            aria-labelledby="alert-dialog-title"
            aria-describedby="alert-dialog-description"
        >
            <DialogTitle id="alert-dialog-title">
                {title}
            </DialogTitle>
            <DialogContent>
                <DialogContentText id="alert-dialog-description">
                    There are unsaved changes. Are you sure want to leave this page?
                </DialogContentText>
            </DialogContent>
            <DialogActions>
                <Button onClick={onCancel}>Stay on page</Button>
                <Button onClick={handleConfirm} autoFocus>
                    Leave
                </Button>
            </DialogActions>
        </Dialog>
    );
}
Related