I've recently migrated from version 3 to 4.1.2 in the hope that the following bug caused by the setRouteLeaveHook
won't be present in the <Prompt />
component from react-router-dom
.
So, as per the use case of Prompt
component, when the user standing in the form refreshes the page, edit some fields and clicks back button, the confirmation alert pops up and when the user cancels the navigation, it stays on the page but the url gets changed and does not remain the same as of the current form page.
Can you post a more concrete example? I'm not seeing this behavior myself. Are you using a HashRouter, by chance?
I'm experiencing the exact same thing using BrowserRouter
. If the user clicks a link or does an operation that pushes a route to history everything works as expected. If however the user clicks the browser back button I get a prompt as expected but the URL in the address bar has changed to the previous path.
If I then click cancel on the prompt indicating I do not wish to leave, one of two things will happen.
In both cases my current component is still shown and my components lifecycle hooks includingcomponentWillReceiveProps
are called which messes up the state of my component.
i have the same question. have you resolved?
@timdorr just FYI, I'm using the BrowserRouter
and my issue is exactly the same as @arvinsingla described "In both cases my current component is still shown and my components lifecycle hooks includingcomponentWillReceiveProps are called which messes up the state of my component."
If any of you are diving into this, PRs are always appreciated for this sort of thing.
At the very least, someone should provide a minimal reproduction repo. It is a lot easier for other people to look into this issue if they have code they can take a look at. Just describing a problem means that anyone who wants to help has to first build their own project before they can attempt to debug, which isn't exactly motivating.
If there actually is an issue with the code here (I'd have to see it happen. I can't think of a reason why what is being described is happening unless there are browser related issues), then it is most likely going to be an issue with history
, not react-router
.
Also, to make things easier, you can use our codesandbox project as a starting point: https://codesandbox.io/s/n55VljYk7
CodeSandbox unfortunately won't work here. Their fake browser implementation doesn't work correctly with the <Prompt>
. I mentioned it to one of the devs, so they are at least somewhat aware of that issue, but I would consider that bug pretty low priority. That wouldn't be the same issue that is happening here, but it might give the illusion of it.
Ah, I thought they passed that through to the browser. My bad.
It has some browser integration, but I wish that it didn't. I have some embedded sandboxes in the curi docs website and its annoying that the back button is hijacked. I much prefer Ryan's fake browser that uses an in memory history.
Back to this issue, I was trying to think of why componentWillReceiveProps
is being triggered, and there might be an issue there if the navigation was done using the browser's forward/back button. Those are caught by event listeners, and history attempts to reverse the event (counter go(-1)
with go(1)
). history
also will notify any listeners when this happens, but maybe it should be doing nothing when that happens?
I'm running into this this issue as well. I've created a a test case: https://codesandbox.io/s/4lvjoow5mw. I get the same behavior on a real browser.
@alexandersoto I loaded your example as a full page (https://4lvjoow5mw.codesandbox.io/) and it worked as expected (although you seem to need to load that page directly, there is some weird behavior if you open it from the editor page).
@pshrmn thanks for checking it out so quickly. The test case appears to work as a full page, but I found another related issue. Click on Broken -> go back -> go back a second time. The dialog box disappears and the url changes to /
, but the router is rendering /broken
.
Going back a second time cancels the dialog, but doesn't update the url like clicking cancel does. I'm testing this out on Chrome.
Hmm, that second case is interesting. The behavior is different between Chrome and Firefox (Firefox will go to the page two pages back).
// given the history
const history = [
'google.com',
'sandbox.com',
'sandbox.com/block'
];
let index = 2;
In Firefox, clicking the back button once will open up the prompt. Clicking it a second time (while the prompt is open) will go to 'google.com'. In Chrome, as you described above, the first click is the same, but the second click will go to 'sandbox.com'. The Firefox behavior is what I would expect to happen.
I also forked your sandbox (https://codesandbox.io/s/l248ml7kj7) to add a third route so I could see what the behavior is when I can go back twice within the same site. When leaving the site entirely, that still works, but in both Chrome and Firefox it has weird behavior because it stacks prompts.
This double prompt issue is actually with the history package, so it would probably be best to work on it over there.
As for the original issue from here, we still need a reproducible test case, so if anyone wants to put one together, it would be appreciated.
I have a same issue with Prompt. Sometimes after clicking browser back button the hash url is returned back, if I say "do not redirect me", sometimes no.. But in both cases the browser is displaying the hash of previous page/state first. My issue is exactly what is described here https://4lvjoow5mw.codesandbox.io/
Same issue here as @arvinsingla described - I'm experiencing the exact same two scenarios, and they both seem to occur quite randomly.
I made a gif of the issue I'm experiencing, which is rather strange behavior: https://gfycat.com/HotNeatInganue
Repro steps:
/
/broken
The path change between steps 3 and 4 makes React think you've already navigated away before you even get a chance to select "Cancel" or "Ok" on the dialog box, so React unmounts the "Broken" component and then re-renders the "Broken" component after you click on "Cancel" which is when the path switches back. So if you're using <Prompt />
on a page with a form, then you just lost all the data saved in the form component's state due to the unmount.
jk, there was another issue that was causing the unmount
Is there any hack for this to be solve in the mean time?
I experienced the same issue.
When I come from an in app route, the behaviour of Prompt is working as expected, but when I access the page directly with the url of the view containing the Prompt component and I trigger it, the browser url remains modified even if I click on cancel.
I made a repo to reproduce the issue : https://github.com/Edistra/react-router-dom-prompt-issue
I also made a gif of the issue :
I seem to be observing similar behavior without the <Prompt>
component, but instead while trying to implement something like https://github.com/ReactTraining/react-router/issues/4635#issuecomment-300465164 (a custom styled popup in my app to handle the back button). The URL bar string changes before my custom popup renders, but the page acts as expected (my popup shows and the app doesn't re-route). The difference is it always seems to change & stay changed after using the browser back button, rather than the reported cases here where the URL flips back after the dialog is closed.
I have the same problem
Look this animation.
Below this part of my code
import React, {Component} from 'react';
import { Prompt } from 'react-router-dom';
...
class AgricultorEdit extends Component {
...
render() {
return (
<div>
<Prompt
when={!!props.change}
message={location => 'Voc锚 alterou o cadastro e n茫o salvou, tem certeza que deseja sair desta tela?' } />
....
</div>
)
}
Any updates on this issue @timdorr ? I'm experiencing the same problem, using React Router v4, React Router Redux 5-alpha, and using a pretty similar solution to this one to show a custom modal.
Apparently the issue happens when entering the route via POP action (i.e. by directly accessing/reloading), but not if you come via PUSH (navigating from another route in the app).
Could this be related to the history
api instead (https://github.com/ReactTraining/history#blocking-transitions)?
Edit: reference to an old issue in history
: https://github.com/ReactTraining/history/issues/367
Any update on this issue ? I'm facing the same problem
Any update on this issue ? I'm facing the same problem too
This bug is over a year old and one of the most upvoted issues here. It would be very nice to get an update on the status of this issue or some hack to fix this. Thanks!
In case this is helpful to anyone, I ended up writing my own version of the Prompt component which doesn't have this bug and also prompts for leaving the page by closing the tab. Written in TypeScript and working with history/createBrowserHistory
from [email protected]
, [email protected]
and [email protected]
.
import * as React from "react"
import {withRouter} from "react-router"
// Explanation: This component is meant to prevent accidental page transitions
// which could cause unsaved work to be lost. When this component is mounted
// and the `when` prop is true, it does two things:
//
// 1. Ask for confirmation before allowing page transitions from back/forward
// browser navigation and react-router Link navigation. This uses the
// react-router provided history.
// 2. Ask for confirmation before allowing "leave page" browser navigation
// (close/refresh). This uses the window's beforeunload event.
//
// The first one is what the react-router Prompt component is supposed to do.
// However, it is buggy. See here:
// https://github.com/ReactTraining/react-router/issues/5405
const DEFAULT_MESSAGE =
"You have unsaved changes, are you sure you want to leave?"
interface Props {
when: boolean
message?: string
history?: any
}
class NavigationLock extends React.PureComponent<Props> {
public unblock?: () => void
public componentDidMount() {
if (this.props.when) {
this.startBlocking()
}
}
public componentWillUnmount() {
this.stopBlocking()
}
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.when && !this.props.when) {
this.startBlocking()
} else if (!nextProps.when && this.props.when) {
this.stopBlocking()
}
}
public render() {
return null
}
private onBeforeUnload = (event: any) => {
// Prompts the user before closing the page, see:
// https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
event.preventDefault()
event.returnValue = ""
}
private startBlocking = () => {
if (!this.unblock) {
const message = this.props.message || DEFAULT_MESSAGE
this.unblock = this.props.history.block(message)
}
window.addEventListener("beforeunload", this.onBeforeUnload)
}
private stopBlocking = () => {
if (this.unblock) {
this.unblock()
this.unblock = undefined
}
window.removeEventListener("beforeunload", this.onBeforeUnload)
}
}
export default withRouter(NavigationLock)
Thanks for this awesome library. Any update on this from official mentainer.
@davidxmoody Above code does not work when hitting back button. The route url changes and reverts back on cancel and the whole component is reloaded loosing the state, but the direct browser refresh works.
Same problem on react-router-dom "^4.3.1" I am using.. I use Prompt to alert user when leaving the current page wherein data/inputs provided are not yet submitted. Is there any fix this time? Thanks.
The same with "react-router-dom": "^5.0.0"
and without hashes, and without history
.
Tried to create example via codesandbox - works well.
Case #1:
1) Go to edit page
2) Change value of the input (local state of component, not Redux)
3) Click logo - alert displayed and works as expected
4) Click back button and cancel button - local state become empty (value of input) and URL does not change.
Case #2:
1) On the Edit Page and click refresh
2) Edit input
3) Click logo - works as expected
4) Click back button and cancel button - URL changed to a previous path but current page/component displayed.
MainApp.js
import React from 'react';
import { Provider } from 'react-redux';
import App from './containers';
import configureStore from './store';
const store = configureStore();
const MainApp = () => (
<Provider store={store}>
<App />
</Provider>
);
export default MainApp;
Routes
import React, { lazy, Suspense } from 'react';
import { Switch, BrowserRouter } from 'react-router-dom';
import { PrivateRoute, PublicRoute } from '../components/routes';
const SignInPage = lazy(() => import('./SignIn'));
const Page404 = lazy(() => import('../components/Error404'));
const Homepage = lazy(() => import('./Homepage'));
const Profile = lazy(() => import('./Profile'));
const App = () => (
<Suspense fallback={null}>
<BrowserRouter>
<Switch>
<PrivateRoute path="/" exact component={Homepage} />
<PrivateRoute path="/profile" component={Profile} />
<PublicRoute path="/login" component={SignInPage} />
<PublicRoute component={Page404} />
</Switch>
</BrowserRouter>
</Suspense>
);
export default App;
Ohai
I don't know whether this helps anyone out, but I had the same issue: On back button I was prompted but the url indeed changed and sometimes it messed up the page sometimes it did not. (react-router/ -dom version 5.0.0)
After some debugging I realized that I have been using components from 'react-router' and 'react-router-dom' completely mixed, sometimes I import them from one sometimes the other. Now this should not matter much, but apparently in this case it does.
After I have ditched 'react-router' in favor of importing everything from 'react-router-dom' it works as 'expected' (Such as on browser back button it : 1. change the url, but does not unload the component, 2. shows the prompt window, 3. if I choose cancel the url is written back and the component is not unmounted or messed up).
I'm also using import {Router, Route} from 'react-router-dom';
with exported history import {createBrowserHistory} from "history";
export default createBrowserHistory();
I am having this very same issue, i have Formik implemented with react and the browser back button when clicked does throw the alert.. but upon clicking cancel button the state of the form is lost...
hence, doesnt make any sense if you prompt user for unsaved changes and just clean the form when they click cancel . :/
Im surprised to see that this issue is open since 2017 , have tried all the upper mentioned tricks.. none of them work for me..
Are there any updates on this issue?
Is it possible to cancel the location change if the user clicks cancel? I too am experiencing the bug where the user cancels and the prompt does not show up a 2nd time and they lose form data even if they clicked cancel.
@sibelius Thank you I will try this. What is useRouter
referring to in that gist?
import { useContext } from 'react';
import { __RouterContext, RouteComponentProps } from 'react-router-dom';
export const useRouter = <TParams = {}>() => {
return useContext(__RouterContext) as RouteComponentProps<TParams>;
};
@sibelius What is the double underscore in front of RouterContext? Is that a TypeScript thing? When I try to use it, it tells me there's no RouterContext in react-router-dom.
@shantp This is a naming convention we use in React Router to make clear, that it is not public api and it could change anytime without announcing it.
Thanks @MeiKatz, also it is only in React Router 5 and I was using 4 so I had to upgrade to access it.
I've done a little more debugging and hopefully can shed some light on the unexpected behavior, though I still don't have a solution. Testing the default Prompt
from react-router-dom
, I'm seeing the same behavior in both Firefox 69.0.1 and Chrome 77.0.3865.90.
Let's say I navigate from page A -> B -> C, where only page C has a Prompt
on it.
When I click the browser's back button, the URL changes to B and the prompt comes up. If I cancel, the URL changes back to C and data is retained. If I instead click OK, the URL stays at B and data changes accordingly.
Here's where the issues begin:
Now, after navigating to page C, if I _refresh the page_, the browser still has pages A and B in its history. If I click the browser's back button, the URL changes to B and I get the prompt, as expected. If I click OK, the URL stays at B and the data changes, also as expected. However, if I click cancel, the URL _still_ stays at B, and data _does not change_ to reflect the new URL.
FWIW the same also applies to the browser's Forward button.
Basically I think the key to finding unexpected behavior is when react-router didn't control the initial route navigation (e.g. when the URL was manually entered, the page was refreshed, or possibly from an a
tag rather than a Link
).
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
You can add the fresh
label to prevent me from taking any action.
This is not yet fixed, can someone tag it fresh please. Thanks!
It's weird that this has been open open for 2 years now, but with no actual solution, on an actively maintained repo. Is there something preventing us from fixing it ?
Someone just needs to make a PR.
This issue is still present with react-router 5.1.2.
Reading through the thread, I can't tell if the issue is with react router or the history package.
What is the best work-around so far, if any?
This is a browser related issue, not a issue in react-router or react itself. Not even related to JS.
Since what we are trying to achieve is to handle a browser event (back button click), during this the state of page is lost as the history array is popped then pushed again.
You can actually see this by implementing a window listener for 'onBeforeUnload'.. u will see it changing in the console. But notice only for back button not window close button. Which is strange.
I also went a little deeper to see how WordPress handles this situation, on WordPress u can also see a kind of jerk or delay in re-fetch of data when u click back button, it seems like WordPress developers have gone out of the way to maintain a seperate trail of changes of the form area in the browser's local storage.
So, its better to inform chrome, Firefox, etc about this so that they can patch it.
Thank you @MskShahrukh .
If that can be of help to anyone, I have the following hack for mobx-react-router to work around this behaviour:
(MobxRouterStore.prototype as any)._updateLocation = function (location: Location<any>) {
// Here be dragons...
// Stop the same location from being pushed twice
// when clicking the browser's back button and using a Prompt
if (location.key && location.key === this.location?.key) {
return;
}
this.location = location;
}
This is the only way I've found to prompt user every time he tries to exit and also keep the URL (it change to previous URL but if user press "cancel" it'll change again to current url)
/* [...] */
const [callAccepted, setCallAccepted] = useState(false);
/* I omitted the function where I set callAccepted to true to keep it short */
const check = (location, action) => {
return callAccepted
? "Are you sure you want to exit?"
: true;
};
return (
<>
<Prompt when={callAccepted} message={check} />
<div>
{/* rest of component here... */}
</div>
</>
)
If I remove when
prop from <Prompt>
or even if I remove params from check
function, it'll show the prompt only one time without keeping the URL.
This is the only way I've found to prompt user every time he tries to exit and also keep the URL (it change to previous URL but if user press "cancel" it'll change again to current url)
/* [...] */ const [callAccepted, setCallAccepted] = useState(false); /* I omitted the function where I set callAccepted to true to keep it short */ const check = (location, action) => { return callAccepted ? "Are you sure you want to exit?" : true; }; return ( <> <Prompt when={callAccepted} message={check} /> <div> {/* rest of component here... */} </div> </> )
If I remove
when
prop from<Prompt>
or even if I remove params fromcheck
function, it'll show the prompt only one time without keeping the URL.
@joaquinwojcik I have tried it and it does prevent me from a redirect whenever the condition is true. However the url is still updated to the previous one.
After a bit of tweaking, I have replaced the window history for now with the current path and it doesn't change to the previous url.
`<Prompt when={true} message={(location, action) => {
window.history.replaceState(null, null, "/your/current/path")
return true
? "Are you sure you want to exit?"
: true;
}} />`
This fixes it for me now. It also has a support for majority of the browsers. People reading this may check the browser support as per the project's user base.
This fixes it for me now. It also has a support for majority of the browsers. People reading this may check the browser support as per the project's user base.
@SufiyaanRajput which browser are you using? I've tested it on Chrome ^84
@joaquinwojcik Version 84.0.4147.125 (Official Build) (64-bit) chrome
I've been wrestling with the same issue, and finally found a solution that works for my use case. As others have pointed out, when a POP
action triggers the getUserConfirmation
method and the navigation is canceled, React Router correctly prevents the route transition within the React app, but the browser has already popped that item from window.history
, so the URL in the browser will have changed.
My solution was to update my getUserConfirmation
handler to detect a POP
action and call window.history.forward()
to return to the original URL within the browser history. But in order for getUserConfirmation
to know what action was performed, I needed to pass along that information in the message from the <Prompt />
component. The message is just a string, so I'm using a serialized JSON string to pass along the action along with my message and some other structured content that I need for my customized confirmation prompt.
As a bit of context, I'm using the HashRouter
because my use case is within a browser extension. I can't verify whether the same issue happens or whether my solution is adequate for the BrowserRouter
. I also don't know if this addresses all of the scenarios discussed in this thread, but I wanted to provide a reduced example of my solution in hopes that it can help someone out:
// In the component that renders the <Prompt />:
<Prompt
message={(_nextLocation, action) => {
return JSON.stringify({
action,
message: 'Some message'
})
}}
/>
// In the component that renders the router:
<HashRouter
getUserConfirmation={(payload, callback) => {
const { action, message } = JSON.parse(payload);
const confirmed = window.confirm(message)
callback(confirmed)
if (!confirmed && action === 'POP') window.history.forward()
}}
>
...
</HashRouter />
@wosephjeber Your solution worked wonders for me! Well I have built my own component that uses this prompt so my code was a bit different:
const onMessage = useCallback((location: any, action: any) => {
if (action === 'POP') {
window.history.forward();
}
const card = document.getElementById('dirty_save_revert');
card?.classList.add('shake');
return false;
}, []);
This one just prevents the back automatically, (or other routes) and shakes a "Save / Cancel" popup. Works wonders!
It's the same component that Discord uses: When you change some settings on a server a pop-up shows, and won't let you close the tab/nav out until you revert your changes or save. It's quite a nice UX imo.
@Andrew1431 Thanks for your suggestion. Hope this solution help some developers who are struggling with the same issue.
The Problem we faced is explained neatly by @redbmk's Comment.
The issue is that the prompt is allowing the URL to change even after clicking on cancel navigation. This happens only after you reload on a page that has prompt.
We ended up using the following approach according to our prompt configuration.
const currentPath = useRef(null);
useEffect(() => {
if (!!currentPath) {
currentPath.current = window.location.pathname;
}
}, []);
const handleBlockedNavigation = (nextLocation, action) => {
if (formIsDirty) //A boolean state to check if the page has any unsaved changes
{
//A workaround for Prompt replacing the URL after page reload, even if navigation is cancelled
if (currentPath?.current !== nextLocation.pathname && action === 'POP') {
window.history.forward();
}
return false;
}
return true;
};
<Prompt when={blockNavigation} message={handleBlockedNavigation} />
blockNavigation --- is the boolean state to trigger the Prompt.
handleBlockedNavigation --- will check for the unsaved changes on the page and return either true or false. If it returns true then the navigation is allowed, if false then the navigation is blocked.
currentPath --- Ref holds the current URL and would be checked before forwarding the URL by _window.history.forward();_
You may find blockNavigation and formIsDirty are doing same work (or think they may be redundant), but it is according to our page requirements, we are checking another boolean on the page to make sure the user is safe to navigate or not (Omitted other conditions around it for brevity).
@wosephjeber Your solution worked wonders for me! Well I have built my own component that uses this prompt so my code was a bit different:
const onMessage = useCallback((location: any, action: any) => { if (action === 'POP') { window.history.forward(); } const card = document.getElementById('dirty_save_revert'); card?.classList.add('shake'); return false; }, []);
This one just prevents the back automatically, (or other routes) and shakes a "Save / Cancel" popup. Works wonders!
It's the same component that Discord uses: When you change some settings on a server a pop-up shows, and won't let you close the tab/nav out until you revert your changes or save. It's quite a nice UX imo.
Hi, where does you put this function? is it passed on message props? and what does this card mean? Is it refer to your component? Thanks!
Most helpful comment
I'm experiencing the exact same thing using
BrowserRouter
. If the user clicks a link or does an operation that pushes a route to history everything works as expected. If however the user clicks the browser back button I get a prompt as expected but the URL in the address bar has changed to the previous path.If I then click cancel on the prompt indicating I do not wish to leave, one of two things will happen.
In both cases my current component is still shown and my components lifecycle hooks including
componentWillReceiveProps
are called which messes up the state of my component.