DEV Community

Atilla Baspinar
Atilla Baspinar

Posted on

14 - React Router: actions, forms, and submission

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');
}
Enter fullscreen mode Exit fullscreen mode

The route can then declare the action:

const router = createBrowserRouter([
  {
    path: '/products/:productId',
    Component: Product,
    action: productAction,
    errorElement: <ProductError />,
  },
]);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Possible values usually include:

  • idle
  • loading
  • submitting

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>}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

The fetcher keeps its own state independent from the main route navigation:

const { data, state } = fetcher;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
  • Form automatically submits to the closest route action
  • useSubmit submits data programmatically
  • useNavigation tracks idle, loading, and submitting
  • useActionData returns the value from the action
  • useFetcher submits data without triggering a route change
  • Await is 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)