Routes can define an action in addition to a loader. Actions are used for form submissions, mutations, and server-like updates.
1. Defining a route action
import { redirect } from 'react-router';
export async function productAction({ request, params }: ActionFunctionArgs) {
const formData = await request.formData();
const productId = params.productId;
const name = formData.get('name');
if (!name || typeof name !== 'string') {
return { error: 'Product name is required.' };
}
await saveProduct(productId, name);
return redirect('/products');
}
The route can then declare the action:
const router = createBrowserRouter([
{
path: '/products/:productId',
Component: Product,
action: productAction,
errorElement: <ProductError />,
},
]);
The action runs when a form submits to that route. This is the React Router equivalent of handling a POST request in a route.
2. Form element
React Router provides a special Form component that automatically submits to the closest route action.
import { Form } from 'react-router';
export function ProductForm() {
return (
<Form method="post">
<input name="name" type="text" />
<button type="submit">Save</button>
</Form>
);
}
This is the simplest approach for most forms because:
- no custom submit handler is required
- the browser form submission is converted to a route action
- the form data is available inside the action via
request.formData()
If the action returns redirect(...), the route will navigate after the form is processed.
3. useSubmit
If you do not want to use <Form>, you can submit programmatically with useSubmit.
import { useSubmit } from 'react-router';
export function DeleteButton({ productId }: { productId: string }) {
const submit = useSubmit();
const handleDelete = () => {
submit(
{ productId },
{ method: 'delete' }
);
};
return <button onClick={handleDelete}>Delete</button>;
}
This is useful for buttons, custom UI, or programmatic mutation flows. The submission still goes through the route action, but you are controlling when it happens.
4. useNavigation
useNavigation works with both loaders and actions. It gives you the current navigation state and can be used to disable buttons or show a loading indicator.
import { useNavigation } from 'react-router';
export function SaveButton() {
const navigation = useNavigation();
return (
<button disabled={navigation.state === 'submitting'}>
{navigation.state === 'submitting' ? 'Saving...' : 'Save'}
</button>
);
}
Possible values usually include:
idleloadingsubmitting
This is especially useful for giving immediate visual feedback while a form is being submitted.
5. useActionData
useActionData returns the value returned from the action function. It is similar in spirit to useLoaderData but for actions.
import { useActionData } from 'react-router';
export function ProductForm() {
const actionData = useActionData() as { error?: string } | undefined;
return (
<>
<Form method="post">
<input name="name" type="text" />
<button type="submit">Save</button>
</Form>
{actionData?.error && <p>{actionData.error}</p>}
</>
);
}
This pattern is commonly used for validation errors or server-side feedback after a form submission.
6. useFetcher
useFetcher is useful when you want to submit or load data without causing a full route navigation.
import { useFetcher } from 'react-router';
export function ProductSearch() {
const fetcher = useFetcher();
return (
<fetcher.Form method="get" action="/products/search">
<input name="q" type="text" />
<button type="submit">Search</button>
</fetcher.Form>
);
}
The fetcher keeps its own state independent from the main route navigation:
const { data, state } = fetcher;
This means a form can submit in the background while the page stays where it is. This is useful for search, autosave panels, or non-navigation-related updates.
7. Await
React Router still supports the Await component for rendering asynchronous data in a Suspense-friendly way.
import { Await, Suspense } from 'react-router';
export function ProductList({ productsPromise }: { productsPromise: Promise<Product[]> }) {
return (
<Suspense fallback={<div>Loading products...</div>}>
<Await resolve={productsPromise}>
{(products) => <ProductGrid items={products} />}
</Await>
</Suspense>
);
}
This is useful for asynchronous route data or deferred rendering patterns. In modern React Router, a more common pattern is to use normal loaders and useLoaderData, but Await remains a valid tool for specific deferred-loading scenarios.
Summary
- route actions handle form submissions and mutations
-
Formautomatically submits to the closest route action -
useSubmitsubmits data programmatically -
useNavigationtracksidle,loading, andsubmitting -
useActionDatareturns the value from the action -
useFetchersubmits data without triggering a route change -
Awaitis still available for deferred async rendering
The general rule is simple: loaders fetch data, actions mutate data, and forms connect the two through route actions.
Top comments (0)