UNPKG

5.73 kB Markdown View Raw
1---
2title: Navigation Blocking
3---
4
5# Navigation Blocking
6
7[MODES: framework, data]
8
9When 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
11This example will show:
12
13- Setting up a route with a form and action called with a fetcher
14- Blocking navigation when the form is dirty
15- Showing a confirmation when the user tries to leave the page
16
17## 1. Set up a route with a form
18
19Add a route with the form, we'll use a "contact" route for this example:
20
21```ts filename=routes.ts
22import {
23 type RouteConfig,
24 index,
25 route,
26} from "@react-router/dev/routes";
27
28export default [
29 index("routes/home.tsx"),
30 route("contact", "routes/contact.tsx"),
31] satisfies RouteConfig;
32```
33
34Add the form to the contact route module:
35
36```tsx filename=routes/contact.tsx
37import { useFetcher } from "react-router";
38import type { Route } from "./+types/contact";
39
40export 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
50export 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
75To 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]
78export 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]
100import { useBlocker } from "react-router";
101
102export 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
113While this will now block a navigation, there's no way for the user to confirm it.
114
115## 4. Show confirmation UI
116
117This uses a simple div, but you may want to use a modal dialog.
118
119```tsx filename=routes/contact.tsx lines=[19-41]
120export 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
162If 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
166If 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.
167
168```tsx filename=routes/contact.tsx
169useEffect(() => {
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
180While unrelated to navigation blocking, let's clear the form when the action resolves with a ref.
181
182```tsx
183let 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
198useEffect(() => {
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
209Alternatively, if a navigation is currently blocked, instead of resetting the blocker, you can proceed through to the blocked navigation.
210
211```tsx
212useEffect(() => {
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
224In 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