Skip to content

Commit 0db3f44

Browse files
committed
docs: added navigation blocking
1 parent 16ef1d9 commit 0db3f44

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed

docs/how-to/navigation-blocking.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
---
2+
title: Navigation Blocking
3+
---
4+
5+
# Navigation Blocking
6+
7+
[MODES: framework, data]
8+
9+
When users are in the middle of a workflow, like filling out an important form, you may want to prevent them from navigating away from the page.
10+
11+
This example will show:
12+
13+
- Connecting a form to a route action with [`useFetcher`](../api/hooks/useFetcher)
14+
- Blocking navigation when the form is dirty
15+
- Showing a confirmation dialog when the user tries to leave the page
16+
17+
## 1. Set up a route with a form
18+
19+
Add a route with the form, we'll use a "contact" route for this example:
20+
21+
```ts filename=routes.ts
22+
import {
23+
type RouteConfig,
24+
index,
25+
route,
26+
} from "@react-router/dev/routes";
27+
28+
export default [
29+
index("routes/home.tsx"),
30+
route("contact", "routes/contact.tsx"),
31+
] satisfies RouteConfig;
32+
```
33+
34+
Add the form to the contact route module:
35+
36+
```tsx filename=routes/contact.tsx
37+
import { useFetcher } from "react-router";
38+
import type { Route } from "./+types/contact";
39+
40+
export async function action({
41+
request,
42+
}: Route.ActionArgs) {
43+
let formData = await request.formData();
44+
let email = formData.get("email");
45+
let message = formData.get("message");
46+
console.log(email, message);
47+
return { ok: true };
48+
}
49+
50+
export default function Contact() {
51+
let fetcher = useFetcher();
52+
53+
return (
54+
<fetcher.Form method="post">
55+
<p>
56+
<label>
57+
Email: <input name="email" type="email" />
58+
</label>
59+
</p>
60+
<p>
61+
<textarea name="message" />
62+
</p>
63+
<p>
64+
<button type="submit">
65+
{fetcher.state === "idle" ? "Send" : "Sending..."}
66+
</button>
67+
</p>
68+
</fetcher.Form>
69+
);
70+
}
71+
```
72+
73+
## 2. Add dirty state and onChange handler
74+
75+
To track the dirty state of the form, we'll use a single boolean and a quick form onChange handler. You may want to track the dirty state differently but this works for this guide.
76+
77+
```tsx filename=routes/contact.tsx lines=[2,8-12]
78+
export default function Contact() {
79+
let [isDirty, setIsDirty] = useState(false);
80+
let fetcher = useFetcher();
81+
82+
return (
83+
<fetcher.Form
84+
method="post"
85+
onChange={(event) => {
86+
let email = event.currentTarget.email.value;
87+
let message = event.currentTarget.message.value;
88+
setIsDirty(Boolean(email || message));
89+
}}
90+
>
91+
{/* existing code */}
92+
</fetcher.Form>
93+
);
94+
}
95+
```
96+
97+
## 3. Block navigation when the form is dirty
98+
99+
```tsx filename=routes/contact.tsx lines=[1,6-8]
100+
import { useBlocker } from "react-router";
101+
102+
export default function Contact() {
103+
let [isDirty, setIsDirty] = useState(false);
104+
let fetcher = useFetcher();
105+
let blocker = useBlocker(
106+
useCallback(() => isDirty, [isDirty])
107+
);
108+
109+
// ... existing code
110+
}
111+
```
112+
113+
While this will now block a navigation, there's no way for the user to confirm it.
114+
115+
## 4. Show confirmation UI
116+
117+
This uses a simple div, but you may want to use a modal dialog.
118+
119+
```tsx filename=routes/contact.tsx lines=[19-41]
120+
export default function Contact() {
121+
let [isDirty, setIsDirty] = useState(false);
122+
let fetcher = useFetcher();
123+
let blocker = useBlocker(
124+
useCallback(() => isDirty, [isDirty])
125+
);
126+
127+
return (
128+
<fetcher.Form
129+
method="post"
130+
onChange={(event) => {
131+
let email = event.currentTarget.email.value;
132+
let message = event.currentTarget.message.value;
133+
setIsDirty(Boolean(email || message));
134+
}}
135+
>
136+
{/* existing code */}
137+
138+
{blocker.state === "blocked" && (
139+
<div>
140+
<p>Wait! You didn't send the message yet:</p>
141+
<p>
142+
<button
143+
type="button"
144+
onClick={() => blocker.proceed()}
145+
>
146+
Leave
147+
</button>{" "}
148+
<button
149+
type="button"
150+
onClick={() => blocker.reset()}
151+
>
152+
Stay here
153+
</button>
154+
</p>
155+
</div>
156+
)}
157+
</fetcher.Form>
158+
);
159+
}
160+
```
161+
162+
If the user clicks "leave" then `blocker.proceed()` will proceed with the navigation. If they click "stay here" then `blocker.reset()` will clear the blocker and keep them on the current page.
163+
164+
## 5. Reset the blocker when the action resolves
165+
166+
If the user doesn't click either "leave" or "stay here", then then submits the form, the blocker will still be active. Let's reset the blocker when the action resolves with an effect.
167+
168+
```tsx filename=routes/contact.tsx
169+
useEffect(() => {
170+
if (fetcher.data?.ok) {
171+
if (blocker.state === "blocked") {
172+
blocker.reset();
173+
}
174+
}
175+
}, [fetcher.data]);
176+
```
177+
178+
## 6. Clear the form when the action resolves
179+
180+
While unrelated to navigation blocking, let's clear the form when the action resolves with a ref.
181+
182+
```tsx
183+
let formRef = useRef<HTMLFormElement>(null);
184+
185+
// put it on the form
186+
<fetcher.Form
187+
ref={formRef}
188+
method="post"
189+
onChange={(event) => {
190+
// ... existing code
191+
}}
192+
>
193+
{/* existing code */}
194+
</fetcher.Form>;
195+
```
196+
197+
```tsx
198+
useEffect(() => {
199+
if (fetcher.data?.ok) {
200+
// clear the form in the effect
201+
formRef.current?.reset();
202+
if (blocker.state === "blocked") {
203+
blocker.reset();
204+
}
205+
}
206+
}, [fetcher.data]);
207+
```
208+
209+
Alternatively, if a navigation is currently blocked, instead of resetting the blocker, you can proceed through to the blocked navigation.
210+
211+
```tsx
212+
useEffect(() => {
213+
if (fetcher.data?.ok) {
214+
if (blocker.state === "blocked") {
215+
// proceed with the blocked navigation
216+
blocker.proceed();
217+
} else {
218+
formRef.current?.reset();
219+
}
220+
}
221+
}, [fetcher.data]);
222+
```
223+
224+
In this case the user flow is:
225+
226+
- User fills out the form
227+
- User forgets to click "send" and clicks a link instead
228+
- The navigation is blocked, and the confirmation message is shown
229+
- Instead of clicking "leave" or "stay here", the user submits the form
230+
- The user is taken to the requested page

0 commit comments

Comments
 (0)