Closed callmeberzerker closed 1 year ago
I tried to find a workaround with useEffect
with the location as a dependencies, but couldn't get it working. With many applications we are stuck on 6.beta.1
because of this and #8035.
Is there any guidance on how to implement the same behavior? I didn't find any extension point within the current router implementation. We can't continue with v6 without the possibility to block navigation based on some condition 😢
Removing usePrompt
and useBlocker
in the 7th iteration of the beta was really unexpected.
It would be great to get an alternative way to do the same. Is there any way we can include the removed code in our own codebase? Thanks
I also experienced this and just found out it happened very unexpected in beta7. +1 for bringing it back or at least outline some workaround.
As react-router expose NavigationContext
under UNSAFE_NavigationContext
we can add useBlocker hook back using the same code.
Don't exactly know why "block"
method was omitted from History type? @mjackson maybe we could add it back?
If you really need blocking I'd recommend you remain on v5 until we have time to get it back into v6. v5 will continue to be supported for the foreseeable future. You shouldn't feel any pressure to upgrade to v6 immediately.
As for why it was removed in v6, we decided we'd rather ship with what we have than take even more time to nail down a feature that isn't fully baked. We will absolutely be working on adding this back in to v6 at some point in the near future, but not for our first stable release of 6.x.
I might add (for those of you who want to upgrade immediately to v6) that you could consider another type of user experience instead of blocking navigation away from the current page.
The canonical use case for blocking is so you can show the user a prompt to confirm navigation away from a page with unsaved data. Instead of blocking navigation, another valid way to handle this situation could be to just save the state of the form to local/session storage in the browser as the form values change. Then, when the user returns, repopulate the default values of all the form fields with the ones you saved from the last visit. This experience should be less jarring than throwing up a prompt in front of the user while still giving you the resilience you're looking for.
Again, we are planning on re-adding this functionality to v6 after our stable release. I only offer this suggestion as a possible alternative for those of you who may wish to upgrade immediately.
As for why it was removed in v6, we decided we'd rather ship with what we have than take even more time to nail down a feature that isn't fully baked.
Yes, that's completely understandable. Looking forward for v6 release, great work 👏
My question was about this change, with omitting 'block' from Navigator
@piecyk You can always create your own type to add that method back. I simply removed all the methods from the History
interface that aren't being used anywhere in the router.
A version with breaking changes notice would've been nice prior to a major release. Right now, the project recommends to upgrade, yet the migration guide is partial and application critical features have been removed without notice.
(After updating to v6, I am currently debugging a few errors with react-router-dom
: "TypeError: pathname.match is not a function"
, and "TypeError: meta.relativePath.startsWith is not a function"
. The migration is anything but smooth. Compared to this, @material-ui
, now @mui
, made an excellent job at facilitating the upgrade process, starting a few months ago.)
I also ran into this omission while trying to migrate from v5 to v6. My migration was going so well! I love the changes that come in v6. They are super intuitive. Really great job!!! Thank you! I will wait patiently for useBlocker
or another recommended way of blocking history in v6 before doing our migration.
I will also attempt to convince my company's product owners to use this pattern when possible. Thanks for the suggestion!
We need a way to cancel a long running process if the page is navigated away from in an electron app. There are more use cases than form data or unsaved changes.
As react-router expose
NavigationContext
underUNSAFE_NavigationContext
we can add useBlocker hook back using the same code.
Thanks @piecyk !
I made a gist of this for react-router-dom for easier access: https://gist.github.com/rmorse/426ffcc579922a82749934826fa9f743
Nowhere is the removal of this mentioned anywhere in the v5 to v6 upgrade docs. I just burned an hour trying to figure out why I couldn't resolve Prompt until I stumbled upon this issue.
A note has been added in #8436
@storybook/router
uses v6 already.
this dependency introduce react-router
native .d.ts
instead of @types/react-router-dom
. this changes will occur tsc compile error.
so, If we can update to v6. I'm very glad 😉
@storybook/router
uses v6 already. this dependency introducereact-router
native.d.ts
instead of@types/react-router-dom
. this changes will occur tsc compile error. so, If we can update to v6. I'm very glad 😉
@storybook/router
6.2.9 uses @reach/router
.
The following (and probably some newer versions) is working with React Router v5:
"@storybook/addon-actions": "6.2.9",
"@storybook/addon-docs": "6.2.9",
"@storybook/addons": "6.2.9",
"@storybook/react": "6.2.9",
@mjackson Is there a roadmap for when this will be prioritized and worked on? Got 97% of the way with my upgrade, and the remaining bits are comprised of <Prompt />
and optional route parameters.
Cant upgrade from beta until this is back :/
@wojtekmaj Yes, if you use ts-expect-error, this is true.
I think the notice about Prompt and useBlocker should be at the top of the documentation page. That is a critical blocker for me, but I only discovered it after updating >90% of my codebase to v6. I'll stash the changes and resume the upgrade after this feature is in (or there is a workaround posted) and cross my fingers that the merge gods will smile on me later.
@karagog have you seen this previous answer https://github.com/remix-run/react-router/issues/8139#issuecomment-977790637? For me it is working fine.
@karagog have you seen this previous answer #8139 (comment)? For me it is working fine.
Thanks, but IIUC that will only bring to life the beta-version of useBlocker(), which it sounds like it wasn't ready for primetime so not too hot about using that in my project. Also that code may "work" for now but will be an added maintenance burden I'd rather leave up to the react-router folks to release when it's ready.
I think taking time and doing it the right way is generally a good approach, but I think they should call it out more prominently in the upgrade guide because it's a critical blocker for some folks. I have no problem waiting, but the upgrade doc promises a relatively smooth upgrade without mentioning this major caveat until the very end.
We have also been blocked by the missing
In our case I had to extend usePrompt
of your nice implementation, @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-977790637 to support that "message" might be a function and not a string and gets the target location submitted as in the v5 behaviour:
export function usePrompt(message, when = true) {
const { basename } = useContext(NavigationContext);
const blocker = useCallback(
(tx) => {
if (typeof message === "function") {
let targetLocation = tx?.location?.pathname;
if (targetLocation.startsWith(basename)) {
targetLocation = targetLocation.substring(basename.length);
}
if (message(targetLocation)) {
tx.retry();
}
} else if (typeof message === "string") {
if (window.confirm(message)) {
tx.retry();
}
}
},
[message, basename]
);
Unfortunately our previous usage relied on this behaviour.
A prompt component could be easily implemented like this
import { usePrompt } from "./reactRouterDomPromptBlocker";
const Prompt = ({ message, when }) => {
usePrompt(message, when);
return null;
};
export default Prompt;
No warranties for my code examples, especially if they work in future … ;)
I also crashed into this... I will attempt to use this solution temporarily, since we are using <Prompt />
only in one of our internal libraries so I could build it temporarily... But I would love to not rely on unsupported code. Is there a timeline for having this added back in? Here we are on v6.2.1 and it is still not officially supported yet
We have also been blocked by the missing component and corresponding hooks in multiple web projects during the v6 migration. It would be great, if these could be reintroduced again in future!
In our case I had to extend
usePrompt
of your nice implementation, @piecyk #8139 (comment) to support that "message" might be a function and not a string and gets the target location submitted as in the v5 behaviour:export function usePrompt(message, when = true) { const { basename } = useContext(NavigationContext); const blocker = useCallback( (tx) => { if (typeof message === "function") { let targetLocation = tx?.location?.pathname; if (targetLocation.startsWith(basename)) { targetLocation = targetLocation.substring(basename.length); } if (message(targetLocation)) { tx.retry(); } } else if (typeof message === "string") { if (window.confirm(message)) { tx.retry(); } } }, [message, basename] );
Unfortunately our previous usage relied on this behaviour.
A prompt component could be easily implemented like this
import { usePrompt } from "./reactRouterDomPromptBlocker"; const Prompt = ({ message, when }) => { usePrompt(message, when); return null; }; export default Prompt;
No warranties for my code examples, especially if they work in future … ;)
In order to preserve the behavior from RR5 (where the prompt callback takes the location
and action
, I modified the above usePrompt
code as follows:
/**
* Prompts the user with an Alert before they leave the current screen.
*
* @param message
* @param when
*/
export function usePrompt(message, when = true) {
const blocker = useCallback(
(tx) => {
let response;
if (typeof message === 'function') {
response = message(tx?.location, tx?.action);
if (typeof response === 'string') {
response = window.confirm(response);
}
} else if (typeof message === 'string') {
response = window.confirm(message);
}
if (response) {
tx.retry();
}
},
[message]
);
return useBlocker(blocker, when);
}
@mjackson Is there a roadmap for when this will be prioritized and worked on? Got 97% of the way with my upgrade, and the remaining bits are comprised of
<Prompt />
and optional route parameters.
I tried starting a discussion to get some feedback on where this might be on a roadmap, even if it is just a priority order. Having a little more clarity on what is priority would be a massive help.
Thanks for your help @code-jongleur and @heath-freenome
This is my temporary solution for useBlocker
and usePrompt
in TypeScript:
import type {Blocker, History, Transition} from 'history';
import {ContextType, useCallback, useContext, useEffect} from 'react';
import {Navigator as BaseNavigator, UNSAFE_NavigationContext as NavigationContext} from 'react-router-dom';
interface Navigator extends BaseNavigator {
block: History['block'];
}
type NavigationContextWithBlock = ContextType<typeof NavigationContext> & { navigator: Navigator };
/**
* @source https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874
*/
export function useBlocker(blocker: Blocker, when = true) {
const {navigator} = useContext(NavigationContext) as NavigationContextWithBlock;
useEffect(() => {
if (!when) {
return;
}
const unblock = navigator.block((tx: Transition) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
/**
* @source https://github.com/remix-run/react-router/issues/8139#issuecomment-1021457943
*/
export function usePrompt(message: string | ((
location: Transition['location'],
action: Transition['action'],
) => string), when = true) {
const blocker = useCallback((tx: Transition) => {
let response;
if (typeof message === 'function') {
response = message(tx.location, tx.action);
if (typeof response === 'string') {
response = window.confirm(response);
}
} else {
response = window.confirm(message);
}
if (response) {
tx.retry();
}
}, [message]);
return useBlocker(blocker, when);
}
I tried the above code from @thejahweh and the navigation was blocked but the URL is changing and the confirmation is not being shown. I am using the HashRouter, is there any chance that is causing issues?
See https://github.com/TanStack/react-location/discussions/214#discussioncomment-2156933 - react-location might be an alternative for those looking to upgrade to RR6
I think the notice about Prompt and useBlocker should be at the top of the documentation page. That is a critical blocker for me, but I only discovered it after updating >90% of my codebase to v6. I'll stash the changes and resume the upgrade after this feature is in (or there is a workaround posted) and cross my fingers that the merge gods will smile on me later.
Agree. I also upgraded 90+% of my code without knowing critical features were missing. Partly my bad for trusting without reading the whole doc first but still, it would be nice if a notice could be put at the top of the page.
You should add a specific warning message at the top of the migration guide. It's really frustrating to discover Prompt, usePrompt and useBlocker are not part of the v6 when 90% of the app has been migrated to v6.
@woodreamz agreed regarding a note on the migration docs. Someone could probably open a PR to add that.
And again... it would be helpful if any maintainer at all could give some more clarity on the roadmap so we have an idea what the priorities are, and if anyone could pitch in to get these features working.
Any updates?
Obviously,it's a breaking change , continue focus on
@aliechti ... Thanks for your help @code-jongleur and @heath-freenome
This is my temporary solution for
useBlocker
andusePrompt
in TypeScript: [...]
thanks to @aliechti, @code-jongleur and @heath-freenome for their enlightening samples.
bottom line up front, my unblock() call wasn't working
in case helps others, i added using a ref to avoid the navigator.block() being called for every re-render...
if you look at the code for history.block() it pushes your blocker callback to a stack and returns a function for unblock() which removes from that stack... re-renders would add another instance of my block function to that stack and the last unblock my scope wound up with wasn't going to remove all of them because it's a value comparison.
i believe the useCallback() in previous code samples above by @code-jongleur and @heath-freenome are probably the equivalent solve.
here's my full working tsx file:
// react-router-dom v6 removed useBlocker() due to edge case downsides
// https://github.com/remix-run/react-router/releases/tag/v6.0.0-beta.7
// more deets about why: https://github.com/remix-run/history/issues/690
// this restores that feature since we don't care about the edge cases
import type { Blocker, History, Transition } from 'history';
import { ContextType, useContext, useEffect, useRef } from 'react';
import { Navigator as BaseNavigator, UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
interface Navigator extends BaseNavigator {
block: History['block'];
}
type NavigationContextWithBlock = ContextType<typeof NavigationContext> & { navigator: Navigator };
// rmorse gist: https://gist.github.com/rmorse/426ffcc579922a82749934826fa9f743
// lower level history library example, helpful for seeing use of confirmation flow inside useBlocker:https://github.com/remix-run/history/blob/main/docs/blocking-transitions.md
// approach below copied and tweaked from github issue:
// https://github.com/remix-run/react-router/issues/8139#issuecomment-1023105785
export function useBlocker(blocker: Blocker, when = true) {
const { navigator } = useContext(NavigationContext) as NavigationContextWithBlock;
//main tweak required to OP was wrapping unblock in ref so we're only pushing one blocker on the stack for this when expression (i.e. not for every render)
const refUnBlock = useRef<()=>void>();
useEffect(() => {
if (!when) {
refUnBlock.current?.();
return;
}
if (!refUnBlock.current) refUnBlock.current = navigator.block((tx: Transition) => {
const autoUnblockingTx = {
...tx,
retry() {
refUnBlock.current?.(); //need to unblock so retry succeeds
tx.retry();
},
};
blocker(autoUnblockingTx);
});
}, [navigator, blocker, when]);
}
My example based on https://github.com/remix-run/react-router/issues/8139#issuecomment-1023105785 and https://github.com/remix-run/react-router/issues/8139#issuecomment-1094106064 with custom prompt (based on Ant Design Modal component)
import {Modal, Button} from 'antd'
import {useBlocker, Tx} from '@/hooks'
type NavBlockerProps = {
when?: boolean
onSave?: () => Promise<any> | undefined
}
export const NavBlocker: FC<NavBlockerProps> = ({when, onSave}) => {
const [t] = useTranslation(undefined, {keyPrefix: 'NavBlocker'})
const [block, setBlock] = useState(when)
const [visible, setVisible] = useState(false)
const [tx, setTx] = useState<Tx | undefined>()
const [saving, setSaving] = useState(false)
const show = () => setVisible(true)
const hide = () => setVisible(false)
const skip = () => {
setBlock(false)
setTimeout(() => {
tx?.retry()
hide()
}, 0)
}
const save = async () => {
setSaving(true)
try {
await onSave?.()
skip()
} catch (e) {
console.error(e)
}
setSaving(false)
}
useEffect(() => {
setBlock(when)
}, [when])
useBlocker((tx) => {
setTx(tx)
show()
}, block)
return (
<Modal
visible={visible}
width={580}
onCancel={hide}
footer={[
<Button type="primary" ghost key="save" onClick={save} loading={saving}>
{t('save')}
</Button>,
<Button type="primary" ghost danger key="skip" onClick={skip}>
{t('skip')}
</Button>,
<Button onClick={hide} key="cancel">
{t('cancel')}
</Button>,
]}
>
{t('message')}
</Modal>
)
}
import type {Transition} from 'history'
export type Tx = Transition & {retry: () => void}
Has anyone been able to get this workaround working for a HashRouter
? My navigation is blocked but I see the URL changing but my Modal is not showing.
Any updates on this?
I have made a demonstration on using usePrompt and useBlocker, based on the code from aliechti.
This demonstration is also specifically for react-iframe form, for anyone who just want to watch the usage only, see usePromptBlocker.ts and Demo.tsx
Made a MUI example based on the code from aliechti 😁
Is there a timeline when to approximately expect these features to be added back into v6? A rough idea would be very helpful, so we don't implement an alternative to find out it is added back in next week.
Is there a timeline when to approximately expect these features to be added back into v6? A rough idea would be very helpful, so we don't implement an alternative to find out it is added back in next week.
Sounds like it will be a bit since they're focusing on getting the "remixified" react-router out first. Our team just went with a modified version of what Myzonj made until Prompt gets integrated back in. It was a pretty quick integration.
This is my temporary solution for
useBlocker
andusePrompt
in TypeScript:
I used the below wrapper component in combination with the hooks from @aliechti to supplement the missing
const Prompt = function ({message, when}) {
usePrompt(message, when);
return false;
}
please bring it back...
above few guys implemented code has significant bug which irrecoverable by refresh page.
For anyone trying to use the above workaround in the 6.4 prerelease: it won't work because @remix-run/router
has its own history
implementation and it doesn't include block
.
https://github.com/remix-run/react-router/blob/8bae986/packages/router/history.ts#L587-L614
For anyone trying to use the above workaround in the 6.4 prerelease: it won't work because
@remix-run/router
has its ownhistory
implementation and it doesn't includeblock
.https://github.com/remix-run/react-router/blob/8bae986/packages/router/history.ts#L587-L614
So they are making it WORSE?! :(
Why not just adding useUnstablePrompt
or useUnstableBlock
or something like that to the library? It was already working fine for everyone.
It is quite a shame that version 6 has come this long way without being feature complete with the previous version! So much time has passed.
As for why it was removed in v6, we decided we'd rather ship with what we have than take even more time to nail down a feature that isn't fully baked.
Let this be a lesson for anyone who thinks "we can do it later."
Awesome, I just spend half an hour upgrading to v6 just to realize at the very end that Prompt
isn't supported...
Any ETA when <Prompt />
will finally come back?
I think in general most people won't be able to upgrade to v6 since in the latest beta
usePrompt
anduseBlocker
are removed.Most apps rely on the
usePrompt
orPrompt
to prevent navigation in case the user has unsaved changes (aka theform
is dirty).With this issue maybe we can have some feedback on why they (
usePrompt
,Prompt
) were removed (they worked fine in the previous beta version v6.0.0-beta.6) and what makes them problematic, what's the outlook for getting them back in the router and potentially userland solutions to this problem.