Skip to content

Commit

Permalink
Stabilize useBlocker (#7882)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Nov 14, 2023
1 parent 8eb9621 commit cf9b507
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/stabilize-use-blocker.md
Original file line number Diff line number Diff line change
@@ -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.
82 changes: 82 additions & 0 deletions docs/hooks/use-blocker.md
Original file line number Diff line number Diff line change
@@ -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.

<docs-info>
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 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event" target="_blank">`beforeunload`</a> event handler.
</docs-info>

<docs-warning>
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.
</docs-warning>

```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 (
<Form method="post">
<label>
Enter some important data:
<input
name="data"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</label>
<button type="submit">Save</button>

{blocker.state === "blocked" ? (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={() => blocker.proceed()}>
Proceed
</button>
<button onClick={() => blocker.reset()}>
Cancel
</button>
</div>
) : null}
</Form>
);
}
```

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
49 changes: 49 additions & 0 deletions docs/hooks/use-prompt.md
Original file line number Diff line number Diff line change
@@ -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.

<docs-info>
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 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event" target="_blank">`beforeunload`</a> event handler.
</docs-info>

<docs-warning>
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.
</docs-warning>

<docs-warning>
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.
</docs-warning>

```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 (
<Form method="post">
<label>
Enter some important data:
<input
name="data"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</label>
<button type="submit">Save</button>
</Form>
);
}
```

[window-confirm]: https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm
4 changes: 2 additions & 2 deletions integration/form-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand All @@ -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 ({
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export {
useRouteError,
useSearchParams,
useSubmit,
unstable_useBlocker,
useBlocker,
unstable_usePrompt,
unstable_useViewTransitionState,
} from "react-router-dom";
Expand Down
4 changes: 2 additions & 2 deletions packages/remix-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-server-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/remix-testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 21 additions & 21 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit cf9b507

Please sign in to comment.