UNPKG

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