Have you read the Contributing Guidelines on issues?
Prerequisites
Description
The history.block() API fails unpredictably when used on a Docusaurus website. The reason is that this API is also used by some system components, but the API doesn't allow more than one callback to be registered simultaneously.
Why this matters: We need history.block() to prevent accidental navigation in situations such as unsaved changes. Otherwise the user might accidentally click on the navigation header and lose all their work. (It's the same problem usually solved by window.addEventListener('beforeunload', ...), but for the case of in-page navigation.)
What we could do above it: Docusaurus should provide a wrapper API to address this requirement, but does not seem to.
Steps to reproduce
Below is a simple repro. It's attempting to use the history.block() API from @docusaurus/router to prevent navigation:
import {useEffect} from 'react';
import { useHistory, useLocation } from '@docusaurus/router';
function HistoryTest() {
const history = useHistory();
useEffect(() => {
console.log("+++ history.block()");
const unblock = history.block((location, action) => {
console.log("+++ callback was called");
// Prevent navigation to other pages
return false;
});
return () => {
unblock();
};
}, [history]);
return <>REPRO</>;
}
Expected behavior
When the user clicks a navigation hyperlink, the callback should be invoked, giving the component a chance to reject the action by returning false.
Actual behavior
It occasionally works. But quite often the callback will NOT get called. The console logs show +++ history.block() but not +++ callback was called.
And this warning appears sometimes in the console:
Warning: A history supports only one prompt at a time
This warning is telling us that the history.block() API does not allow more than one callback to be registered simultaneously. Some debugging revealed that another callback is being registered inside useHistoryPopHandler() in this component:
|
function useContextValue(): ContextValue { |
|
const disabled = useIsNavbarMobileSidebarDisabled(); |
|
const windowSize = useWindowSize(); |
|
|
|
const shouldRender = !disabled && windowSize === 'mobile'; |
|
|
|
const [shown, setShown] = useState(false); |
|
|
|
// Close mobile sidebar on navigation pop |
|
// Most likely firing when using the Android back button (but not only) |
|
useHistoryPopHandler(() => { |
This conflict happens when setting up the NavbarMobileSidebarProvider context. The useHistoryPopHandler() conflict occurs even if we are not on a mobile site. Even if we have no use for NavbarMobileSidebar.
Proposed solution
Docusaurus reexports useHistory() from @docusaurus/router, implying that it is supported for use by the website. But given how React components get combined together from the theme and plugins, it seems unrealistic to expect components to somehow globally coordinate their access to history.block().
Instead, maybe we can provide an API that wraps history.block() in a way that allows multiple components to handle the event. Thinking about the design of this callback, I don't see any semantic problem with each component getting a turn to reject the navigation event. Once it is rejected, we can simply skip calling the remaining event handlers. (Actually, I don't understand why history.block() imposed this restriction in the first place, unless it was intended to be a low level facility to be wrapped a higher level system such as I am proposing.)
Your environment
- Docusaurus version used: 3.5.1 ... 3.7.0 and https://new.docusaurus.io/
- Environment name and version (e.g. Chrome 89, Node.js 16.4): Firefox 136, Node 20.9.0
- Operating system and version (e.g. Ubuntu 20.04.2 LTS): Linux/Windows
Self-service
Have you read the Contributing Guidelines on issues?
Prerequisites
npm run clearoryarn clearcommand.rm -rf node_modules yarn.lock package-lock.jsonand re-installing packages.Description
The
history.block()API fails unpredictably when used on a Docusaurus website. The reason is that this API is also used by some system components, but the API doesn't allow more than one callback to be registered simultaneously.Why this matters: We need
history.block()to prevent accidental navigation in situations such as unsaved changes. Otherwise the user might accidentally click on the navigation header and lose all their work. (It's the same problem usually solved bywindow.addEventListener('beforeunload', ...), but for the case of in-page navigation.)What we could do above it: Docusaurus should provide a wrapper API to address this requirement, but does not seem to.
Steps to reproduce
Below is a simple repro. It's attempting to use the
history.block()API from@docusaurus/routerto prevent navigation:Expected behavior
When the user clicks a navigation hyperlink, the callback should be invoked, giving the component a chance to reject the action by returning
false.Actual behavior
It occasionally works. But quite often the callback will NOT get called. The console logs show
+++ history.block()but not+++ callback was called.And this warning appears sometimes in the console:
This warning is telling us that the
history.block()API does not allow more than one callback to be registered simultaneously. Some debugging revealed that another callback is being registered insideuseHistoryPopHandler()in this component:docusaurus/packages/docusaurus-theme-common/src/contexts/navbarMobileSidebar.tsx
Lines 48 to 58 in fd51384
This conflict happens when setting up the
NavbarMobileSidebarProvidercontext. TheuseHistoryPopHandler()conflict occurs even if we are not on a mobile site. Even if we have no use forNavbarMobileSidebar.Proposed solution
Docusaurus reexports
useHistory()from@docusaurus/router, implying that it is supported for use by the website. But given how React components get combined together from the theme and plugins, it seems unrealistic to expect components to somehow globally coordinate their access tohistory.block().Instead, maybe we can provide an API that wraps
history.block()in a way that allows multiple components to handle the event. Thinking about the design of this callback, I don't see any semantic problem with each component getting a turn to reject the navigation event. Once it is rejected, we can simply skip calling the remaining event handlers. (Actually, I don't understand whyhistory.block()imposed this restriction in the first place, unless it was intended to be a low level facility to be wrapped a higher level system such as I am proposing.)Your environment
Self-service