# What Is a Fetcher in React Router and How It Differs from Navigation Loaders

> Discover React Router fetchers. Learn how they let components invoke loaders and actions independently of navigation, maintaining separate states without URL changes. Understand this key data-fetching primitive.

- Repository: [Remix/react-router](https://github.com/remix-run/react-router)
- Tags: deep-dive
- Published: 2026-03-06

---

**A fetcher is a standalone data-fetching primitive that lets React components invoke route loaders and actions without changing the URL or browser history, maintaining independent state while keeping the navigation stack untouched.**

The Data Router API in `remix-run/react-router` provides two mechanisms for asynchronous data operations: navigation-bound loaders and isolated fetchers. While both execute the same underlying loader and action functions, a **fetcher in React Router** operates entirely outside the navigation lifecycle, making it essential for building non-disruptive, component-scoped interactions.

## What Is a Fetcher in React Router?

A fetcher is a lightweight request object created by the `useFetcher()` hook. According to the source code in [`packages/react-router/lib/hooks.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx) (lines 2894-2925), calling `useFetcher()` returns a `FetcherWithComponents` object that contains its own state machine, data cache, and AbortController.

The fetcher maintains three distinct states defined in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts) (lines 630-698):

- `idle` – No request is active
- `loading` – A GET request to a loader is in flight
- `submitting` – A mutation (POST/PUT/PATCH/DELETE) to an action is being processed

### The Fetcher API Surface

The object returned by `useFetcher()` exposes these properties and methods:

- `state` – Current status (`"idle"`, `"loading"`, or `"submitting"`)
- `data` – The JSON response from the executed loader or action
- `load(url)` – Triggers a GET request that runs the target route's loader without URL changes
- `submit(target, options)` – Sends form data to a route's action without navigation
- `Form` – A React component (`<fetcher.Form>`) that automatically wires submission handling, implemented in [`packages/react-router/lib/dom/lib.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx) (lines 1800-1824)
- `reset()` – Clears the fetcher's state and data, returning it to `idle`

## How Fetchers Work Under the Hood

Internally, the router exposes a private `fetch(key, routeId, href, opts)` method in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts) (lines 179-186). When `useFetcher()` is invoked, it generates a unique key (or accepts a user-provided one via the `key` option) and registers the instance in `router.state.fetchers`.

When you call `fetcher.load()` or `fetcher.submit()`, the router creates a submission object and invokes the same internal data-strategy pipeline used for full navigation. Crucially, the implementation deliberately skips the history stack update—**the URL remains unchanged** and no new entry is pushed to `window.history`.

Each fetcher also maintains its own AbortController, allowing requests to be cancelled independently of route transitions. This isolation ensures that aborting a fetcher request does not interrupt an active navigation loader or other concurrent fetchers.

## Fetcher vs Navigation Loader: Key Differences

| Feature | Navigation Loader | Fetcher |
|---------|-------------------|---------|
| **Trigger Mechanism** | `router.navigate()` or `<Link>`/`<Form>` elements causing URL change | `fetcher.load()` or `fetcher.submit()`—**no URL change** |
| **State Storage** | `router.state.loaderData` mapped to current route | `router.state.fetchers.get(key).data`—isolated per fetcher instance |
| **History Side-Effect** | Updates browser history via push or replace | **No history entry**; user remains on current location |
| **Revalidation** | Automatic on navigation, `revalidate()` calls, or data mutations | Manual via explicit `fetcher.load()` call or when a parent navigation triggers route revalidation |
| **Typical Use Case** | Page transitions, SEO-critical data, deep-linkable resources | Inline updates, modal dialogs, "load more" buttons, autocomplete, optimistic UI |

Navigation loaders are intrinsically tied to the URL. When a user clicks a `<Link to="/users">`, React Router executes the target route's loader and updates the address bar. A fetcher, by contrast, can invoke that same `/users` loader while keeping the user on the current page, retrieving data in the background without disrupting the existing UI context.

## Practical Implementation Examples

### Basic Fetcher Usage

This component loads data on mount and submits a form without navigation:

```tsx
import { useFetcher } from "react-router";

function TodoList() {
  const fetcher = useFetcher<{ items: string[] }>();

  React.useEffect(() => {
    fetcher.load("/todos");
  }, [fetcher]);

  return (
    <>
      {fetcher.state === "loading" && <p>Loading…</p>}
      {fetcher.state === "idle" && (
        <ul>
          {fetcher.data?.items.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      )}

      <fetcher.Form method="post" action="/todos">
        <input name="task" required />
        <button type="submit">Add Todo</button>
      </fetcher.Form>
    </>
  );
}

```

The `load()` call triggers the `/todos` loader, while `<fetcher.Form>` submits to the `/todos` action—neither operation changes the browser URL.

### Sharing Fetcher State Across Components

Using the `key` option allows multiple components to reference the same fetcher instance:

```tsx
// Component A - initiates the search
function SearchInput() {
  const fetcher = useFetcher({ key: "search" });
  return (
    <input
      placeholder="Search users"
      onChange={e => fetcher.load(`/search?q=${e.target.value}`)}
    />
  );
}

// Component B - displays results
function SearchResults() {
  const fetcher = useFetcher({ key: "search" });
  if (fetcher.state === "loading") return <p>Searching…</p>;
  return <ul>{/* render fetcher.data */}</ul>;
}

```

Both components access `router.state.fetchers.get("search")`, enabling synchronized state without prop drilling.

### Contrast with Navigation-Based Loading

To illustrate the behavioral difference:

```tsx
// Navigation approach - changes URL and history
function NavigationalLink() {
  const navigate = useNavigate();
  return (
    <button onClick={() => navigate("/users/123")}>
      Go to User Profile
    </button>
  );
}

// Fetcher approach - stays on current page
function InlineDataLoad() {
  const fetcher = useFetcher();
  return (
    <button onClick={() => fetcher.load("/users/123")}>
      Load User Data Inline
    </button>
  );
}

```

The navigation example pushes `/users/123` onto the history stack and renders that route's component tree. The fetcher example retrieves the user data while keeping the current route's UI mounted and the URL unchanged.

## Summary

- A **fetcher in React Router** is an isolated data-fetching primitive accessed via `useFetcher()` that executes route loaders and actions without navigation side-effects.
- Fetchers maintain independent state (`idle` → `loading`/`submitting`) stored in `router.state.fetchers`, separate from the global `router.state.loaderData` used by navigation loaders.
- Unlike navigation, fetchers do not modify browser history or the URL, making them ideal for modal forms, infinite scroll, and background data synchronization.
- The implementation spans [`packages/react-router/lib/hooks.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx) (public API), [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts) (state machine), and [`packages/react-router/lib/dom/lib.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx) (form components).
- Fetchers support shared state through user-defined keys and provide their own AbortController for request cancellation independent of route transitions.

## Frequently Asked Questions

### Can a fetcher trigger a navigation loader?

No. Fetchers are explicitly designed to avoid navigation. However, if a subsequent navigation occurs that revalidates the route a fetcher previously interacted with, the fetcher's data may reflect updates if the route's loader returns new data when revalidated.

### When should I use a fetcher instead of a navigation loader?

Use a **fetcher** when you need to load or mutate data without disrupting the user's current location, such as for inline validation, dropdown autocomplete, "load more" pagination, or modal dialogs. Use **navigation loaders** when the data represents a distinct location that users should be able to bookmark, share, or navigate back to via browser history.

### How does fetcher state differ from `useNavigation()`?

`useNavigation()` tracks the state of the current page transition (loading or submitting) for navigation loaders globally. A fetcher's `state` property tracks only that specific fetcher's request. Multiple fetchers can simultaneously exist in different states (e.g., one `loading` and another `submitting`) while `useNavigation` reflects only the active route transition state.

### Can I cancel an active fetcher request?

Yes. Each fetcher instance maintains its own AbortController. When a component unmounts or a new request is initiated on the same fetcher key, React Router automatically aborts the previous request. This cancellation logic is handled internally in [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts) without affecting other active fetchers or navigation requests.