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
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
FetcherWithComponents<T>
An object with the following properties:
Show properties
Show properties
'idle' | 'loading' | 'submitting'
The current state of the fetcher.
T
The data returned from the loader or action.
FormData
The
FormData being submitted, available during submitting state.string
The URL being submitted to.
'get' | 'post' | 'put' | 'patch' | 'delete'
The HTTP method being used.
React.Component
A form component that doesn’t cause navigation when submitted.
(href: string, opts?) => Promise<void>
Loads data from a route loader.
FetcherSubmitFunction
Submits data to a route action.
(opts?) => void
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