UNPKG

7.82 kB Markdown View Raw
1---
2title: Using Fetchers
3---
4
5# Using Fetchers
6
7[MODES: framework, data]
8
9Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
10
11Fetchers track their own, independent state and can be used to load data, mutate data, submit forms, and generally interact with loaders and actions.
12
13## Calling Actions
14
15The most common case for a fetcher is to submit data to an action, triggering a revalidation of route data. Consider the following route module:
16
17```tsx
18import { useLoaderData } from "react-router";
19
20export async function clientLoader({ request }) {
21 let title = localStorage.getItem("title") || "No Title";
22 return { title };
23}
24
25export default function Component() {
26 let data = useLoaderData();
27 return (
28 <div>
29 <h1>{data.title}</h1>
30 </div>
31 );
32}
33```
34
35### 1. Add an action
36
37First we'll add an action to the route for the fetcher to call:
38
39```tsx lines=[7-11]
40import { useLoaderData } from "react-router";
41
42export async function clientLoader({ request }) {
43 // ...
44}
45
46export async function clientAction({ request }) {
47 await new Promise((res) => setTimeout(res, 1000));
48 let data = await request.formData();
49 localStorage.setItem("title", data.get("title"));
50 return { ok: true };
51}
52
53export default function Component() {
54 let data = useLoaderData();
55 // ...
56}
57```
58
59### 2. Create a fetcher
60
61Next create a fetcher and render a form with it:
62
63```tsx lines=[7,12-14]
64import { useLoaderData, useFetcher } from "react-router";
65
66// ...
67
68export default function Component() {
69 let data = useLoaderData();
70 let fetcher = useFetcher();
71 return (
72 <div>
73 <h1>{data.title}</h1>
74
75 <fetcher.Form method="post">
76 <input type="text" name="title" />
77 </fetcher.Form>
78 </div>
79 );
80}
81```
82
83### 3. Submit the form
84
85If you submit the form now, the fetcher will call the action and revalidate the route data automatically. See [Revalidation Optimization](./optimize-revalidation) to skip that reload for a route or a single submission.
86
87### 4. Render pending state
88
89Fetchers make their state available during the async work so you can render pending UI the moment the user interacts:
90
91```tsx lines=[10]
92export default function Component() {
93 let data = useLoaderData();
94 let fetcher = useFetcher();
95 return (
96 <div>
97 <h1>{data.title}</h1>
98
99 <fetcher.Form method="post">
100 <input type="text" name="title" />
101 {fetcher.state !== "idle" && <p>Saving...</p>}
102 </fetcher.Form>
103 </div>
104 );
105}
106```
107
108### 5. Optimistic UI
109
110Sometimes there's enough information in the form to render the next state immediately. You can access the form data with `fetcher.formData`:
111
112```tsx lines=[3-4,8]
113export default function Component() {
114 let data = useLoaderData();
115 let fetcher = useFetcher();
116 let title = fetcher.formData?.get("title") || data.title;
117
118 return (
119 <div>
120 <h1>{title}</h1>
121
122 <fetcher.Form method="post">
123 <input type="text" name="title" />
124 {fetcher.state !== "idle" && <p>Saving...</p>}
125 </fetcher.Form>
126 </div>
127 );
128}
129```
130
131### 6. Fetcher Data and Validation
132
133Data returned from an action is available in the fetcher's `data` property. This is primarily useful for returning error messages to the user for a failed mutation:
134
135```tsx lines=[7-10,28-32]
136// ...
137
138export async function clientAction({ request }) {
139 await new Promise((res) => setTimeout(res, 1000));
140 let data = await request.formData();
141
142 let title = data.get("title") as string;
143 if (title.trim() === "") {
144 return { ok: false, error: "Title cannot be empty" };
145 }
146
147 localStorage.setItem("title", title);
148 return { ok: true, error: null };
149}
150
151export default function Component() {
152 let data = useLoaderData();
153 let fetcher = useFetcher();
154 let title = fetcher.formData?.get("title") || data.title;
155
156 return (
157 <div>
158 <h1>{title}</h1>
159
160 <fetcher.Form method="post">
161 <input type="text" name="title" />
162 {fetcher.state !== "idle" && <p>Saving...</p>}
163 {fetcher.data?.error && (
164 <p style={{ color: "red" }}>
165 {fetcher.data.error}
166 </p>
167 )}
168 </fetcher.Form>
169 </div>
170 );
171}
172```
173
174## Loading Data
175
176Another common use case for fetchers is to load data from a route for something like a combobox.
177
178### 1. Create a search route
179
180Consider the following route with a very basic search:
181
182```tsx filename=./search-users.tsx
183// { path: '/search-users', filename: './search-users.tsx' }
184const users = [
185 { id: 1, name: "Ryan" },
186 { id: 2, name: "Michael" },
187 // ...
188];
189
190export async function loader({ request }) {
191 await new Promise((res) => setTimeout(res, 300));
192 let url = new URL(request.url);
193 let query = url.searchParams.get("q");
194 return users.filter((user) =>
195 user.name.toLowerCase().includes(query.toLowerCase()),
196 );
197}
198```
199
200### 2. Render a fetcher in a combobox component
201
202```tsx
203import { useFetcher } from "react-router";
204
205export function UserSearchCombobox() {
206 let fetcher = useFetcher();
207 return (
208 <div>
209 <fetcher.Form method="get" action="/search-users">
210 <input type="text" name="q" />
211 </fetcher.Form>
212 </div>
213 );
214}
215```
216
217- The action points to the route we created above: "/search-users"
218- The name of the input is "q" to match the query parameter
219
220### 3. Add type inference
221
222```tsx lines=[2,5]
223import { useFetcher } from "react-router";
224import type { loader } from "./search-users";
225
226export function UserSearchCombobox() {
227 let fetcher = useFetcher<typeof loader>();
228 // ...
229}
230```
231
232Ensure you use `import type` so you only import the types.
233
234### 4. Render the data
235
236```tsx lines=[10-16]
237import { useFetcher } from "react-router";
238
239export function UserSearchCombobox() {
240 let fetcher = useFetcher<typeof loader>();
241 return (
242 <div>
243 <fetcher.Form method="get" action="/search-users">
244 <input type="text" name="q" />
245 </fetcher.Form>
246 {fetcher.data && (
247 <ul>
248 {fetcher.data.map((user) => (
249 <li key={user.id}>{user.name}</li>
250 ))}
251 </ul>
252 )}
253 </div>
254 );
255}
256```
257
258Note you will need to hit "enter" to submit the form and see the results.
259
260### 5. Render a pending state
261
262```tsx lines=[12-14]
263import { useFetcher } from "react-router";
264
265export function UserSearchCombobox() {
266 let fetcher = useFetcher<typeof loader>();
267 return (
268 <div>
269 <fetcher.Form method="get" action="/search-users">
270 <input type="text" name="q" />
271 </fetcher.Form>
272 {fetcher.data && (
273 <ul
274 style={{
275 opacity: fetcher.state === "idle" ? 1 : 0.25,
276 }}
277 >
278 {fetcher.data.map((user) => (
279 <li key={user.id}>{user.name}</li>
280 ))}
281 </ul>
282 )}
283 </div>
284 );
285}
286```
287
288### 6. Search on user input
289
290Fetchers can be submitted programmatically with `fetcher.submit`:
291
292```tsx lines=[5-7]
293<fetcher.Form method="get" action="/search-users">
294 <input
295 type="text"
296 name="q"
297 onChange={(event) => {
298 fetcher.submit(event.currentTarget.form);
299 }}
300 />
301</fetcher.Form>
302```
303
304Note the input event's form is passed as the first argument to `fetcher.submit`. The fetcher will use that form to submit the request, reading its attributes and serializing the data from its elements.
305
306`fetcher.submit` accepts the same [`defaultShouldRevalidate`][fetcher-submit-options] option as [`<Form>`][form]. Pass `false` to skip loader revalidation for that submission. See [Revalidation Optimization](./optimize-revalidation) for how that interacts with route-level `shouldRevalidate`.
307
308[fetcher-submit-options]: https://api.reactrouter.com/v8/interfaces/react-router.FetcherSubmitOptions.html#defaultshouldrevalidate
309[form]: ../api/components/Form#defaultshouldrevalidate