UNPKG

11.6 kB Markdown View Raw
1---
2title: Form vs. fetcher
3---
4
5# Form vs. fetcher
6
7[MODES: framework, data]
8
9## Overview
10
11Developing in React Router offers a rich set of tools that can sometimes overlap in functionality, creating a sense of ambiguity for newcomers. The key to effective development in React Router is understanding the nuances and appropriate use cases for each tool. This document seeks to provide clarity on when and why to use specific APIs.
12
13## APIs in Focus
14
15- [`<Form>`][form-component]
16- [`useFetcher`][use-fetcher]
17- [`useNavigation`][use-navigation]
18
19Understanding the distinctions and intersections of these APIs is vital for efficient and effective React Router development.
20
21## URL Considerations
22
23The primary criterion when choosing among these tools is whether you want the URL to change or not:
24
25- **URL Change Desired**: When navigating or transitioning between pages, or after certain actions like creating or deleting records. This ensures that the user's browser history accurately reflects their journey through your application.
26 - **Expected Behavior**: In many cases, when users hit the back button, they should be taken to the previous page. Other times the history entry may be replaced but the URL change is important nonetheless.
27
28- **No URL Change Desired**: For actions that don't significantly change the context or primary content of the current view. This might include updating individual fields or minor data manipulations that don't warrant a new URL or page reload. This also applies to loading data with fetchers for things like popovers, combo boxes, etc.
29
30### When the URL Should Change
31
32These actions typically reflect significant changes to the user's context or state:
33
34- **Creating a New Record**: After creating a new record, it's common to redirect users to a page dedicated to that new record, where they can view or further modify it.
35
36- **Deleting a Record**: If a user is on a page dedicated to a specific record and decides to delete it, the logical next step is to redirect them to a general page, such as a list of all records.
37
38For these cases, developers should consider using a combination of [`<Form>`][form-component] and [`useNavigation`][use-navigation]. These tools can be coordinated to handle form submission, invoke specific actions, retrieve action-related data through component props, and manage navigation respectively.
39
40### When the URL Shouldn't Change
41
42These actions are generally more subtle and don't require a context switch for the user:
43
44- **Updating a Single Field**: Maybe a user wants to change the name of an item in a list or update a specific property of a record. This action is minor and doesn't necessitate a new page or URL.
45
46- **Deleting a Record from a List**: In a list view, if a user deletes an item, they likely expect to remain on the list view, with that item no longer in the list.
47
48- **Creating a Record in a List View**: When adding a new item to a list, it often makes sense for the user to remain in that context, seeing their new item added to the list without a full page transition.
49
50- **Loading Data for a Popover or Combobox**: When loading data for a popover or combobox, the user's context remains unchanged. The data is loaded in the background and displayed in a small, self-contained UI element.
51
52For such actions, [`useFetcher`][use-fetcher] is the go-to API. It's versatile, combining functionalities of these APIs, and is perfectly suited for tasks where the URL should remain unchanged.
53
54## API Comparison
55
56As you can see, the two sets of APIs have a lot of similarities:
57
58| Navigation/URL API | Fetcher API |
59| ----------------------------- | -------------------- |
60| `<Form>` | `<fetcher.Form>` |
61| `actionData` (component prop) | `fetcher.data` |
62| `navigation.state` | `fetcher.state` |
63| `navigation.formAction` | `fetcher.formAction` |
64| `navigation.formData` | `fetcher.formData` |
65
66## Examples
67
68### Creating a New Record
69
70```tsx filename=app/pages/new-recipe.tsx lines=[16,23-24,29]
71import {
72 Form,
73 redirect,
74 useNavigation,
75} from "react-router";
76import type { Route } from "./+types/new-recipe";
77
78export async function action({
79 request,
80}: Route.ActionArgs) {
81 const formData = await request.formData();
82 const errors = await validateRecipeFormData(formData);
83 if (errors) {
84 return { errors };
85 }
86 const recipe = await db.recipes.create(formData);
87 return redirect(`/recipes/${recipe.id}`);
88}
89
90export function NewRecipe({
91 actionData,
92}: Route.ComponentProps) {
93 const { errors } = actionData || {};
94 const navigation = useNavigation();
95 const isSubmitting =
96 navigation.formAction === "/recipes/new";
97
98 return (
99 <Form method="post">
100 <label>
101 Title: <input name="title" />
102 {errors?.title ? <span>{errors.title}</span> : null}
103 </label>
104 <label>
105 Ingredients: <textarea name="ingredients" />
106 {errors?.ingredients ? (
107 <span>{errors.ingredients}</span>
108 ) : null}
109 </label>
110 <label>
111 Directions: <textarea name="directions" />
112 {errors?.directions ? (
113 <span>{errors.directions}</span>
114 ) : null}
115 </label>
116 <button type="submit">
117 {isSubmitting ? "Saving..." : "Create Recipe"}
118 </button>
119 </Form>
120 );
121}
122```
123
124The example leverages [`<Form>`][form-component], component props, and [`useNavigation`][use-navigation] to facilitate an intuitive record creation process.
125
126Using `<Form>` ensures direct and logical navigation. After creating a record, the user is naturally guided to the new recipe's unique URL, reinforcing the outcome of their action.
127
128The component props bridge server and client, providing immediate feedback on submission issues. This quick response enables users to rectify any errors without hindrance.
129
130Lastly, `useNavigation` dynamically reflects the form's submission state. This subtle UI change, like toggling the button's label, assures users that their actions are being processed.
131
132Combined, these APIs offer a balanced blend of structured navigation and feedback.
133
134### Updating a Record
135
136Now consider we're looking at a list of recipes that have delete buttons on each item. When a user clicks the delete button, we want to delete the recipe from the database and remove it from the list without navigating away from the list.
137
138First, consider the basic route setup to get a list of recipes on the page:
139
140```tsx filename=app/pages/recipes.tsx
141import type { Route } from "./+types/recipes";
142
143export async function loader({
144 request,
145}: Route.LoaderArgs) {
146 return {
147 recipes: await db.recipes.findAll({ limit: 30 }),
148 };
149}
150
151export default function Recipes({
152 loaderData,
153}: Route.ComponentProps) {
154 const { recipes } = loaderData;
155 return (
156 <ul>
157 {recipes.map((recipe) => (
158 <RecipeListItem key={recipe.id} recipe={recipe} />
159 ))}
160 </ul>
161 );
162}
163```
164
165Now we'll look at the action that deletes a recipe and the component that renders each recipe in the list.
166
167```tsx filename=app/pages/recipes.tsx lines=[10,21,27]
168import { useFetcher } from "react-router";
169import type { Recipe } from "./recipe.server";
170import type { Route } from "./+types/recipes";
171
172export async function action({
173 request,
174}: Route.ActionArgs) {
175 const formData = await request.formData();
176 const id = formData.get("id");
177 await db.recipes.delete(id);
178 return { ok: true };
179}
180
181export default function Recipes() {
182 return (
183 // ...
184 // doesn't matter, somewhere it's using <RecipeListItem />
185 )
186}
187
188function RecipeListItem({ recipe }: { recipe: Recipe }) {
189 const fetcher = useFetcher();
190 const isDeleting = fetcher.state !== "idle";
191
192 return (
193 <li>
194 <h2>{recipe.title}</h2>
195 <fetcher.Form method="post">
196 <input type="hidden" name="id" value={recipe.id} />
197 <button disabled={isDeleting} type="submit">
198 {isDeleting ? "Deleting..." : "Delete"}
199 </button>
200 </fetcher.Form>
201 </li>
202 );
203}
204```
205
206Using [`useFetcher`][use-fetcher] in this scenario works perfectly. Instead of navigating away or refreshing the entire page, we want in-place updates. When a user deletes a recipe, the `action` is called and the fetcher manages the corresponding state transitions.
207
208The key advantage here is the maintenance of context. The user stays on the list when the deletion completes. The fetcher's state management capabilities are leveraged to give real-time feedback: it toggles between `"Deleting..."` and `"Delete"`, providing a clear indication of the ongoing process.
209
210Furthermore, with each `fetcher` having the autonomy to manage its own state, operations on individual list items become independent, ensuring that actions on one item don't affect the others (though revalidation of the page data is a shared concern covered in [Network Concurrency Management][network-concurrency-management]).
211
212In essence, `useFetcher` offers a seamless mechanism for actions that don't necessitate a change in the URL or navigation, enhancing the user experience by providing real-time feedback and context preservation.
213
214### Mark Article as Read
215
216Imagine you want to mark that an article has been read by the current user, after they've been on the page for a while and scrolled to the bottom. You could make a hook that looks something like this:
217
218```tsx
219import { useFetcher } from "react-router";
220
221function useMarkAsRead({ articleId, userId }) {
222 const marker = useFetcher();
223
224 useSpentSomeTimeHereAndScrolledToTheBottom(() => {
225 marker.submit(
226 { userId },
227 {
228 action: `/article/${articleId}/mark-as-read`,
229 method: "post",
230 },
231 );
232 });
233}
234```
235
236### User Avatar Details Popup
237
238Anytime you show the user avatar, you could put a hover effect that fetches data from a loader and displays it in a popup.
239
240```tsx filename=app/pages/user-details.tsx
241import { useState, useEffect } from "react";
242import { useFetcher } from "react-router";
243import type { Route } from "./+types/user-details";
244
245export async function loader({ params }: Route.LoaderArgs) {
246 return await fakeDb.user.find({
247 where: { id: params.id },
248 });
249}
250
251type LoaderData = Route.ComponentProps["loaderData"];
252
253function UserAvatar({ partialUser }) {
254 const userDetails = useFetcher<LoaderData>();
255 const [showDetails, setShowDetails] = useState(false);
256
257 useEffect(() => {
258 if (
259 showDetails &&
260 userDetails.state === "idle" &&
261 !userDetails.data
262 ) {
263 userDetails.load(`/user-details/${partialUser.id}`);
264 }
265 }, [showDetails, userDetails, partialUser.id]);
266
267 return (
268 <div
269 onMouseEnter={() => setShowDetails(true)}
270 onMouseLeave={() => setShowDetails(false)}
271 >
272 <img src={partialUser.profileImageUrl} />
273 {showDetails ? (
274 userDetails.state === "idle" && userDetails.data ? (
275 <UserPopup user={userDetails.data} />
276 ) : (
277 <UserPopupLoading />
278 )
279 ) : null}
280 </div>
281 );
282}
283```
284
285## Conclusion
286
287React Router offers a range of tools to cater to varied developmental needs. While some functionalities might seem to overlap, each tool has been crafted with specific scenarios in mind. By understanding the intricacies and ideal applications of `<Form>`, `useFetcher`, and `useNavigation`, along with how data flows through component props, developers can create more intuitive, responsive, and user-friendly web applications.
288
289[form-component]: ../api/components/Form
290[use-fetcher]: ../api/hooks/useFetcher
291[use-navigation]: ../api/hooks/useNavigation
292[network-concurrency-management]: ./concurrency
293
\No newline at end of file