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.
Concurrency Patterns
React Router automatically manages concurrent requests, ensuring your UI stays responsive and displays the most recent data.
Browser Behavior
React Router mirrors browser behavior for navigation:
Link Clicks: When you click a link and then click another before the first loads, the browser cancels the first request.
Form Submissions: When you submit a form and then submit another, the browser cancels the first submission.
React Router implements the same behavior for client-side navigation.
Automatic Cancellation
Interrupted Navigation
Requests are automatically cancelled when interrupted:
// User clicks Link A
<Link to="/page-a">Page A</Link>
// → Starts loading /page-a
// User quickly clicks Link B (before A finishes)
<Link to="/page-b">Page B</Link>
// → Cancels /page-a request
// → Starts loading /page-b
Interrupted Submissions
Form submissions cancel previous submissions:
// User submits Form 1
<Form method="post" action="/update">
{/* ... */}
</Form>
// → POST to /update starts
// User quickly submits Form 2
<Form method="post" action="/create">
{/* ... */}
</Form>
// → Cancels Form 1's POST
// → Starts Form 2's POST
Concurrent Fetchers
Unlike navigation, fetchers can run simultaneously:
import { useFetcher } from "react-router";
export function Component() {
const fetcher1 = useFetcher();
const fetcher2 = useFetcher();
const fetcher3 = useFetcher();
return (
<div>
{/* All three can be loading at once */}
<fetcher1.Form method="post" action="/action1">
<button type="submit">Action 1</button>
</fetcher1.Form>
<fetcher2.Form method="post" action="/action2">
<button type="submit">Action 2</button>
</fetcher2.Form>
<fetcher3.Form method="post" action="/action3">
<button type="submit">Action 3</button>
</fetcher3.Form>
</div>
);
}
Revalidation
After any action completes, React Router revalidates all page data:
Using this key:
| Submission begins
✓ Action complete, revalidation begins
✅ Revalidation complete, data committed to UI
❌ Request cancelled
submission 1: |----✓-----✅
submission 2: |-----✓-----✅
submission 3: |-----✓-----✅
Each submission triggers its own revalidation, and they can overlap.
Stale Data Prevention
React Router prevents stale data from appearing:
submission 1: |----✓---------❌
submission 2: |-----✓-----✅
submission 3: |-----✓-----✅
If submission 2’s revalidation completes before submission 1’s, the data from submission 1 is discarded as stale.
The rule: Data from requests that started later takes precedence.
Concurrent Loaders
Loaders run in parallel by default:
const routes = [
{
path: "/",
loader: rootLoader, // ← runs in parallel
children: [
{
path: "dashboard",
loader: dashboardLoader, // ← runs in parallel
},
],
},
];
// When navigating to /dashboard:
// Both rootLoader and dashboardLoader start immediately
Sequential Loading
Use data strategy for sequential loading:
const router = createBrowserRouter(routes, {
async dataStrategy({ matches }) {
// Wait for each loader sequentially
const results = [];
for (const match of matches) {
results.push(await match.resolve());
}
return results;
},
});
Type-Ahead Search
Automatic concurrency management for search:
export async function loader({ request }) {
const url = new URL(request.url);
const query = url.searchParams.get("q");
return searchCities(query);
}
export function CitySearch() {
const fetcher = useFetcher();
return (
<fetcher.Form action="/city-search">
<input
name="q"
onChange={(e) => {
// Submit on every keystroke
fetcher.submit(e.target.form);
}}
/>
{/* Always shows results for latest query */}
{fetcher.data?.map((city) => (
<div key={city.id}>{city.name}</div>
))}
</fetcher.Form>
);
}
How it works:
- User types “New”
- Request 1:
?q=N
- User types “e” (before request 1 completes)
- Request 1 cancelled, Request 2:
?q=Ne
- User types “w”
- Request 2 cancelled, Request 3:
?q=New
- Only Request 3’s results are shown
Debouncing
Reduce request frequency:
import { useFetcher } from "react-router";
import { useDebouncedCallback } from "use-debounce";
export function SearchBox() {
const fetcher = useFetcher();
const search = useDebouncedCallback(
(form) => fetcher.submit(form),
300
);
return (
<fetcher.Form action="/search">
<input
name="q"
onChange={(e) => search(e.target.form)}
/>
</fetcher.Form>
);
}
Throttling
Limit request rate:
import { useThrottledCallback } from "use-debounce";
export function InfiniteScroll() {
const fetcher = useFetcher();
const loadMore = useThrottledCallback(
() => fetcher.load("/api/next-page"),
1000,
{ trailing: false }
);
useEffect(() => {
const handleScroll = () => {
if (window.scrollY + window.innerHeight >= document.height) {
loadMore();
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return <div>{/* content */}</div>;
}
Race Condition Prevention
Fetchers prevent UI bugs from race conditions:
export function TodoList() {
const fetcher = useFetcher();
const [optimisticFilter, setOptimisticFilter] = useState("all");
const handleFilterChange = (filter) => {
// Set optimistic state
setOptimisticFilter(filter);
// Submit to server
fetcher.submit(
{ filter },
{ method: "get", action: "/todos" }
);
};
// fetcher.data always matches the latest request
// Even if requests resolve out of order
return (
<div>
<FilterButtons
value={optimisticFilter}
onChange={handleFilterChange}
/>
<TodoItems items={fetcher.data ?? []} />
</div>
);
}
Request Coordination
Coordinate multiple dependent requests:
export function Dashboard() {
const userFetcher = useFetcher();
const settingsFetcher = useFetcher();
useEffect(() => {
// Load user first
userFetcher.load("/api/user");
}, []);
useEffect(() => {
// Load settings after user loads
if (userFetcher.data?.id) {
settingsFetcher.load(`/api/settings/${userFetcher.data.id}`);
}
}, [userFetcher.data?.id]);
return <div>{/* ... */}</div>;
}
Batch Requests
Batch multiple requests into one:
const router = createBrowserRouter(routes, {
async dataStrategy({ matches }) {
// Collect all route IDs
const routeIds = matches.map(m => m.route.id);
// Single fetch for all data
const response = await fetch(
`/api/batch?routes=${routeIds.join(",")}`
);
const batchData = await response.json();
// Map data back to routes
return matches.map((match) => ({
type: "data",
result: batchData[match.route.id],
}));
},
});
Optimistic UI
Show immediate feedback during concurrent requests:
export function LikeButton({ postId, initialLikes }) {
const fetcher = useFetcher();
// Calculate optimistic state
const likes = fetcher.formData
? initialLikes + 1
: fetcher.data?.likes ?? initialLikes;
const isLiking = fetcher.state !== "idle";
return (
<fetcher.Form method="post" action="/like">
<input type="hidden" name="postId" value={postId} />
<button type="submit" disabled={isLiking}>
♥ {likes}
</button>
</fetcher.Form>
);
}
Polling
Poll for updates without blocking UI:
export function LiveData() {
const fetcher = useFetcher();
useEffect(() => {
const interval = setInterval(() => {
if (fetcher.state === "idle") {
fetcher.load("/api/live-data");
}
}, 5000);
return () => clearInterval(interval);
}, [fetcher]);
return <div>{fetcher.data?.value}</div>;
}
Request Deduplication
Prevent duplicate concurrent requests:
const requestCache = new Map();
export async function loader({ request }) {
const url = request.url;
if (requestCache.has(url)) {
return requestCache.get(url);
}
const promise = fetch(url).then(r => r.json());
requestCache.set(url, promise);
try {
return await promise;
} finally {
requestCache.delete(url);
}
}
Best Practices
- Trust automatic cancellation: Don’t manually track requests
- Use fetchers for concurrent operations: Navigation is singleton
- Debounce rapid inputs: Reduce server load
- Show loading states: Indicate pending requests
- Handle optimistic failures: Revert on error
Monitoring Concurrency
Track concurrent request metrics:
import { useNavigation, useFetchers } from "react-router";
export function RequestMonitor() {
const navigation = useNavigation();
const fetchers = useFetchers();
const activeRequests = [
navigation.state !== "idle" ? "navigation" : null,
...fetchers
.filter(f => f.state !== "idle")
.map((_, i) => `fetcher-${i}`)
].filter(Boolean);
return (
<div>
Active requests: {activeRequests.length}
{activeRequests.map(id => (
<div key={id}>{id}</div>
))}
</div>
);
}