| 1 | ---
|
| 2 | title: Using Fetchers
|
| 3 | ---
|
| 4 |
|
| 5 | # Using Fetchers
|
| 6 |
|
| 7 | [MODES: framework, data]
|
| 8 |
|
| 9 | <br/>
|
| 10 | <br/>
|
| 11 |
|
| 12 | Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
|
| 13 |
|
| 14 | Fetchers 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 |
|
| 18 | 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:
|
| 19 |
|
| 20 | ```tsx
|
| 21 | import { useLoaderData } from "react-router";
|
| 22 |
|
| 23 | export async function clientLoader({ request }) {
|
| 24 | let title = localStorage.getItem("title") || "No Title";
|
| 25 | return { title };
|
| 26 | }
|
| 27 |
|
| 28 | export 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 |
|
| 40 | First we'll add an action to the route for the fetcher to call:
|
| 41 |
|
| 42 | ```tsx lines=[7-11]
|
| 43 | import { useLoaderData } from "react-router";
|
| 44 |
|
| 45 | export async function clientLoader({ request }) {
|
| 46 | // ...
|
| 47 | }
|
| 48 |
|
| 49 | export 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 |
|
| 56 | export default function Component() {
|
| 57 | let data = useLoaderData();
|
| 58 | // ...
|
| 59 | }
|
| 60 | ```
|
| 61 |
|
| 62 | ### 2. Create a fetcher
|
| 63 |
|
| 64 | Next create a fetcher and render a form with it:
|
| 65 |
|
| 66 | ```tsx lines=[7,12-14]
|
| 67 | import { useLoaderData, useFetcher } from "react-router";
|
| 68 |
|
| 69 | // ...
|
| 70 |
|
| 71 | export 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 |
|
| 88 | If you submit the form now, the fetcher will call the action and revalidate the route data automatically.
|
| 89 |
|
| 90 | ### 4. Render pending state
|
| 91 |
|
| 92 | Fetchers make their state available during the async work so you can render pending UI the moment the user interacts:
|
| 93 |
|
| 94 | ```tsx lines=[10]
|
| 95 | export 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 |
|
| 113 | Sometimes 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]
|
| 116 | export 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 |
|
| 136 | 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:
|
| 137 |
|
| 138 | ```tsx lines=[7-10,28-32]
|
| 139 | // ...
|
| 140 |
|
| 141 | export 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 |
|
| 154 | export 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 |
|
| 179 | Another 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 |
|
| 183 | Consider the following route with a very basic search:
|
| 184 |
|
| 185 | ```tsx filename=./search-users.tsx
|
| 186 | // { path: '/search-users', filename: './search-users.tsx' }
|
| 187 | const users = [
|
| 188 | { id: 1, name: "Ryan" },
|
| 189 | { id: 2, name: "Michael" },
|
| 190 | // ...
|
| 191 | ];
|
| 192 |
|
| 193 | export 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
|
| 206 | import { useFetcher } from "react-router";
|
| 207 |
|
| 208 | export 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]
|
| 226 | import { useFetcher } from "react-router";
|
| 227 | import type { loader } from "./search-users";
|
| 228 |
|
| 229 | export function UserSearchCombobox() {
|
| 230 | let fetcher = useFetcher<typeof loader>();
|
| 231 | // ...
|
| 232 | }
|
| 233 | ```
|
| 234 |
|
| 235 | Ensure you use `import type` so you only import the types.
|
| 236 |
|
| 237 | ### 4. Render the data
|
| 238 |
|
| 239 | ```tsx lines=[10-16]
|
| 240 | import { useFetcher } from "react-router";
|
| 241 |
|
| 242 | export 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 |
|
| 261 | Note 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]
|
| 266 | import { useFetcher } from "react-router";
|
| 267 |
|
| 268 | export 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 |
|
| 293 | Fetchers 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 |
|
| 307 | 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.
|