useFetcher
Own an independent route request channel.
Compatibility
Import from @kasperrt/smolter/react @kasperrt/smolter/preact
Signature
function useFetcher<T extends RouteModule>(
route: RouteDefinition<T>,
): TypedFetcher<
| DataOf<Extract<T["action"] | T["clientAction"], AnyDataFunction>>
| DataOf<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>
>;
function useFetcher<T = DataFunction>(): TypedFetcher<RouteData<T>>;Parameters
Optionally pass a typed route definition to bind the default target and infer loader/action results. Otherwise provide a handler type generic.
Returns
data, error, state, Form, load(url?), and submit(formData, { action?, method? }).
Usage
Import this API from one of the entry points listed above. Keep server and build helpers in their dedicated subpaths.
const fetcher = useFetcher<typeof loader>();
<button onClick={() => fetcher.load("/tasks/1")}>Load task</button>
<p>{fetcher.data?.title}</p>Behavior and edge cases
load reads a registered loader into fetcher.data. submit defaults to POST and does not navigate unless the action redirects. A bound dynamic pattern uses the concrete active URL only when that route is active; otherwise provide a concrete URL. Unmounting aborts the fetcher. Reads do not revalidate the page; successful writes do.
load(url): independent reads
load selects the loader on a registered route. It changes this fetcher from idle to loading, then back to idle. The returned loader value goes to fetcher.data. It does not navigate or refresh the current page loader. A missing URL defaults to the bound route or current location.
import { useFetcher } from "@kasperrt/smolter/react";
import type { clientLoader } from "../tasks.$id/loader";
export default function TaskPreview() {
const preview = useFetcher<typeof clientLoader>();
const busy = preview.state !== "idle";
return (
<section aria-busy={busy}>
<button onClick={() => preview.load("/tasks/42")} disabled={busy}>
Preview task
</button>
{preview.data && <p>{preview.data.title}</p>}
{preview.error && <p role="alert">{preview.error.message}</p>}
</section>
);
}GET Form: independent search
A fetcher GET form serializes fields into the target URL and loads that route. The browser address does not change. Render the returned search results from this fetcher, rather than useLoaderData. The target loader reads q from request.url.
const search = useFetcher<typeof clientLoader>();
return (
<section>
<search.Form action="/tasks/search" method="get">
<label>Query <input name="q" /></label>
<button>Find tasks</button>
</search.Form>
{search.data?.results.map((task) => <p key={task.id}>{task.title}</p>)}
</section>
);submit(body, options): mutations
Pass FormData, an optional action URL, and an uppercase method. The default method is POST. The action result goes to fetcher.data. Successful mutations revalidate the active page loader subject to shouldRevalidate. The returned Promise resolves to a Result with data or error; use fetcher.data for the hook’s inferred result type. A subsequent registered revalidator can still reject the submission Promise. The fetcher leaves submitting state before that failure is propagated, including after a redirect, and remains usable for later requests. Handle that rejection without repeating a successful mutation.
const body = new FormData();
body.set("title", "Updated task");
await fetcher.submit(body, { action: "/tasks/42", method: "PATCH" });
// DELETE uses the same registered route action.
await fetcher.submit(new FormData(), {
action: "/tasks/42",
method: "DELETE",
});Form: declarative mutations
The bound Form accepts the same props as Form, including action and lowercase method. It uses this fetcher’s state and data rather than page actionData. Standard named fields and the selected submit button become FormData.
<fetcher.Form action="/tasks/42" method="patch">
<label>
Title <input name="title" required />
</label>
<button disabled={fetcher.state !== "idle"}>Save</button>
</fetcher.Form>;state: pending UI
idle means this channel has no work pending. loading means a read is pending, or page data is being revalidated after an action. submitting means the mutation is pending. A successful write normally moves submitting → loading → idle. Keep controls stable and show a separate status message. Each useFetcher call has independent state.
data and error: ownership and lifetime
data is initially undefined and retains the previous result while another request is pending. A validation object is data, while an Error is exposed through error. An action result is not the same as the page’s freshly loaded data. There is no global keyed fetcher registry. Successful mutations revalidate registered fetchers that have completed a read, subject to their route’s shouldRevalidate; submitting fetchers and disposed channels are skipped.
Types and dynamic URLs
useFetcher<typeof handler>() derives its data from a specific loader or action. useFetcher(typedRoute) derives a union of the route’s read and mutation results. Narrow that union before reading operation-specific fields. For a bound /tasks/:id route that is not active, pass a concrete URL such as /tasks/42; the router cannot invent id.
Concurrency and cleanup
Starting another request on the same fetcher aborts its previous request and ignores late completion. Separate fetchers can run concurrently. The framework hook disposes its channel when unmounted. Forward signal from the handler to fetch so cancellation reaches the network. Cancellation does not roll back a write already processed by a backend.
Differences to keep in mind
This API does not expose React Router’s fetcher.formData, formAction, formMethod, keyed fetchers, or a reset method. Keep any optimistic draft in your own component state and reconcile it with the confirmed result. A redirect returned by an action navigates even though an ordinary fetcher submission stays on the current page.