React Router: Навігаційна система сучасного вебу

Data APIs: Loaders та Actions

Це розділ, який відділяє новачків від професіоналів. До версії 6.4 React Router займався лише UI. Тепер він бере на себе керування даними.

Data APIs: Loaders та Actions

Це розділ, який відділяє новачків від професіоналів. До версії 6.4 React Router займався лише UI. Тепер він бере на себе керування даними.

Ми переходимо від пасивного "відображення сторінок" до активного керування життєвим циклом даних.

1. Проблема: Fetch-on-Render (Водоспади)

Традиційний спосіб завантаження даних в React має суттєві архітектурні вади.

// ❌ Старий спосіб (Fetch-on-Render)
function ProductPage() {
    const [product, setProduct] = useState(null);
    const { id } = useParams();

    useEffect(() => {
        // Починаємо завантаження ТІЛЬКИ ПІСЛЯ того, як компонент відрендерився
        fetch(`/api/products/${id}`)
            .then(res => res.json())
            .then(setProduct);
    }, [id]);

    if (!product) return <Spinner />;
    return <div>{product.name}</div>;
}

Чому це погано?

  1. Затримка (Network Chasm): Браузер завантажує JS -> React рендерить пустий компонент -> Спрацьовує ефект -> Йде запит. Ми втрачаємо дорогоцінні мілісекунди.
  2. Водоспади (Waterfalls): Якщо у батька свій fetch, а у дитини свій — дитина не почне завантаження, поки батько не отримає дані і не відрендерить її.

2. Рішення: Loaders (Завантажувачі)

React Router v6.4 пропонує патерн "Fetch-then-Render". Ми кажемо роутеру: "Перш ніж показати цей маршрут, піди і візьми ось ці дані. Не малюй компонент, поки дані не будуть готові".

Анатомія loader

Loader — це звичайна асинхронна функція. Вона НЕ є хуком, тому тут не можна використовувати useParams чи useContext. Натомість, роутер передає контекст через аргументи.

// Анатомія функції loader
export async function loader({ params, request }) {
  // 1. params: Об'єкт з динамічними сегментами URL (наприклад, :productId)
  const id = params.productId;

  // 2. request: Стандартний Web Request об'єкт.
  // Дозволяє читати заголовки, cookies, URL запиту.
  const url = new URL(request.url);
  const searchTerm = url.searchParams.get("q");

  // 3. Виконання запиту
  // Важливо: ми повертаємо Promise. Роутер сам чекає його виконання (await).
  // Response об'єкт від fetch можна повертати напряму!
  return fetch(`/api/products/${id}?q=${searchTerm}`);
}

Реалізація в коді

src/pages/ProductDetailsPage.jsx
import { useLoaderData } from "react-router-dom";

// 1. Компонент тепер "тупий" і чистий. Він просто показує дані.
export default function ProductDetailsPage() {
  const product = useLoaderData(); // Хук для доступу до даних з loader-а
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}$</p>
    </div>
  );
}

// 2. Експортуємо лоадер для використання в роутері
export const productLoader = async ({ params }) => {
  const res = await fetch(`/api/products/${params.productId}`);
  
  if (!res.ok) {
    // Викидаємо помилку, яку спіймає <ErrorPage> (errorElement)
    throw new Response("Not Found", { status: 404 });
  }
  
  return res.json();
};

Підключення в main.jsx залишається простим:

{
  path: "products/:productId",
  element: <ProductDetailsPage />,
  loader: productLoader, // <--- Зв'язок
  errorElement: <ErrorPage />,
}

3. Мутації даних: Actions та компонент <Form>

Якщо loader — це читання (GET), то action — це запис (POST, PUT, DELETE).

React Router переосмислює роботу з формами, повертаючись до витоків вебу, але з потужністю SPA. Замість того, щоб вручну писати onSubmit, e.preventDefault(), та fetch, ми використовуємо декларативний компонент <Form>.

Анатомія компонента <Form>

Компонент <Form> — це розумна обгортка над стандартним HTML <form>.

<Form 
  method="post"    // Метод: post, put, patch, delete (get за замовчуванням)
  action="/search" // Куди відправити дані (шлях до маршруту з відповідним action)
  replace          // Замінити поточний запис в історії браузера замість додавання нового
>
  <input name="query" />
  <button type="submit">Шукати</button>
</Form>

Як це працює:

  1. Коли користувач натискає "Submit", React Router перехоплює подію.
  2. Він збирає всі дані з полів введення (використовуючи атрибут name).
  3. Він шукає action функцію, прив'язану до маршруту в action пропсі форми (або поточного маршруту).
  4. Він викликає цей action, передаючи туди об'єкт request.

Анатомія функції action

Action викликається автоматично при сабміті форми.

export async function action({ request, params }) {
  // 1. Отримання даних через стандартний FormData API
  const formData = await request.formData();
  
  // Читаємо значення за атрибутом 'name' інпутів
  const title = formData.get("title"); 

  // 2. Виконання логіки (запит на сервер)
  const res = await fetch('/api/items', {
    method: 'POST',
    body: JSON.stringify({ title }),
  });

  // 3. Результат
  // Можна повернути помилки, об'єкт або зробити редірект
  if (!res.ok) return { error: "Не вдалося зберегти" };
  
  return redirect("/dashboard");
}

4. Стан завантаження: useNavigation

Коли ми використовуємо <Form>, браузер не перезавантажується, тому користувач може не зрозуміти, що запит пішов. Для цього нам потрібен хук useNavigation.

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

function CreatePost() {
  const navigation = useNavigation();
  
  // navigation.state може бути:
  // "idle" — нічого не відбувається
  // "submitting" — зараз виконується action
  // "loading" — action завершився, і зараз перезавантажуються лоадери
  
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input name="title" disabled={isSubmitting} />
      <button type="submit">
        {isSubmitting ? "Збереження..." : "Створити"}
      </button>
    </Form>
  );
}

5. Інтеграція з React Hook Form

Нативні <Form> від React Router ідеальні для простих мутацій (видалення, прості форми), але для складних форм з валідацією "на льоту" ми використовуємо React Hook Form (RHF).

Як їх подружити?

  • RHF: Відповідає за UI, валідацію полів, відображення помилок під час введення.
  • React Router Action: Відповідає за відправку даних на сервер і перехід.
  • Міст: Хук useSubmit від React Router.

Патерн "Smart Bridge"

Ми використовуємо RHF для збору даних, а потім передаємо їх в React Router через submit().

src/pages/NewProductPage.jsx
import { useForm } from "react-hook-form";
import { useSubmit, useActionData, redirect } from "react-router-dom";

export default function NewProductPage() {
  // 1. React Hook Form ініціалізація
  const { 
    register, 
    handleSubmit, 
    formState: { errors: clientErrors } // Помилки валідації на клієнті
  } = useForm();

  // 2. React Router хуки
  const submit = useSubmit(); // Функція для запуску action
  const serverErrors = useActionData(); // Помилки, що повернув action (якщо були)

  // 3. Обробник сабміту
  const onSubmit = (data) => {
    // data — це чистий JS об'єкт { title: "...", price: "..." }
    
    // Ми передаємо його в submit. 
    // Другим аргументом вказуємо метод і action (опціонально, якщо поточний URL)
    // encType: "application/json" дозволить прочитати дані через request.json() в action
    submit(data, { method: "post", encType: "application/json" });
  };

  return (
    <div className="form-container">
      <h1>Створити товар</h1>

      {/* RHF handleSubmit перевіряє валідацію перед викликом onSubmit */}
      <form onSubmit={handleSubmit(onSubmit)}>
        
        <div className="form-group">
          <label>Назва</label>
          <input 
            {...register("title", { required: "Це поле обов'язкове" })} 
            className={clientErrors.title ? "error" : ""}
          />
          {/* Показуємо помилку від RHF */}
          {clientErrors.title && <span className="error-msg">{clientErrors.title.message}</span>}
          {/* АБО помилку від сервера */}
          {serverErrors?.title && <span className="error-msg">{serverErrors.title}</span>}
        </div>

        <div className="form-group">
          <label>Ціна</label>
          <input 
            type="number"
            {...register("price", { min: { value: 1, message: "Ціна має бути > 0" } })} 
          />
           {clientErrors.price && <span className="error-msg">{clientErrors.price.message}</span>}
        </div>

        <button type="submit">Створити</button>
      </form>
    </div>
  );
}

// Action для обробки JSON даних
export const createProductAction = async ({ request }) => {
  // Оскільки ми вказали encType: "application/json", читаємо JSON
  const data = await request.json();
  
  // Тут також можна робити серверну валідацію
  if (data.title === "Заборонене слово") {
    return { title: "Ця назва недопустима" }; // Повернеться в useActionData
  }

  await fetch('/api/products', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  return redirect('/products');
};
Чому такий підхід кращий? Ви отримуєте миттєвий фідбек для користувача завдяки React Hook Form (UI не "тупить"), але зберігаєте архітектурну чистоту React Router (вся логіка запитів ізольована в action).

6. Автоматична Ревалідація

Це магія, про яку не можна мовчати. Після того, як action успішно завершився (і зробив редірект або просто повернув дані):

  1. React Router знає, що дані могли змінитися.
  2. Він автоматично перезапускає всі loaders, які зараз активні на сторінці.
  3. Ваш UI оновлюється свіжими даними з сервера.

Вам більше не потрібні refetchQueries (як в React Query) або ручне оновлення контексту. Ви просто робите дію, а сторінка "самозцілюється".

Резюме Data APIs

ІнструментАнатомія / Роль
Loader({ params, request }) => Data. Запускається ДО рендеру компонента.
Action`({ params, request }) => Response
useLoaderDataХук для отримання результату Loader-а.
useActionDataХук для отримання помилок або даних, які повернув Action.
useSubmit"Міст" для програмного запуску Action (ідеально для React Hook Form).

Цей підхід перетворює React Router з простого "перемикача сторінок" на потужний State Management інструмент для серверних даних.

Copyright © 2026