Last active
August 5, 2024 18:44
-
-
Save chaance/2f3c14ec2351a175024f62fd6ba64aa6 to your computer and use it in GitHub Desktop.
Example implementation of `usePrompt` and React Router v5's `<Prompt>` with `unstable_useBlocker`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ------------------------------------------------------------------------------ | |
* IMPORTANT UPDATE: | |
* This is *not* a complete implementation and I do not suggest using it!. This | |
* was primarily an experiment to determine whether or not a decent blocking | |
* hook could be implemented in userland, and the answer we came up with is NO. | |
* | |
* Luckily we added `usePrompt` (behind an `unstable_` flag) back to React Router | |
* a few versions ago! It's not documented [and I'm no longer on the team, so I | |
* probably won't try to do anything about that], but you can see it in source. | |
* https://github.com/remix-run/react-router/blob/react-router-dom%406.15.0/packages/react-router-dom/index.tsx#L1460 | |
* | |
* Also, it's unstable because browser APIs are not quite consistent or robust | |
* enough for reliable navigation blocking, so it's pretty much impossible to | |
* implement this hook without some weird behavior. This is why it wasn't | |
* originally added to v6, but the team determined that it was important to | |
* provide the best implementation we could for users who needed better | |
* compatibility with v5 and a smooth upgrade path. | |
* | |
* For more context: | |
* https://github.com/remix-run/react-router/issues/8139#issuecomment-1396078490 | |
* | |
* Example usage: | |
* | |
* ```ts | |
* import { unstable_usePrompt as usePrompt } from "react-router-dom"; | |
* | |
* usePrompt({ | |
* when: formIsDirty, | |
* message: "You have unsaved changes. Are you sure you want to leave?", | |
* }); | |
* ``` | |
* ------------------------------------------------------------------------------ | |
*/ | |
import * as React from "react"; | |
import { | |
useBeforeUnload, | |
unstable_useBlocker as useBlocker, | |
} from "react-router-dom"; | |
// You can abstract `useBlocker` to use the browser's `window.confirm` dialog to | |
// determine whether or not the user should navigate within the current origin. | |
// `useBlocker` can also be used in conjunction with `useBeforeUnload` to | |
// prevent navigation away from the current origin. | |
// | |
// IMPORTANT: There are edge cases with this behavior in which React Router | |
// cannot reliably access the correct location in the history stack. In such | |
// cases the user may attempt to stay on the page but the app navigates anyway, | |
// or the app may stay on the correct page but the browser's history stack gets | |
// out of whack. You should test your own implementation thoroughly to make sure | |
// the tradeoffs are right for your users. | |
function usePrompt(message, { beforeUnload } = {}) { | |
let blocker = useBlocker( | |
React.useCallback( | |
() => (typeof message === "string" ? !window.confirm(message) : false), | |
[message] | |
) | |
); | |
let prevState = React.useRef(blocker.state); | |
React.useEffect(() => { | |
if (blocker.state === "blocked") { | |
blocker.reset(); | |
} | |
prevState.current = blocker.state; | |
}, [blocker]); | |
useBeforeUnload( | |
React.useCallback( | |
(event) => { | |
if (beforeUnload && typeof message === "string") { | |
event.preventDefault(); | |
event.returnValue = message; | |
} | |
}, | |
[message, beforeUnload] | |
), | |
{ capture: true } | |
); | |
} | |
// You can also reimplement the v5 <Prompt> component API | |
function Prompt({ when, message, ...props }) { | |
usePrompt(when ? message : false, props); | |
return null; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from "react"; | |
import { | |
useBeforeUnload, | |
unstable_useBlocker as useBlocker, | |
} from "react-router-dom"; | |
// You can abstract `useBlocker` to use the browser's `window.confirm` dialog to | |
// determine whether or not the user should navigate within the current origin. | |
// `useBlocker` can also be used in conjunction with `useBeforeUnload` to | |
// prevent navigation away from the current origin. | |
// | |
// IMPORTANT: There are edge cases with this behavior in which React Router | |
// cannot reliably access the correct location in the history stack. In such | |
// cases the user may attempt to stay on the page but the app navigates anyway, | |
// or the app may stay on the correct page but the browser's history stack gets | |
// out of whack. You should test your own implementation thoroughly to make sure | |
// the tradeoffs are right for your users. | |
function usePrompt( | |
message: string | null | undefined | false, | |
{ beforeUnload }: { beforeUnload?: boolean } = {} | |
) { | |
let blocker = useBlocker( | |
React.useCallback( | |
() => (typeof message === "string" ? !window.confirm(message) : false), | |
[message] | |
) | |
); | |
let prevState = React.useRef(blocker.state); | |
React.useEffect(() => { | |
if (blocker.state === "blocked") { | |
blocker.reset(); | |
} | |
prevState.current = blocker.state; | |
}, [blocker]); | |
useBeforeUnload( | |
React.useCallback( | |
(event) => { | |
if (beforeUnload && typeof message === "string") { | |
event.preventDefault(); | |
event.returnValue = message; | |
} | |
}, | |
[message, beforeUnload] | |
), | |
{ capture: true } | |
); | |
} | |
// You can also reimplement the v5 <Prompt> component API | |
function Prompt({ when, message, ...props }: PromptProps) { | |
usePrompt(when ? message : false, props); | |
return null; | |
} | |
interface PromptProps { | |
when: boolean; | |
message: string; | |
beforeUnload?: boolean; | |
} |
Yeah, I have all the same questions.
@pauldraper @abemedia Just FYI, this is not a complete implementation and you probably shouldn't use it. I added some comments to the Gist to provide context, and an example for how you can use the built-in usePrompt
hook in React Router v6.7.0 and up.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks @chaance!
What's the purpose of storing
blocker.state
inprevState
? It doesn't appear to ever get used.Also, I tested it without the
useEffect
which callsblocker.reset()
and it seemed to work as expected. What is it needed for?