React Router cancels previous loader requests on new navigation
When a user clicks a link while a previous navigation is still in progress, React Router cancels the loader fetch requests from the first navigation and only proceeds with the latest navigation's requests. This mirrors browser behavior where the most recent action is prioritized.
React Router cancels original form submission when new submission interrupts
If a form is submitted and another submission occurs before the first completes, React Router cancels the original fetch requests and waits for the latest submission to complete before triggering revalidation. This mirrors how browsers handle multiple form submissions.
useFetcher allows multiple concurrent requests unlike navigation
Unlike navigation and form submissions which are limited to one request at a time, useFetcher allows multiple requests to be in flight simultaneously.
React Router commits data immediately when one submission completes among concurrent submissions
When multiple form submissions are in progress and one completes, React Router updates the UI with that data immediately without waiting for the other submissions to finish. This keeps the UI responsive. As remaining submissions complete, the UI continues to update with the latest data.
React Router discards earlier revalidation data if later submission completes first
If a later form submission's revalidation completes before an earlier one, React Router cancels and discards the earlier submission's revalidation data. Only the most recent data is committed to the UI, since it was requested later and is more likely to reflect the newest state.
Stale data can rarely appear when interrupted requests reach server after new submission completes
In rare edge cases with unusual network conditions, a canceled request may still reach the server after a subsequent submission and its revalidation have completed. Browser cancellation only releases browser resources but does not prevent the request from reaching the server. Users may briefly see data that differs from the server state. Timestamps in form submissions with server-side staleness checks can mitigate this issue.
Revalidation occurs after form submissions
After a form submission, React Router automatically fetches fresh data from the loaders associated with the route. This process is called revalidation.
useFetcher with Form example for rapid input-triggered requests
In a city search combobox component, keystrokes can trigger rapid network requests. Using useFetcher.Form with onChange handlers on input fields automatically manages these concurrent requests, ensuring displayed results match the most recent query without manual race condition handling:
```tsx
export async function loader({ request }) {
const { searchParams } = new URL(request.url);
const cities = await searchCities(searchParams.get("q"));
return cities;
}
export function CitySearchCombobox() {
const fetcher = useFetcher<typeof loader>();
return (
<fetcher.Form action="/city-search">
<Combobox aria-label="Cities">
<ComboboxInput
name="q"
onChange={(event) =>
fetcher.submit(event.target.form)
}
/>
{fetcher.data ? (
<ComboboxPopover className="shadow-popup">
{fetcher.data.length > 0 ? (
<ComboboxList>
{fetcher.data.map((city) => (
<ComboboxOption
key={city.id}
value={city.name}
/>
))}
</ComboboxList>
) : (
<span>No results found</span>
)}
</ComboboxPopover>
) : null}
</Combobox>
</fetcher.Form>
);
}
```
React Router automatic race condition handling
React Router automatically handles the most common race conditions found in web user interfaces. It is impossible to eliminate every possible race condition in an application.
Browser-inspired network concurrency handling
React Router's handling of network concurrency is heavily inspired by browser behavior when processing documents. When a user clicks a link to a new document and then clicks a different link before the new page finishes loading, the browser cancels the first request and immediately processes the new navigation. The same behavior applies to form submissions: when a pending form submission is interrupted by a new one, the first is canceled and the new submission is immediately processed.
Interrupted navigations cancel in-flight requests
Like the browser, interrupted navigations with links and form submissions will cancel in-flight data requests and immediately process the new event.
Fetcher self-interruption behavior
Fetchers cannot interrupt other fetcher instances, but they can interrupt themselves. When a fetcher is interrupted by a new submission on the same fetcher instance, the first request is canceled and the new one is immediately processed.
Fetcher revalidation and stale request handling
After a fetcher's action request returns to the browser, a revalidation for all page data is sent. This means multiple revalidation requests can be in-flight at the same time. React Router commits all fresh revalidation responses and cancels any stale requests. A stale request is any request that started earlier than one that has returned.
Backend race conditions remain outside React Router scope
Since networks are unpredictable and the server still processes canceled requests, the backend may still experience race conditions and have potential data integrity issues. These risks are the same as using default browser behavior with plain HTML forms, which are considered low risk and outside the scope of React Router.
Type-ahead combobox race condition prevention
When building a type-ahead combobox, as the user types and sends new requests to the server with each character, it is important to not show results for a value that is no longer in the text field. When using a fetcher, this is automatically managed: calls to fetcher.submit will cancel pending requests on that fetcher automatically, ensuring results for a different input value are never shown to the user.
Type-ahead combobox implementation example
Route loader for city search:
```tsx
export async function loader({ request }) {
const { searchParams } = new URL(request.url);
return searchCities(searchParams.get("q"));
}
```
Component with automatic race condition handling:
```tsx
export function CitySearchCombobox() {
const fetcher = useFetcher();
return (
<fetcher.Form action="/city-search">
<Combobox aria-label="Cities">
<ComboboxInput
name="q"
onChange={(event) =>
fetcher.submit(event.target.form)
}
/>
{fetcher.data ? (
<ComboboxPopover className="shadow-popup">
{fetcher.data.length > 0 ? (
<ComboboxList>
{fetcher.data.map((city) => (
<ComboboxOption
key={city.id}
value={city.name}
/>
))}
</ComboboxList>
) : (
<span>No results found</span>
)}
</ComboboxPopover>
) : null}
</Combobox>
</fetcher.Form>
);
}
```
React Transitions in React Router overview
React 18 introduced transitions to differentiate urgent from non-urgent UI updates. React 19 enhances this with Actions and support for async functions in Transitions, plus the useOptimistic hook for showing instant feedback during Transitions. React Router introduces a useTransitions flag to handle the tension between new React features (pending states, optimistic UI) and existing Router patterns.
React Router startTransition default behavior
Since React Router v7, all router state updates are wrapped in React.startTransition by default. In React Router 6.13.0, React.startTransition was introduced behind the future.v7_startTransition flag to make React Router more Suspense-friendly.
useSyncExternalStore and startTransition conflict
React.useSyncExternalStore updates cannot be Transitions. useSyncExternalStore forces a sync update, which means fallbacks can be shown in update transitions that would otherwise avoid showing the fallback. This is one reason to opt-out of using startTransition for router state updates.
Opt-out of Transitions with useTransitions=false
Set useTransitions={false} on router components to opt-out of wrapping internal state updates in startTransition. This is used for applications not 'Transition-friendly' due to useSyncExternalStore or other reasons. Syntax for Framework Mode: <HydratedRouter useTransitions={false} />. For Data Mode: <RouterProvider useTransitions={false} />. For Declarative Mode: <BrowserRouter useTransitions={false} />.
Opt-in to Transitions with useTransitions=true
Set useTransitions={true} (or just useTransitions) on router components to opt-in to enhanced Transition behavior with React 19. This requires React 19 because it needs access to React.useOptimistic. Syntax for Framework Mode: <HydratedRouter useTransitions />. For Data Mode: <RouterProvider useTransitions />. For Declarative Mode: <BrowserRouter useTransitions />.
Transition-enabled navigation with useTransitions=true
When useTransitions=true is enabled, all internal state updates are wrapped in React.startTransition. All <Link> and <Form> navigations are wrapped in React.startTransition using the promise returned by useNavigate/useSubmit so the Transition lasts for the entire navigation duration. useNavigate/useSubmit do not automatically wrap in React.startTransition, allowing opt-out by using them directly.
useOptimistic state surfacing with useTransitions=true
When useTransitions=true is enabled in Framework/Data modes, router state updates during navigation are surfaced via useOptimistic. State related to ongoing navigation and fetchers is surfaced: state.navigation (useNavigation), state.revalidation (useRevalidator), state.actionData (useActionData), and state.fetchers (useFetcher/useFetchers). State related to current location is not surfaced: state.location (useLocation), state.matches (useMatches), state.loaderData (useLoaderData), state.errors (useRouteError).
Automatic vs manual Transition wrapping in React Router
When useTransitions=true, <Link> and <Form> are automatically wrapped in async Transitions. For useNavigate, useSubmit, and fetcher methods, you must manually wrap in startTransition. Examples of automatically Transition-enabled: <Link to="/path" />, <Form method="post" action="/path" />. Examples of manually Transition-enabled: startTransition(() => navigate("/path")), startTransition(() => submit(data, { method: 'post', action: "/path" })), startTransition(() => fetcher.load("/path")), startTransition(() => fetcher.submit(data, { method: "post", action: "/path" })).
Promise return/await requirement in startTransition
When wrapping navigate or submit in startTransition, you must always return or await the promise so that the Transition encompasses the full duration of the navigation. If you forget to return or await the promise, the Transition will end prematurely and things won't work as expected. Correct: startTransition(() => navigate("/path")) or startTransition(async () => { await navigate("/path") }). Incorrect: startTransition(() => { navigate("/path") }) or startTransition(async () => { navigate("/path") }).
popstate navigation optimistic state bug
There is a bug with optimistic states during popstate (back) navigations. If reading the current route during a back navigation that suspends on uncached data, set the optimistic state before navigating back or defer the optimistic update in a timer or microtask.
Future default behavior for useTransitions
React Router v8 plans to make the opt-in useTransitions behavior (useTransitions=true) the default, but will likely retain the opt-out flag for use cases such as useSyncExternalStore.
Pre-rendering concurrency for parallel processing
By default, pages are pre-rendered one path at a time. You can enable concurrency to pre-render multiple paths in parallel to speed up build times. To specify concurrency, move the prerender config into a prerender.paths field and set prerender.concurrency to a number like 4.