Skip to content

Commit 4f6c454

Browse files
authored
Stabilize and document useBlocker (#10991)
1 parent e6ac5f0 commit 4f6c454

File tree

11 files changed

+249
-9
lines changed

11 files changed

+249
-9
lines changed

.changeset/stabilize-use-blocker.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": minor
3+
---
4+
5+
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.

.changeset/update-useprompt-args.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": minor
3+
---
4+
5+
Allow `unstable_usePrompt` to accept a `BlockerFunction` in addition to a `boolean`

docs/hooks/use-blocker.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
title: useBlocker
3+
---
4+
5+
# `useBlocker`
6+
7+
<details>
8+
<summary>Type declaration</summary>
9+
10+
```tsx
11+
declare function useBlocker(
12+
shouldBlock: boolean | BlockerFunction
13+
): Blocker;
14+
15+
type BlockerFunction = (args: {
16+
currentLocation: Location;
17+
nextLocation: Location;
18+
historyAction: HistoryAction;
19+
}) => boolean;
20+
21+
type Blocker =
22+
| {
23+
state: "unblocked";
24+
reset: undefined;
25+
proceed: undefined;
26+
location: undefined;
27+
}
28+
| {
29+
state: "blocked";
30+
reset(): void;
31+
proceed(): void;
32+
location: Location;
33+
}
34+
| {
35+
state: "proceeding";
36+
reset: undefined;
37+
proceed: undefined;
38+
location: Location;
39+
};
40+
41+
interface Location<State = any> extends Path {
42+
state: State;
43+
key: string;
44+
}
45+
46+
interface Path {
47+
pathname: string;
48+
search: string;
49+
hash: string;
50+
}
51+
52+
enum HistoryAction {
53+
Pop = "POP",
54+
Push = "PUSH",
55+
Replace = "REPLACE",
56+
}
57+
```
58+
59+
</details>
60+
61+
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.
62+
63+
<docs-info>
64+
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.
65+
</docs-info>
66+
67+
<docs-warning>
68+
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.
69+
</docs-warning>
70+
71+
```tsx
72+
function ImportantForm() {
73+
let [value, setValue] = React.useState("");
74+
75+
// Block navigating elsewhere when data has been entered into the input
76+
let blocker = useBlocker(
77+
({ currentLocation, nextLocation }) =>
78+
value !== "" &&
79+
currentLocation.pathname !== nextLocation.pathname
80+
);
81+
82+
return (
83+
<Form method="post">
84+
<label>
85+
Enter some important data:
86+
<input
87+
name="data"
88+
value={value}
89+
onChange={(e) => setValue(e.target.value)}
90+
/>
91+
</label>
92+
<button type="submit">Save</button>
93+
94+
{blocker.state === "blocked" ? (
95+
<div>
96+
<p>Are you sure you want to leave?</p>
97+
<button onClick={() => blocker.proceed()}>
98+
Proceed
99+
</button>
100+
<button onClick={() => blocker.reset()}>
101+
Cancel
102+
</button>
103+
</div>
104+
) : null}
105+
</Form>
106+
);
107+
}
108+
```
109+
110+
For a more complete example, please refer to the [example][example] in the repository.
111+
112+
## Properties
113+
114+
### `state`
115+
116+
The current state of the blocker
117+
118+
- `unblocked` - the blocker is idle and has not prevented any navigation
119+
- `blocked` - the blocker has prevented a navigation
120+
- `proceeding` - the blocker is proceeding through from a blocked navigation
121+
122+
### `location`
123+
124+
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.
125+
126+
## Methods
127+
128+
### `proceed()`
129+
130+
When in a `blocked` state, you may call `blocker.proceed()` to proceed to the blocked location.
131+
132+
### `reset()`
133+
134+
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.
135+
136+
[example]: https://github.com/remix-run/react-router/tree/main/examples/navigation-blocking

docs/hooks/use-prompt.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
title: unstable_usePrompt
3+
---
4+
5+
# `unstable_usePrompt`
6+
7+
<details>
8+
<summary>Type declaration</summary>
9+
10+
```tsx
11+
declare function unstable_usePrompt({
12+
when,
13+
message,
14+
}: {
15+
when: boolean | BlockerFunction;
16+
message: string;
17+
}) {
18+
19+
type BlockerFunction = (args: {
20+
currentLocation: Location;
21+
nextLocation: Location;
22+
historyAction: HistoryAction;
23+
}) => boolean;
24+
25+
interface Location<State = any> extends Path {
26+
state: State;
27+
key: string;
28+
}
29+
30+
interface Path {
31+
pathname: string;
32+
search: string;
33+
hash: string;
34+
}
35+
36+
enum HistoryAction {
37+
Pop = "POP",
38+
Push = "PUSH",
39+
Replace = "REPLACE",
40+
}
41+
```
42+
43+
</details>
44+
45+
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.
46+
47+
<docs-info>
48+
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.
49+
</docs-info>
50+
51+
<docs-warning>
52+
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.
53+
</docs-warning>
54+
55+
<docs-warning>
56+
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.
57+
</docs-warning>
58+
59+
```tsx
60+
function ImportantForm() {
61+
let [value, setValue] = React.useState("");
62+
63+
// Block navigating elsewhere when data has been entered into the input
64+
unstable_usePrompt({
65+
message: "Are you sure?",
66+
when: ({ currentLocation, nextLocation }) =>
67+
value !== "" &&
68+
currentLocation.pathname !== nextLocation.pathname,
69+
});
70+
71+
return (
72+
<Form method="post">
73+
<label>
74+
Enter some important data:
75+
<input
76+
name="data"
77+
value={value}
78+
onChange={(e) => setValue(e.target.value)}
79+
/>
80+
</label>
81+
<button type="submit">Save</button>
82+
</Form>
83+
);
84+
}
85+
```
86+
87+
[window-confirm]: https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm

examples/navigation-blocking/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ order: 1
66

77
# Navigation Blocking
88

9-
This example demonstrates using `unstable_useBlocker` to prevent navigating away from a page where you might lose user-entered form data. A potentially better UX for this is storing user-entered information in `sessionStorage` and pre-populating the form on return.
9+
This example demonstrates using `useBlocker` to prevent navigating away from a page where you might lose user-entered form data. A potentially better UX for this is storing user-entered information in `sessionStorage` and pre-populating the form on return.
1010

1111
## Preview
1212

examples/navigation-blocking/src/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
Outlet,
1414
Route,
1515
RouterProvider,
16-
unstable_useBlocker as useBlocker,
16+
useBlocker,
1717
useLocation,
1818
} from "react-router-dom";
1919

packages/react-router-dom-v5-compat/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export {
151151
renderMatches,
152152
resolvePath,
153153
unstable_HistoryRouter,
154-
unstable_useBlocker,
154+
useBlocker,
155155
unstable_usePrompt,
156156
useActionData,
157157
useAsyncError,

packages/react-router-dom/__tests__/use-blocker-test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
NavLink,
1010
Outlet,
1111
RouterProvider,
12-
unstable_useBlocker as useBlocker,
12+
useBlocker,
1313
useNavigate,
1414
} from "../index";
1515

packages/react-router-dom/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
useNavigate,
2525
useNavigation,
2626
useResolvedPath,
27-
unstable_useBlocker as useBlocker,
27+
useBlocker,
2828
UNSAFE_DataRouterContext as DataRouterContext,
2929
UNSAFE_DataRouterStateContext as DataRouterStateContext,
3030
UNSAFE_NavigationContext as NavigationContext,
@@ -48,6 +48,7 @@ import type {
4848
V7_FormMethod,
4949
RouterState,
5050
RouterSubscriber,
51+
BlockerFunction,
5152
} from "@remix-run/router";
5253
import {
5354
createRouter,
@@ -168,7 +169,7 @@ export {
168169
useActionData,
169170
useAsyncError,
170171
useAsyncValue,
171-
unstable_useBlocker,
172+
useBlocker,
172173
useHref,
173174
useInRouterContext,
174175
useLoaderData,
@@ -1810,7 +1811,13 @@ function usePageHide(
18101811
* very incorrectly in some cases) across browsers if user click addition
18111812
* back/forward navigations while the confirm is open. Use at your own risk.
18121813
*/
1813-
function usePrompt({ when, message }: { when: boolean; message: string }) {
1814+
function usePrompt({
1815+
when,
1816+
message,
1817+
}: {
1818+
when: boolean | BlockerFunction;
1819+
message: string;
1820+
}) {
18141821
let blocker = useBlocker(when);
18151822

18161823
React.useEffect(() => {

packages/react-router-native/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export {
9696
useActionData,
9797
useAsyncError,
9898
useAsyncValue,
99-
unstable_useBlocker,
99+
useBlocker,
100100
useHref,
101101
useInRouterContext,
102102
useLoaderData,

0 commit comments

Comments
 (0)