Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
Fetchers track their own, independent state and can be used to load data, mutate data, submit forms, and generally interact with loaders and actions.
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:
import { useLoaderData } from "react-router";
export async function clientLoader({ request }) {
let title = localStorage.getItem("title") || "No Title";
return { title };
}
export default function Component() {
let data = useLoaderData();
return (
<div>
<h1>{data.title}</h1>
</div>
);
}
First we'll add an action to the route for the fetcher to call:
import { useLoaderData } from "react-router";
export async function clientLoader({ request }) {
// ...
}
export async function clientAction({ request }) {
await new Promise((res) => setTimeout(res, 1000));
let data = await request.formData();
localStorage.setItem("title", data.get("title"));
return { ok: true };
}
export default function Component() {
let data = useLoaderData();
// ...
}
Next create a fetcher and render a form with it:
import { useLoaderData, useFetcher } from "react-router";
// ...
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
return (
<div>
<h1>{data.title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
</fetcher.Form>
</div>
);
}
If you submit the form now, the fetcher will call the action and revalidate the route data automatically.
Fetchers make their state available during the async work so you can render pending UI the moment the user interacts:
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
return (
<div>
<h1>{data.title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
{fetcher.state !== "idle" && <p>Saving...</p>}
</fetcher.Form>
</div>
);
}
Sometimes there's enough information in the form to render the next state immediately. You can access the form data with fetcher.formData
:
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
let title = fetcher.formData?.get("title") || data.title;
return (
<div>
<h1>{title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
{fetcher.state !== "idle" && <p>Saving...</p>}
</fetcher.Form>
</div>
);
}
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:
// ...
export async function clientAction({ request }) {
await new Promise((res) => setTimeout(res, 1000));
let data = await request.formData();
let title = data.get("title") as string;
if (title.trim() === "") {
return { ok: false, error: "Title cannot be empty" };
}
localStorage.setItem("title", title);
return { ok: true, error: null };
}
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
let title = fetcher.formData?.get("title") || data.title;
return (
<div>
<h1>{title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
{fetcher.state !== "idle" && <p>Saving...</p>}
{fetcher.data?.error && (
<p style={{ color: "red" }}>
{fetcher.data.error}
</p>
)}
</fetcher.Form>
</div>
);
}
Another common use case for fetchers is to load data from a route for something like a combobox.
Consider the following route with a very basic search:
// { path: '/search-users', filename: './search-users.tsx' }
const users = [
{ id: 1, name: "Ryan" },
{ id: 2, name: "Michael" },
// ...
];
export async function loader({ request }) {
await new Promise((res) => setTimeout(res, 300));
let url = new URL(request.url);
let query = url.searchParams.get("q");
return users.filter((user) =>
user.name.toLowerCase().includes(query.toLowerCase())
);
}
import { useFetcher } from "react-router";
export function UserSearchCombobox() {
let fetcher = useFetcher();
return (
<div>
<fetcher.Form method="get" action="/search-users">
<input type="text" name="q" />
</fetcher.Form>
</div>
);
}
import { useFetcher } from "react-router";
import type { Search } from "./search-users";
export function UserSearchCombobox() {
let fetcher = useFetcher<typeof Search.action>();
// ...
}
Ensure you use import type
so you only import the types.
import { useFetcher } from "react-router";
export function UserSearchCombobox() {
let fetcher = useFetcher<typeof Search.action>();
return (
<div>
<fetcher.Form method="get" action="/search-users">
<input type="text" name="q" />
</fetcher.Form>
{fetcher.data && (
<ul>
{fetcher.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
Note you will need to hit "enter" to submit the form and see the results.
import { useFetcher } from "react-router";
export function UserSearchCombobox() {
let fetcher = useFetcher<typeof Search.action>();
return (
<div>
<fetcher.Form method="get" action="/search-users">
<input type="text" name="q" />
</fetcher.Form>
{fetcher.data && (
<ul
style={{
opacity: fetcher.state === "idle" ? 1 : 0.25,
}}
>
{fetcher.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
Fetchers can be submitted programmatically with fetcher.submit
:
<fetcher.Form method="get" action="/search-users">
<input
type="text"
name="q"
onChange={(event) => {
fetcher.submit(event.currentTarget.form);
}}
/>
</fetcher.Form>
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.