Documentation Index Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
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
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: state
'idle' | 'loading' | 'submitting'
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.
formMethod
'get' | 'post' | 'put' | 'patch' | 'delete'
The HTTP method being used.
A form component that doesn’t cause navigation when submitted.
load
(href: string, opts?) => Promise<void>
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 >
);
}
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