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.