Skip to main content

useFetcher

Useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
This hook only works in Data and Framework modes.
Fetchers track their own, independent state and can be used to load data, submit forms, and generally interact with action and loader functions without navigating.

Signature

function useFetcher<T = any>({
  key,
}?: {
  key?: string;
}): FetcherWithComponents<SerializeFrom<T>>

Parameters

options.key
string
A unique key to identify the fetcher. If you want to access the same fetcher from elsewhere in your app, provide a key. By default, useFetcher generates a unique fetcher scoped to that component.

Returns

fetcher
FetcherWithComponents<T>
An object with the following properties:

Usage

Basic usage

import { useFetcher } from "react-router";

function NewsletterSignup() {
  const fetcher = useFetcher();
  
  return (
    <fetcher.Form method="post" action="/newsletter/subscribe">
      <input type="email" name="email" />
      <button type="submit">
        {fetcher.state === "submitting" ? "Subscribing..." : "Subscribe"}
      </button>
      
      {fetcher.data?.success && <p>Thanks for subscribing!</p>}
    </fetcher.Form>
  );
}

Load data

function SearchCombobox() {
  const fetcher = useFetcher();
  
  return (
    <div>
      <input
        type="search"
        onChange={(e) => {
          fetcher.load(`/search?q=${e.target.value}`);
        }}
      />
      
      {fetcher.state === "loading" && <Spinner />}
      
      {fetcher.data && (
        <ul>
          {fetcher.data.results.map((result) => (
            <li key={result.id}>{result.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Submit data imperatively

function TodoItem({ todo }) {
  const fetcher = useFetcher();
  
  return (
    <div>
      <span>{todo.title}</span>
      <button
        onClick={() => {
          fetcher.submit(
            { intent: "delete", id: todo.id },
            { method: "post", action: "/todos" }
          );
        }}
      >
        {fetcher.state === "submitting" ? "Deleting..." : "Delete"}
      </button>
    </div>
  );
}

Submit FormData

function ImageUploader() {
  const fetcher = useFetcher();
  
  return (
    <div>
      <input
        type="file"
        onChange={(e) => {
          const formData = new FormData();
          formData.append("image", e.target.files[0]);
          
          fetcher.submit(formData, {
            method: "post",
            action: "/upload",
            encType: "multipart/form-data",
          });
        }}
      />
      
      {fetcher.state === "submitting" && <p>Uploading...</p>}
      {fetcher.data?.url && <img src={fetcher.data.url} />}
    </div>
  );
}

Submit JSON

function SaveButton({ data }) {
  const fetcher = useFetcher();
  
  return (
    <button
      onClick={() => {
        fetcher.submit(
          { userId: 1, data },
          {
            method: "post",
            action: "/api/save",
            encType: "application/json",
          }
        );
      }}
    >
      Save
    </button>
  );
}

Reset fetcher

function Component() {
  const fetcher = useFetcher();
  
  return (
    <div>
      <fetcher.Form method="post">
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </fetcher.Form>
      
      {fetcher.data?.success && (
        <div>
          <p>Message sent!</p>
          <button onClick={() => fetcher.reset()}>
            Dismiss
          </button>
        </div>
      )}
    </div>
  );
}

Shared fetcher with key

// Component A
function ComponentA() {
  const fetcher = useFetcher({ key: "my-key" });
  
  return (
    <button onClick={() => fetcher.load("/data")}>
      Load Data
    </button>
  );
}

// Component B - access same fetcher
function ComponentB() {
  const fetcher = useFetcher({ key: "my-key" });
  
  return (
    <div>
      {fetcher.state === "loading" && <Spinner />}
      {fetcher.data && <Data data={fetcher.data} />}
    </div>
  );
}

Common Patterns

Optimistic UI

function TodoList({ todos }) {
  const fetcher = useFetcher();
  
  // Optimistically show as complete
  const optimisticTodos = todos.map((todo) => {
    if (fetcher.formData?.get("id") === todo.id) {
      return { ...todo, complete: true };
    }
    return todo;
  });
  
  return (
    <ul>
      {optimisticTodos.map((todo) => (
        <li key={todo.id}>
          <span
            style={{
              textDecoration: todo.complete ? "line-through" : "none",
            }}
          >
            {todo.title}
          </span>
          
          {!todo.complete && (
            <fetcher.Form method="post" action="/todos/complete">
              <input type="hidden" name="id" value={todo.id} />
              <button type="submit">Complete</button>
            </fetcher.Form>
          )}
        </li>
      ))}
    </ul>
  );
}

Inline editing

function EditableField({ value, name, action }) {
  const [isEditing, setIsEditing] = useState(false);
  const fetcher = useFetcher();
  
  useEffect(() => {
    if (fetcher.state === "idle" && fetcher.data) {
      setIsEditing(false);
    }
  }, [fetcher.state, fetcher.data]);
  
  if (!isEditing) {
    return (
      <div onClick={() => setIsEditing(true)}>
        {value}
      </div>
    );
  }
  
  return (
    <fetcher.Form method="post" action={action}>
      <input
        type="text"
        name={name}
        defaultValue={value}
        autoFocus
      />
      <button type="submit">Save</button>
      <button onClick={() => setIsEditing(false)}>Cancel</button>
    </fetcher.Form>
  );
}

Mark as read

function Notification({ notification }) {
  const fetcher = useFetcher();
  
  // Show as read optimistically
  const isRead = notification.read ||
    fetcher.formData?.get("id") === notification.id;
  
  return (
    <div style={{ fontWeight: isRead ? "normal" : "bold" }}>
      <p>{notification.message}</p>
      
      {!isRead && (
        <fetcher.Form
          method="post"
          action="/notifications/mark-read"
        >
          <input type="hidden" name="id" value={notification.id} />
          <button type="submit">Mark as Read</button>
        </fetcher.Form>
      )}
    </div>
  );
}

Autocomplete

function Autocomplete() {
  const fetcher = useFetcher();
  const [query, setQuery] = useState("");
  
  useEffect(() => {
    if (query) {
      fetcher.load(`/search?q=${query}`);
    }
  }, [query]);
  
  return (
    <div>
      <input
        type="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      
      {fetcher.data && (
        <ul>
          {fetcher.data.results.map((result) => (
            <li key={result.id}>
              <a href={result.url}>{result.name}</a>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Add to cart

function ProductCard({ product }) {
  const fetcher = useFetcher();
  const isAdding = fetcher.state === "submitting";
  const added = fetcher.state === "idle" && fetcher.data != null;
  
  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      
      <fetcher.Form method="post" action="/cart/add">
        <input type="hidden" name="productId" value={product.id} />
        <button type="submit" disabled={isAdding}>
          {isAdding ? "Adding..." : added ? "Added!" : "Add to Cart"}
        </button>
      </fetcher.Form>
    </div>
  );
}

Type Safety

With TypeScript

interface NewsletterData {
  success?: boolean;
  error?: string;
}

function Newsletter() {
  const fetcher = useFetcher<NewsletterData>();
  
  // fetcher.data is typed
  if (fetcher.data?.success) {
    return <p>Thanks for subscribing!</p>;
  }
  
  return <fetcher.Form method="post">...</fetcher.Form>;
}

With loader/action types

import type { action } from "./route";

function Component() {
  const fetcher = useFetcher<typeof action>();
  
  // fetcher.data is inferred from action return type
  return <div>{fetcher.data?.message}</div>;
}
  • useFetchers - Access all in-flight fetchers
  • useSubmit - Submit forms imperatively with navigation
  • Form - Form component with navigation
  • action - Define route action
  • loader - Define route loader

Build docs developers (and LLMs) love