Це розділ, який відділяє новачків від професіоналів. До версії 6.4 React Router займався лише UI. Тепер він бере на себе керування даними.
Ми переходимо від пасивного "відображення сторінок" до активного керування життєвим циклом даних.
Традиційний спосіб завантаження даних в 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>;
}
Чому це погано?
fetch, а у дитини свій — дитина не почне завантаження, поки батько не отримає дані і не відрендерить її.React Router v6.4 пропонує патерн "Fetch-then-Render". Ми кажемо роутеру: "Перш ніж показати цей маршрут, піди і візьми ось ці дані. Не малюй компонент, поки дані не будуть готові".
loaderLoader — це звичайна асинхронна функція. Вона НЕ є хуком, тому тут не можна використовувати 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}`);
}
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 />,
}
<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>
Як це працює:
name).action функцію, прив'язану до маршруту в action пропсі форми (або поточного маршруту).action, передаючи туди об'єкт request.actionAction викликається автоматично при сабміті форми.
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");
}
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>
);
}
Нативні <Form> від React Router ідеальні для простих мутацій (видалення, прості форми), але для складних форм з валідацією "на льоту" ми використовуємо React Hook Form (RHF).
Як їх подружити?
useSubmit від React Router.Ми використовуємо RHF для збору даних, а потім передаємо їх в React Router через submit().
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');
};
action).Це магія, про яку не можна мовчати.
Після того, як action успішно завершився (і зробив редірект або просто повернув дані):
Вам більше не потрібні refetchQueries (як в React Query) або ручне оновлення контексту. Ви просто робите дію, а сторінка "самозцілюється".
| Інструмент | Анатомія / Роль |
|---|---|
| Loader | ({ params, request }) => Data. Запускається ДО рендеру компонента. |
| Action | `({ params, request }) => Response |
| useLoaderData | Хук для отримання результату Loader-а. |
| useActionData | Хук для отримання помилок або даних, які повернув Action. |
| useSubmit | "Міст" для програмного запуску Action (ідеально для React Hook Form). |
Цей підхід перетворює React Router з простого "перемикача сторінок" на потужний State Management інструмент для серверних даних.
Динамічні Маршрути та Параметри
До цього моменту ми працювали з фіксованими URL: /about, /contact. Але реальні додатки працюють з динамічними даними: користувачі (/user/123), товари (/products/iphone-15) або замовлення (/order/abc-999).
Просунуті Патерни
Ми вже вміємо будувати повноцінні додатки. Але що робити зі складними сценаріями? Авторизація, оптимізація повільних запитів, інтерактивність без переходу на нову сторінку.