MENU
useFetcher
Sometimes you want to load or submit data without it counting as a page navigation – a "like" button, an inline edit in one row of a table, a search-as-you-type combobox. useFetcher() is built for exactly this: it gives back a "fetcher" object that talks to a route's loader and action independently of whatever the current page itself is doing.
A fetcher tracks its own state ("idle" / "loading" / "submitting") and data, separate from useNavigation()'s page-wide state. It exposes fetcher.Form (a <Form> that submits without navigating), fetcher.load(href) to imperatively read a route's loader, fetcher.submit(data, options) to imperatively hit a route's action, and fetcher.reset() to clear it back to its initial state. Like the rest of the hooks in this section, it only works underneath a data router (see the Routers section).
import React from "react";
import { createBrowserRouter, useFetcher, useLoaderData } from "react-router";
import { RouterProvider } from "react-router/dom";
let todosDb = [
{ id: "1", title: "Buy milk", completed: false },
{ id: "2", title: "Walk the dog", completed: true },
{ id: "3", title: "Reply to emails", completed: false },
];
const router = createBrowserRouter([
{
path: "/",
element: <TodoList />,
loader: () => todosDb,
action: async ({ request }) => {
let formData = await request.formData();
let id = formData.get("id");
let completed = formData.get("completed") === "true";
let todo = todosDb.find((t) => t.id === id);
todo.completed = completed;
return { ok: true };
},
},
]);
export default function UseFetcherExample() {
return <RouterProvider router={router} />;
}
function TodoList() {
let todos = useLoaderData();
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
function TodoItem({ todo }) {
let fetcher = useFetcher();
// While a submission is in flight, trust the value being sent
// instead of waiting on the round trip to the action.
let completed = fetcher.formData
? fetcher.formData.get("completed") === "true"
: todo.completed;
return (
<li>
<fetcher.Form method="post">
<input type="hidden" name="id" value={todo.id} />
<input type="hidden" name="completed" value={String(!completed)} />
<button type="submit">{completed ? "Done" : "Mark done"}</button>
</fetcher.Form>{" "}
{todo.title}
{fetcher.state !== "idle" && <span> (saving...)</span>}
</li>
);
}Submitting the fetcher's form never navigates away from the list – only that one row's own fetcher goes into a "submitting" state. TodoItem reads fetcher.formData (the in-flight form values) instead of todo.completed while a submission is pending, so its label flips immediately instead of waiting on a round trip; this is the standard "optimistic UI" pattern for fetchers. Once the action returns, React Router automatically revalidates every loader on the page – including the list's own – so the list ends up showing whatever was actually saved. If you don't need a fetcher's isolated state and just want to fire a route's action from code, see useSubmit.