| 1 | ---
|
| 2 | title: Using Fetchers
|
| 3 | ---
|
| 4 |
|
| 5 | # Using Fetchers
|
| 6 |
|
| 7 | [MODES: framework, data]
|
| 8 |
|
| 9 | Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
|
| 10 |
|
| 11 | Fetchers 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 |
|
| 15 | The 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
|
| 18 | import { useLoaderData } from "react-router";
|
| 19 |
|
| 20 | export async function clientLoader({ request }) {
|
| 21 | let title = localStorage.getItem("title") || "No Title";
|
| 22 | return { title };
|
| 23 | }
|
| 24 |
|
| 25 | export 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 |
|
| 37 | First we'll add an action to the route for the fetcher to call:
|
| 38 |
|
| 39 | ```tsx lines=[7-11]
|
| 40 | import { useLoaderData } from "react-router";
|
| 41 |
|
| 42 | export async function clientLoader({ request }) {
|
| 43 | // ...
|
| 44 | }
|
| 45 |
|
| 46 | export 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 |
|
| 53 | export default function Component() {
|
| 54 | let data = useLoaderData();
|
| 55 | // ...
|
| 56 | }
|
| 57 | ```
|
| 58 |
|
| 59 | ### 2. Create a fetcher
|
| 60 |
|
| 61 | Next create a fetcher and render a form with it:
|
| 62 |
|
| 63 | ```tsx lines=[7,12-14]
|
| 64 | import { useLoaderData, useFetcher } from "react-router";
|
| 65 |
|
| 66 | // ...
|
| 67 |
|
| 68 | export 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 |
|
| 85 | If 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 |
|
| 89 | Fetchers make their state available during the async work so you can render pending UI the moment the user interacts:
|
| 90 |
|
| 91 | ```tsx lines=[10]
|
| 92 | export 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 |
|
| 110 | Sometimes 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]
|
| 113 | export 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 |
|
| 133 | Data 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 |
|
| 138 | export 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 |
|
| 151 | export 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 |
|
| 176 | Another 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 |
|
| 180 | Consider the following route with a very basic search:
|
| 181 |
|
| 182 | ```tsx filename=./search-users.tsx
|
| 183 | // { path: '/search-users', filename: './search-users.tsx' }
|
| 184 | const users = [
|
| 185 | { id: 1, name: "Ryan" },
|
| 186 | { id: 2, name: "Michael" },
|
| 187 | // ...
|
| 188 | ];
|
| 189 |
|
| 190 | export 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
|
| 203 | import { useFetcher } from "react-router";
|
| 204 |
|
| 205 | export 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]
|
| 223 | import { useFetcher } from "react-router";
|
| 224 | import type { loader } from "./search-users";
|
| 225 |
|
| 226 | export function UserSearchCombobox() {
|
| 227 | let fetcher = useFetcher<typeof loader>();
|
| 228 | // ...
|
| 229 | }
|
| 230 | ```
|
| 231 |
|
| 232 | Ensure you use `import type` so you only import the types.
|
| 233 |
|
| 234 | ### 4. Render the data
|
| 235 |
|
| 236 | ```tsx lines=[10-16]
|
| 237 | import { useFetcher } from "react-router";
|
| 238 |
|
| 239 | export 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 |
|
| 258 | Note 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]
|
| 263 | import { useFetcher } from "react-router";
|
| 264 |
|
| 265 | export 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 |
|
| 290 | Fetchers 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 |
|
| 304 | Note 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
|