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.
action and loader functions without navigating.
Signature
function useFetcher<T = any>({
key,
}?: {
key?: string;
}): FetcherWithComponents<SerializeFrom<T>>
Parameters
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
An object with the following properties:
Show properties
Show properties
The current state of the fetcher.
The data returned from the loader or action.
The
FormData being submitted, available during submitting state.The URL being submitted to.
The HTTP method being used.
A form component that doesn’t cause navigation when submitted.
Loads data from a route loader.
Submits data to a route action.
Resets the fetcher to idle state.
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>;
}
Related
useFetchers- Access all in-flight fetchersuseSubmit- Submit forms imperatively with navigationForm- Form component with navigationaction- Define route actionloader- Define route loader