useFetchers
Returns an array of all in-flight fetchers. This is useful for components throughout the app that didn’t create the fetchers but want to use their submissions to participate in optimistic UI.
This hook only works in Data and Framework modes.
Signature
function useFetchers () : ( Fetcher & { key : string })[]
Parameters
None.
Returns
fetchers
Array<Fetcher & { key: string }>
An array of all in-flight fetchers, each with a unique key property and the following fields: A unique identifier for the fetcher.
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.
Usage
Global loading indicator
import { useFetchers } from "react-router" ;
function GlobalLoadingIndicator () {
const fetchers = useFetchers ();
const isLoading = fetchers . some (
( fetcher ) => fetcher . state !== "idle"
);
if ( ! isLoading ) return null ;
return (
< div className = "global-spinner" >
< Spinner />
</ div >
);
}
Optimistic UI across components
// Component A - adds items
function AddToCart ({ productId }) {
const fetcher = useFetcher ();
return (
< fetcher.Form method = "post" action = "/cart/add" >
< input type = "hidden" name = "productId" value = { productId } />
< button type = "submit" > Add to Cart </ button >
</ fetcher.Form >
);
}
// Component B - shows cart count
function CartBadge () {
const fetchers = useFetchers ();
const { cart } = useLoaderData ();
// Count items being added
const addingToCart = fetchers . filter (
( f ) =>
f . formAction === "/cart/add" &&
f . state === "submitting"
). length ;
const totalItems = cart . items . length + addingToCart ;
return < span className = "badge" > { totalItems } </ span > ;
}
Track pending deletions
function TodoList ({ todos }) {
const fetchers = useFetchers ();
// Get IDs of todos being deleted
const deletingIds = fetchers
. filter (( f ) => f . formData ?. get ( "intent" ) === "delete" )
. map (( f ) => f . formData ?. get ( "id" ));
return (
< ul >
{ todos . map (( todo ) => {
const isDeleting = deletingIds . includes ( todo . id );
return (
< li
key = { todo . id }
style = { { opacity: isDeleting ? 0.5 : 1 } }
>
{ todo . title }
{ isDeleting && " (deleting...)" }
</ li >
);
}) }
</ ul >
);
}
function PendingMessages () {
const fetchers = useFetchers ();
const pendingMessages = fetchers
. filter (
( f ) =>
f . formAction === "/messages" &&
f . formData != null
)
. map (( f ) => ({
id: f . key ,
text: f . formData . get ( "message" ),
pending: true ,
}));
if ( pendingMessages . length === 0 ) return null ;
return (
< div className = "pending-messages" >
< h3 > Sending... </ h3 >
< ul >
{ pendingMessages . map (( msg ) => (
< li key = { msg . id } className = "pending" >
{ msg . text }
</ li >
)) }
</ ul >
</ div >
);
}
Count active requests
function RequestCounter () {
const fetchers = useFetchers ();
const activeCount = fetchers . filter (
( f ) => f . state === "loading" || f . state === "submitting"
). length ;
if ( activeCount === 0 ) return null ;
return < div > { activeCount } active requests </ div > ;
}
Common Patterns
function ShoppingCart () {
const { cart } = useLoaderData ();
const fetchers = useFetchers ();
// Combine real items with optimistic additions
const itemsBeingAdded = fetchers
. filter (( f ) => f . formAction ?. includes ( "/cart/add" ))
. map (( f ) => ({
id: `temp- ${ f . key } ` ,
productId: f . formData ?. get ( "productId" ),
pending: true ,
}));
const allItems = [ ... cart . items , ... itemsBeingAdded ];
return (
< div >
< h2 > Cart ( { allItems . length } ) </ h2 >
< ul >
{ allItems . map (( item ) => (
< li key = { item . id } >
Product { item . productId }
{ item . pending && " (adding...)" }
</ li >
)) }
</ ul >
</ div >
);
}
function GlobalErrors () {
const fetchers = useFetchers ();
const errors = fetchers
. filter (( f ) => f . data ?. error )
. map (( f ) => ({
key: f . key ,
error: f . data . error ,
}));
if ( errors . length === 0 ) return null ;
return (
< div className = "error-banner" >
{ errors . map (({ key , error }) => (
< div key = { key } className = "error" >
{ error }
</ div >
)) }
</ div >
);
}
function SubmitButton () {
const fetchers = useFetchers ();
const isSubmitting = fetchers . some (
( f ) => f . state === "submitting"
);
return (
< button disabled = { isSubmitting } >
{ isSubmitting ? "Saving..." : "Save All" }
</ button >
);
}
Track upload progress
function UploadManager () {
const fetchers = useFetchers ();
const uploads = fetchers
. filter (( f ) => f . formAction === "/upload" )
. map (( f ) => ({
key: f . key ,
filename: f . formData ?. get ( "file" )?. name ,
state: f . state ,
}));
if ( uploads . length === 0 ) return null ;
return (
< div className = "upload-manager" >
< h3 > Uploading { uploads . length } files </ h3 >
< ul >
{ uploads . map (( upload ) => (
< li key = { upload . key } >
{ upload . filename } - { upload . state }
</ li >
)) }
</ ul >
</ div >
);
}
Optimistic list updates
function TodoApp () {
const { todos } = useLoaderData ();
const fetchers = useFetchers ();
// Get optimistic additions
const optimisticTodos = fetchers
. filter (
( f ) =>
f . formAction === "/todos/new" &&
f . formData != null
)
. map (( f ) => ({
id: `temp- ${ f . key } ` ,
title: f . formData . get ( "title" ),
pending: true ,
}));
// Get IDs being deleted
const deletingIds = fetchers
. filter (( f ) => f . formData ?. get ( "intent" ) === "delete" )
. map (( f ) => f . formData ?. get ( "id" ));
// Combine real and optimistic, filter out deleting
const allTodos = [ ... todos , ... optimisticTodos ]
. filter (( todo ) => ! deletingIds . includes ( todo . id ))
. sort (( a , b ) => a . pending ? - 1 : b . pending ? 1 : 0 );
return (
< ul >
{ allTodos . map (( todo ) => (
< li key = { todo . id } >
{ todo . title }
{ todo . pending && " (pending...)" }
</ li >
)) }
</ ul >
);
}
Type Safety
interface CartItem {
productId : string ;
quantity : number ;
}
function Component () {
const fetchers = useFetchers ();
// Filter and type fetchers
const cartFetchers = fetchers . filter (
( f ) : f is typeof f & { formData : FormData } =>
f . formAction === "/cart/add" && f . formData != null
);
const addingItems = cartFetchers . map (( f ) => ({
productId: f . formData . get ( "productId" ) as string ,
quantity: Number ( f . formData . get ( "quantity" )),
}));
}
Important Notes
Only in-flight fetchers
useFetchers only returns fetchers that are currently active (not idle with no data). Once a fetcher completes and moves to idle state, it will still appear in the array if it has data.
Key uniqueness
Each fetcher has a unique key that you can use to identify it:
const fetchers = useFetchers ();
fetchers . forEach (( fetcher ) => {
console . log ( fetcher . key ); // Unique identifier
});
For large numbers of fetchers, consider memoizing filtered results:
import { useMemo } from "react" ;
import { useFetchers } from "react-router" ;
function Component () {
const fetchers = useFetchers ();
const cartFetchers = useMemo (
() => fetchers . filter (( f ) => f . formAction === "/cart/add" ),
[ fetchers ]
);
// Use cartFetchers...
}