UNPKG

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