Обработка отправки форм через action

React Router начиная с версии 6.4 внедрил концепцию data routers, где маршруты могут содержать асинхронные функции для загрузки данных (loader) и обработки действий пользователей (action). Механизм action предназначен для обработки отправки форм и выполнения связанных операций на стороне маршрута, таких как сохранение данных или их валидация.

Создание маршрута с action

Каждый маршрут, который должен обрабатывать отправку формы, может иметь свойство action, которое принимает функцию:

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import FormPage from "./FormPage";
import { handleFormSubmit } from "./actions";

const router = createBrowserRouter([
  {
    path: "/form",
    element: <FormPage />,
    action: handleFormSubmit,
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

В данном примере при отправке формы с маршрута /form будет вызвана функция handleFormSubmit.

Структура функции action

Функция action принимает объект ActionFunctionArgs, который содержит:

  • request — объект запроса, предоставляющий метод formData(), json(), text() для чтения данных;
  • params — параметры маршрута, если путь содержит динамические сегменты;
  • context — опциональный объект контекста, переданный через роутер.

Пример функции action:

export async function handleFormSubmit({ request, params }) {
  const formData = await request.formData();
  const name = formData.get("name");
  const email = formData.get("email");

  if (!name || !email) {
    return { error: "Все поля обязательны" };
  }

  // Симуляция отправки данных на сервер
  await fetch("/api/submit", {
    method: "POST",
    body: JSON.stringify({ name, email }),
    headers: {
      "Content-Type": "application/json",
    },
  });

  return { success: true };
}

Использование Form компонента из React Router

React Router предоставляет компонент <Form>, который автоматически связывается с action маршрута и отправляет данные в соответствующую функцию:

import { Form, useActionData } from "react-router-dom";

export default function FormPage() {
  const actionData = useActionData();

  return (
    <div>
      <h2>Регистрация</h2>
      {actionData?.error && <p style={{ color: "red" }}>{actionData.error}</p>}
      {actionData?.success && <p style={{ color: "green" }}>Данные отправлены!</p>}
      <Form method="post">
        <label>
          Имя:
          <input type="text" name="name" />
        </label>
        <br />
        <label>
          Email:
          <input type="email" name="email" />
        </label>
        <br />
        <button type="submit">Отправить</button>
      </Form>
    </div>
  );
}

Ключевые моменты использования <Form>:

  • method может быть "post" или "get".
  • Форма автоматически вызывает action текущего маршрута.
  • Данные формы могут быть получены с помощью useActionData после обработки.

Валидация и возвращение ошибок

Функция action может возвращать данные, которые затем становятся доступными через useActionData. Это позволяет реализовать встроенную валидацию форм без дополнительного состояния:

export async function handleFormSubmit({ request }) {
  const formData = await request.formData();
  const password = formData.get("password");

  if (!password || password.length < 6) {
    return { error: "Пароль должен быть не менее 6 символов" };
  }

  return { success: true };
}

В компоненте формы это можно отобразить сразу после отправки:

{actionData?.error && <p className="error">{actionData.error}</p>}

Работа с редиректами после отправки

Для перенаправления после успешной отправки используется функция redirect из react-router-dom:

import { redirect } from "react-router-dom";

export async function handleFormSubmit({ request }) {
  const formData = await request.formData();
  const username = formData.get("username");

  if (!username) {
    return { error: "Введите имя пользователя" };
  }

  // Сохранение данных
  await fetch("/api/register", {
    method: "POST",
    body: JSON.stringify({ username }),
    headers: { "Content-Type": "application/json" },
  });

  return redirect("/success");
}

При этом <Form> автоматически перенаправит пользователя на указанный маршрут, если action возвращает результат redirect.

Асинхронная обработка и состояния загрузки

Для работы с состояниями загрузки после отправки формы используется хук useNavigation:

import { Form, useNavigation } from "react-router-dom";

export default function FormPage() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input type="text" name="name" />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Отправка..." : "Отправить"}
      </button>
    </Form>
  );
}

Таким образом, форма динамически реагирует на процесс отправки без дополнительной настройки состояния через useState.

Работа с динамическими маршрутами и action

Если маршрут содержит динамические параметры, они доступны в params функции action:

const router = createBrowserRouter([
  {
    path: "/users/:userId/edit",
    element: <UserEdit />,
    action: updateUser,
  },
]);

export async function updateUser({ request, params }) {
  const formData = await request.formData();
  const userId = params.userId;

  await fetch(`/api/users/${userId}`, {
    method: "PUT",
    body: JSON.stringify(Object.fromEntries(formData)),
    headers: { "Content-Type": "application/json" },
  });

  return redirect(`/users/${userId}`);
}

Советы по использованию action

  • Всегда использовать await request.formData() для чтения данных форм.
  • Для обработки ошибок или валидации возвращать объект с ключами ошибок, который можно сразу отобразить.
  • Для перенаправлений использовать встроенную функцию redirect.
  • Компонент <Form> автоматически синхронизируется с action, не требуется ручная отправка через fetch.
  • Динамические параметры маршрута удобно передавать через params.

Эта модель делает маршруты React Router самодостаточными: каждый маршрут может самостоятельно обрабатывать отправку форм, валидацию и редиректы, уменьшая количество логики в компонентах.