diff --git a/.changeset/stabilize-use-blocker.md b/.changeset/stabilize-use-blocker.md
new file mode 100644
index 00000000000..e13e70ba9e4
--- /dev/null
+++ b/.changeset/stabilize-use-blocker.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/react": minor
+---
+
+Remove the `unstable_` prefix from the [`useBlocker`](https://reactrouter.com/en/main/hooks/use-blocker) hook as it's been in use for enough time that we are confident in the API. We do not plan to remove the prefix from `unstable_usePrompt` due to differences in how browsers handle `window.confirm` that prevent React Router from guaranteeing consistent/correct behavior.
diff --git a/docs/hooks/use-blocker.md b/docs/hooks/use-blocker.md
new file mode 100644
index 00000000000..c1f18bf807a
--- /dev/null
+++ b/docs/hooks/use-blocker.md
@@ -0,0 +1,82 @@
+---
+title: useBlocker
+---
+
+# `useBlocker`
+
+The `useBlocker` hook allows you to prevent the user from navigating away from the current location, and present them with a custom UI to allow them to confirm the navigation.
+
+
+This only works for client-side navigations within your React Router application and will not block document requests. To prevent document navigations you will need to add your own `beforeunload` event handler.
+
+
+
+Blocking a user from navigating is a bit of an anti-pattern, so please carefully consider any usage of this hook and use it sparingly. In the de-facto use case of preventing a user navigating away from a half-filled form, you might consider persisting unsaved state to `sessionStorage` and automatically re-filling it if they return instead of blocking them from navigating away.
+
+
+```tsx
+function ImportantForm() {
+ const [value, setValue] = React.useState("");
+
+ // Block navigating elsewhere when data has been entered into the input
+ const blocker = useBlocker(
+ ({ currentLocation, nextLocation }) =>
+ value !== "" &&
+ currentLocation.pathname !== nextLocation.pathname
+ );
+
+ return (
+
+ );
+}
+```
+
+For a more complete example, please refer to the [example][example] in the repository.
+
+## Properties
+
+### `state`
+
+The current state of the blocker
+
+- `unblocked` - the blocker is idle and has not prevented any navigation
+- `blocked` - the blocker has prevented a navigation
+- `proceeding` - the blocker is proceeding through from a blocked navigation
+
+### `location`
+
+When in a `blocked` state, this represents the location to which we blocked a navigation. When in a `proceeding` state, this is the location being navigated to after a `blocker.proceed()` call.
+
+## Methods
+
+### `proceed()`
+
+When in a `blocked` state, you may call `blocker.proceed()` to proceed to the blocked location.
+
+### `reset()`
+
+When in a `blocked` state, you may call `blocker.reset()` to return the blocker back to an `unblocked` state and leave the user at the current location.
+
+[example]: https://github.com/remix-run/react-router/tree/main/examples/navigation-blocking
diff --git a/docs/hooks/use-prompt.md b/docs/hooks/use-prompt.md
new file mode 100644
index 00000000000..47b1a3a3f29
--- /dev/null
+++ b/docs/hooks/use-prompt.md
@@ -0,0 +1,49 @@
+---
+title: unstable_usePrompt
+---
+
+# `unstable_usePrompt`
+
+The `unstable_usePrompt` hook allows you to prompt the user for confirmation via [`window.confirm`][window-confirm] prior to navigating away from the current location.
+
+
+This only works for client-side navigations within your React Router application and will not block document requests. To prevent document navigations you will need to add your own `beforeunload` event handler.
+
+
+
+Blocking a user from navigating is a bit of an anti-pattern, so please carefully consider any usage of this hook and use it sparingly. In the de-facto use case of preventing a user navigating away from a half-filled form, you might consider persisting unsaved state to `sessionStorage` and automatically re-filling it if they return instead of blocking them from navigating away.
+
+
+
+We do not plan to remove the `unstable_` prefix from this hook because the behavior is non-deterministic across browsers when the prompt is open, so React Router cannot guarantee correct behavior in all scenarios. To avoid this non-determinism, we recommend using `useBlocker` instead which also gives you control over the confirmation UX.
+
+
+```tsx
+function ImportantForm() {
+ const [value, setValue] = React.useState("");
+
+ // Block navigating elsewhere when data has been entered into the input
+ unstable_usePrompt({
+ message: "Are you sure?",
+ when: ({ currentLocation, nextLocation }) =>
+ value !== "" &&
+ currentLocation.pathname !== nextLocation.pathname,
+ });
+
+ return (
+
+ );
+}
+```
+
+[window-confirm]: https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm
diff --git a/integration/form-test.ts b/integration/form-test.ts
index f2331ad2987..b2c5ead7a2b 100644
--- a/integration/form-test.ts
+++ b/integration/form-test.ts
@@ -921,7 +921,7 @@ test.describe("Forms", () => {
await app.goto("/projects/blarg");
let html = await app.getHtml();
let el = getElement(html, `#${SPLAT_ROUTE_NO_ACTION}`);
- expect(el.attr("action")).toMatch("/projects");
+ expect(el.attr("action")).toMatch("/projects/blarg");
});
test("no action resolves to URL including search params", async ({
@@ -931,7 +931,7 @@ test.describe("Forms", () => {
await app.goto("/projects/blarg?foo=bar");
let html = await app.getHtml();
let el = getElement(html, `#${SPLAT_ROUTE_NO_ACTION}`);
- expect(el.attr("action")).toMatch("/projects?foo=bar");
+ expect(el.attr("action")).toMatch("/projects/blarg?foo=bar");
});
test("absolute action resolves relative to the root route", async ({
diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json
index b0e13f3e1b0..340ef14cf7b 100644
--- a/packages/remix-dev/package.json
+++ b/packages/remix-dev/package.json
@@ -29,7 +29,7 @@
"@mdx-js/mdx": "^2.3.0",
"@npmcli/package-json": "^4.0.1",
"@remix-run/node": "2.2.0",
- "@remix-run/router": "1.11.0",
+ "@remix-run/router": "1.12.0-pre.0",
"@remix-run/server-runtime": "2.2.0",
"@types/mdx": "^2.0.5",
"@vanilla-extract/integration": "^6.2.0",
diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx
index a5ac3ef8cae..10531970fe9 100644
--- a/packages/remix-react/index.tsx
+++ b/packages/remix-react/index.tsx
@@ -48,7 +48,7 @@ export {
useRouteError,
useSearchParams,
useSubmit,
- unstable_useBlocker,
+ useBlocker,
unstable_usePrompt,
unstable_useViewTransitionState,
} from "react-router-dom";
diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json
index cdaa1d6be7d..e74f9014341 100644
--- a/packages/remix-react/package.json
+++ b/packages/remix-react/package.json
@@ -16,9 +16,9 @@
"typings": "dist/index.d.ts",
"module": "dist/esm/index.js",
"dependencies": {
- "@remix-run/router": "1.11.0",
+ "@remix-run/router": "1.12.0-pre.0",
"@remix-run/server-runtime": "2.2.0",
- "react-router-dom": "6.18.0"
+ "react-router-dom": "6.19.0-pre.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index 99709a09431..981100608cf 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -16,7 +16,7 @@
"typings": "dist/index.d.ts",
"module": "dist/esm/index.js",
"dependencies": {
- "@remix-run/router": "1.11.0",
+ "@remix-run/router": "1.12.0-pre.0",
"@types/cookie": "^0.5.3",
"@web3-storage/multipart-parser": "^1.0.0",
"cookie": "^0.5.0",
diff --git a/packages/remix-testing/package.json b/packages/remix-testing/package.json
index 0cb369be94a..7dbec765e78 100644
--- a/packages/remix-testing/package.json
+++ b/packages/remix-testing/package.json
@@ -18,8 +18,8 @@
"dependencies": {
"@remix-run/node": "2.2.0",
"@remix-run/react": "2.2.0",
- "@remix-run/router": "1.11.0",
- "react-router-dom": "6.18.0"
+ "@remix-run/router": "1.12.0-pre.0",
+ "react-router-dom": "6.19.0-pre.0"
},
"devDependencies": {
"@types/node": "^18.17.1",
diff --git a/yarn.lock b/yarn.lock
index 54ce28dcc5c..79e0d85149b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2319,10 +2319,10 @@
"@changesets/types" "^5.0.0"
dotenv "^8.1.0"
-"@remix-run/router@1.11.0":
- version "1.11.0"
- resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz#e0e45ac3fff9d8a126916f166809825537e9f955"
- integrity sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==
+"@remix-run/router@1.12.0-pre.0":
+ version "1.12.0-pre.0"
+ resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.12.0-pre.0.tgz#a7a3ed1941016f574147a97d64d3c687bf343113"
+ integrity sha512-+bBn9KqD2AC0pttSGydVFOZSsT0NqQ1+rGFwMTx9dRANk6oGxrPbKTDxLLikocscGzSL5przvcK4Uxfq8yU7BQ==
"@remix-run/web-blob@^3.1.0":
version "3.1.0"
@@ -3417,9 +3417,9 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.0.0, acorn@^8.8.0, acorn@^8.8.1:
- version "8.11.0"
- resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.0.tgz#04306e13732231c995ac4363f331ee09db278d82"
- integrity sha512-hNiSyky+cuYVALBrsjB7f9gMN9P4u09JyAiMNMLaVfsmkDJuH84M1T/0pfDX/OJfGWcobd2A7ecXYzygn8wibA==
+ version "8.11.2"
+ resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
+ integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
agent-base@6:
version "6.0.2"
@@ -10918,9 +10918,9 @@ pumpify@^1.3.3:
pump "^2.0.0"
punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0:
- version "2.3.0"
- resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
- integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
pure-rand@^6.0.0:
version "6.0.2"
@@ -11028,20 +11028,20 @@ react-refresh@^0.14.0:
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz"
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
-react-router-dom@6.18.0:
- version "6.18.0"
- resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz#0a50c167209d6e7bd2ed9de200a6579ea4fb1dca"
- integrity sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==
+react-router-dom@6.19.0-pre.0:
+ version "6.19.0-pre.0"
+ resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.19.0-pre.0.tgz#99d561cf50babea4c8c65317950884bb1db76395"
+ integrity sha512-elYzIXUmv92//Stn0IK8EYQixv1zeuUWw/HZM9LTQ4aGdNeBTqpTEq/K66KJbmfklzgVpg29Nr515onohZA1dA==
dependencies:
- "@remix-run/router" "1.11.0"
- react-router "6.18.0"
+ "@remix-run/router" "1.12.0-pre.0"
+ react-router "6.19.0-pre.0"
-react-router@6.18.0:
- version "6.18.0"
- resolved "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz#32e2bedc318e095a48763b5ed7758e54034cd36a"
- integrity sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==
+react-router@6.19.0-pre.0:
+ version "6.19.0-pre.0"
+ resolved "https://registry.npmjs.org/react-router/-/react-router-6.19.0-pre.0.tgz#bebd659375430700b894e6007311bd1ff4bd84c5"
+ integrity sha512-qyPRUTh6jnZHu9t0UtdLdcVeKCWVmJbIZo0YgZb4ETxygqe/43WAPHsJ8TjA6/b6A+SeYhDZqeVu/U/lpBdUsA==
dependencies:
- "@remix-run/router" "1.11.0"
+ "@remix-run/router" "1.12.0-pre.0"
react@^18.2.0:
version "18.2.0"