Skip to content

Commit

Permalink
Stabilize and document useBlocker (#10991)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Nov 3, 2023
1 parent e6ac5f0 commit 4f6c454
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 9 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 @@
---
"react-router": 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.
5 changes: 5 additions & 0 deletions .changeset/update-useprompt-args.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": minor
---

Allow `unstable_usePrompt` to accept a `BlockerFunction` in addition to a `boolean`
136 changes: 136 additions & 0 deletions docs/hooks/use-blocker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: useBlocker
---

# `useBlocker`

<details>
<summary>Type declaration</summary>

```tsx
declare function useBlocker(
shouldBlock: boolean | BlockerFunction
): Blocker;

type BlockerFunction = (args: {
currentLocation: Location;
nextLocation: Location;
historyAction: HistoryAction;
}) => boolean;

type Blocker =
| {
state: "unblocked";
reset: undefined;
proceed: undefined;
location: undefined;
}
| {
state: "blocked";
reset(): void;
proceed(): void;
location: Location;
}
| {
state: "proceeding";
reset: undefined;
proceed: undefined;
location: Location;
};

interface Location<State = any> extends Path {
state: State;
key: string;
}

interface Path {
pathname: string;
search: string;
hash: string;
}

enum HistoryAction {
Pop = "POP",
Push = "PUSH",
Replace = "REPLACE",
}
```

</details>

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() {
let [value, setValue] = React.useState("");

// Block navigating elsewhere when data has been entered into the input
let 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
87 changes: 87 additions & 0 deletions docs/hooks/use-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: unstable_usePrompt
---

# `unstable_usePrompt`

<details>
<summary>Type declaration</summary>

```tsx
declare function unstable_usePrompt({
when,
message,
}: {
when: boolean | BlockerFunction;
message: string;
}) {

type BlockerFunction = (args: {
currentLocation: Location;
nextLocation: Location;
historyAction: HistoryAction;
}) => boolean;

interface Location<State = any> extends Path {
state: State;
key: string;
}

interface Path {
pathname: string;
search: string;
hash: string;
}

enum HistoryAction {
Pop = "POP",
Push = "PUSH",
Replace = "REPLACE",
}
```
</details>
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() {
let [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
2 changes: 1 addition & 1 deletion examples/navigation-blocking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ order: 1

# Navigation Blocking

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.
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.

## Preview

Expand Down
2 changes: 1 addition & 1 deletion examples/navigation-blocking/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
Outlet,
Route,
RouterProvider,
unstable_useBlocker as useBlocker,
useBlocker,
useLocation,
} from "react-router-dom";

Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-dom-v5-compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export {
renderMatches,
resolvePath,
unstable_HistoryRouter,
unstable_useBlocker,
useBlocker,
unstable_usePrompt,
useActionData,
useAsyncError,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-dom/__tests__/use-blocker-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
NavLink,
Outlet,
RouterProvider,
unstable_useBlocker as useBlocker,
useBlocker,
useNavigate,
} from "../index";

Expand Down
13 changes: 10 additions & 3 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
useNavigate,
useNavigation,
useResolvedPath,
unstable_useBlocker as useBlocker,
useBlocker,
UNSAFE_DataRouterContext as DataRouterContext,
UNSAFE_DataRouterStateContext as DataRouterStateContext,
UNSAFE_NavigationContext as NavigationContext,
Expand All @@ -48,6 +48,7 @@ import type {
V7_FormMethod,
RouterState,
RouterSubscriber,
BlockerFunction,
} from "@remix-run/router";
import {
createRouter,
Expand Down Expand Up @@ -168,7 +169,7 @@ export {
useActionData,
useAsyncError,
useAsyncValue,
unstable_useBlocker,
useBlocker,
useHref,
useInRouterContext,
useLoaderData,
Expand Down Expand Up @@ -1810,7 +1811,13 @@ function usePageHide(
* very incorrectly in some cases) across browsers if user click addition
* back/forward navigations while the confirm is open. Use at your own risk.
*/
function usePrompt({ when, message }: { when: boolean; message: string }) {
function usePrompt({
when,
message,
}: {
when: boolean | BlockerFunction;
message: string;
}) {
let blocker = useBlocker(when);

React.useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-native/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export {
useActionData,
useAsyncError,
useAsyncValue,
unstable_useBlocker,
useBlocker,
useHref,
useInRouterContext,
useLoaderData,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export {
redirectDocument,
renderMatches,
resolvePath,
useBlocker as unstable_useBlocker,
useBlocker,
useActionData,
useAsyncError,
useAsyncValue,
Expand Down

0 comments on commit 4f6c454

Please sign in to comment.