Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Маршрутизация и загрузка данных
routing

Маршрутизация и загрузка данных

React Router 8, вложенные маршруты, загрузчики, действия, интерфейс ожидания, границы ошибок и типизированные URL.

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

Маршрутизация и загрузка данных

Маршрут — это граница данных, ошибок, доступа и истории, а не просто условие по пути URL.

#Результат урока

Вы построите вложенное дерево маршрутов с загрузчиками и действиями, типизированными параметрами, состояниями ожидания и ошибки и отменяемой навигацией.

Понадобятся асинхронные границы, действия, формы и семантика URL. Итог урока — рабочее место с вложенными маршрутами, фильтрами в URL, изменением данных и восстановлением после ошибок.

#1. Дерево маршрутов и вложенная разметка

Вложенные маршруты отражают устойчивые границы разметки, данных и ошибок. Родитель отрисовывает Outlet для дочернего маршрута. Плоский список абсолютных путей дублирует каркас страницы и усложняет владение данными. Индексный маршрут (index route) представляет дочернюю страницу по умолчанию без нового сегмента URL.

Конфигурация читается как дерево владения: боковая панель и справочники живут у родителя, детали заявки — у потомка:

// src/routes.ts export default [ route('tickets', 'routes/tickets/layout.tsx', [ index('routes/tickets/index.tsx'), // /tickets — список по умолчанию route(':ticketId', 'routes/tickets/detail.tsx'), // /tickets/42 ]), ] satisfies RouteConfig;

Родительский элемент отрисовывает общий каркас один раз и оставляет место потомку:

// src/routes/tickets/layout.tsx export default function TicketsLayout() { const { queues } = useLoaderData<typeof loader>(); return ( <div className="tickets"> <QueueSidebar queues={queues} /> <Outlet /> {/* здесь появится index или detail */} </div> ); }

Без Outlet потомок просто не отрисуется: React Router не «вставляет» его автоматически, а ждёт явного места в разметке родителя.

Проверьте себя. Соберите раздел заявок с индексной страницей и дочерним маршрутом деталей.

Частая ошибка. Дублировать боковую панель во всех элементах маршрутов.

#2. Params как недоверенные строки

Параметры URL приходят строками или отсутствуют. UUID, число и перечисление проверяют до вызова предметного клиента. Генерация типов помогает с именами, но не доказывает корректность значения во время выполнения. Неверный идентификатор обычно превращают в ответ маршрута 400 или 404, а не в общий сбой.

Разбор параметра — это отдельный шаг между URL и предметным кодом, и у него два разных отрицательных исхода:

// src/routes/tickets/detail.ts export async function loader({ params }: Route.LoaderArgs) { const id = Number(params.ticketId); if (!Number.isInteger(id) || id <= 0) { // строка вообще не является идентификатором — это ошибка запроса throw new Response('Некорректный идентификатор заявки', { status: 400 }); } const ticket = await fetchTicket(id); if (ticket === null) { // идентификатор корректен, записи нет — это другой исход throw new Response('Заявка не найдена', { status: 404 }); } return { ticket }; }

Number('abc') даёт NaN, Number('') — ноль, а Number('1.5') — нецелое: одна проверка Number.isInteger закрывает все три случая. Различие 400 и 404 важно не только для семантики HTTP — граница ошибок покажет разные сообщения и разные действия пользователю.

Проверьте себя. Разделите неверный числовой идентификатор и отсутствующую запись на разные исходы.

Частая ошибка. Number(param) без проверки NaN/целого/диапазона.

#3. Loaders и параллельная загрузка

Загрузчик (loader) принадлежит маршруту и запускается при навигации. Данные родителя и потомка могут загружаться параллельно; не ждите независимые источники последовательно. Request содержит AbortSignal, который передают в fetch. Компонент читает готовый результат или отложенный ресурс.

Независимые справочники запускают одновременно, а сигнал отмены пробрасывают в каждый запрос:

// src/routes/tickets/layout.ts export async function loader({ request }: Route.LoaderArgs) { const [queues, operators] = await Promise.all([ fetchQueues({ signal: request.signal }), fetchOperators({ signal: request.signal }), ]); return { queues, operators }; }

Последовательный await на двух независимых запросах превратил бы 120 мс в 240 мс без всякой пользы. request.signal прерывается, когда пользователь уходит с маршрута до ответа, — иначе браузер продолжает держать соединение и применяет результат, который уже никому не нужен.

Проверьте себя. Запустите независимые справочники через Promise.all и передайте request.signal.

Частая ошибка. Повторить запрос загрузчика в эффекте компонента.

#4. Действия, формы и повторная загрузка

Действие маршрута (action) изменяет данные из формы или fetcher. После него маршрутизатор повторно запускает загрузчики, возвращая интерфейс к данным сервера. Ошибка ввода отличается от перенаправления, успеха и системного сбоя. Form сохраняет нативную семантику, а fetcher выполняет действие без навигации.

Действие получает FormData, проверяет её и возвращает либо ошибку для отображения, либо признак успеха:

// src/routes/tickets/detail.ts export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const status = formData.get('status'); if (status !== 'open' && status !== 'closed') { return { error: 'Недопустимый статус заявки' }; // ошибка ввода, не исключение } await updateTicketStatus(Number(params.ticketId), status); return { ok: true }; // загрузчики маршрута перезапустятся автоматически }

Возврат обычного объекта — это ошибка ввода, которую компонент покажет рядом с полем. throw new Response(...) — системный сбой, который уйдёт в границу ошибок. Ручного обновления данных на клиенте здесь нет: перезапуск загрузчиков возвращает интерфейс к состоянию сервера.

Проверьте себя. Измените статус через действие и подтвердите обновление данных загрузчика.

Частая ошибка. Изменять объект данных загрузчика в браузере без правила повторной загрузки или кеша.

#5. Состояние навигации в интерфейсе

Состояние навигации показывает загрузку, отправку и данные формы. Общий индикатор полезен при смене маршрута, локальный — для конкретной операции. Не заменяйте всю страницу индикатором при каждой навигации: сохраняйте разметку и, где безопасно, прежние данные.

fetcher знает, какие именно данные он отправляет, поэтому индикатор можно привязать к конкретной строке таблицы:

// src/features/tickets/TicketRow.tsx function TicketRow({ ticket }: { ticket: Ticket }) { const fetcher = useFetcher(); const submittedId = fetcher.formData?.get('ticketId'); const isSaving = fetcher.state !== 'idle' && submittedId === String(ticket.id); return ( <tr aria-busy={isSaving}> <td>{ticket.title}</td> <td> <fetcher.Form method="post"> <input type="hidden" name="ticketId" value={ticket.id} /> <button disabled={isSaving}>{isSaving ? 'Сохраняем…' : 'Закрыть'}</button> </fetcher.Form> </td> </tr> ); }

Проверка идентификатора из formData обязательна: без неё все строки одновременно перейдут в состояние сохранения, потому что fetcher.state — общий для компонента.

Проверьте себя. Покажите состояние отправки только у изменяемой строки, используя идентификатор из formData.

Частая ошибка. Блокировать всё приложение из-за фоновой операции fetcher.

#6. Границы ошибок маршрута и семантика HTTP

Граница маршрута локализует сбой загрузчика, действия или отрисовки. Ожидаемые «не найдено» и «нет доступа» выражают через Response и статус, а непредвиденную ошибку записывают с идентификатором связи (correlation id), не показывая пользователю стек. Родительская граница сохраняет каркас страницы, дочерняя — соседние маршруты.

Граница различает брошенный Response и неожидаемое исключение — это два разных сообщения и два разных действия:

// src/routes/tickets/detail.tsx export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { if (isRouteErrorResponse(error)) { return error.status === 404 ? <EmptyState title="Заявка не найдена" action={<Link to="/tickets">К списку</Link>} /> : <ErrorState title={`Ошибка ${error.status}`} />; } // непредвиденный сбой: пользователю — код обращения, в журнал — детали return <ErrorState title="Не удалось загрузить заявку" hint={`Код обращения: ${getTraceId()}`} />; }

Стек и текст исключения не попадают в разметку: они могут содержать внутренние адреса, имена таблиц и фрагменты запросов. Пользователю достаточно кода обращения, по которому инженер найдёт запись в журнале.

Проверьте себя. Разделите 404 заявки и 503 backend failure.

Частая ошибка. Возвращать 200 с текстом ошибки при любом сбое загрузчика.

#7. URL как источник состояния для обмена ссылкой

Параметры поиска подходят для фильтров, сортировки, страниц и вкладок, которые должны переживать обмен ссылкой, перезагрузку и возврат назад. Разбор задаёт значения по умолчанию и каноническую сериализацию; неизвестные значения не должны приводить к сбою. Временное наведение или секретный черновик в URL не помещают.

Разбор фильтров лучше держать одной функцией: она задаёт значения по умолчанию и не падает на мусоре в адресе:

// src/features/tickets/filters.ts const STATUSES = ['all', 'open', 'closed'] as const; type Status = (typeof STATUSES)[number]; export function parseFilters(url: URL): { status: Status; page: number } { const rawStatus = url.searchParams.get('status'); const status = STATUSES.includes(rawStatus as Status) ? (rawStatus as Status) : 'all'; const page = Math.max(1, Number(url.searchParams.get('page')) || 1); return { status, page }; }

?status=deleted&page=-3 не приведёт к сбою: неизвестный статус схлопывается в all, отрицательная страница — в первую. Обратная сериализация должна быть канонической, иначе один и тот же фильтр даст два разных URL и сломает кеш и историю.

Проверьте себя. Сериализуйте фильтры детерминированно и проверьте переходы назад и вперёд.

Частая ошибка. Дублировать фильтр в URL и локальном состоянии с эффектами в обе стороны.

#8. Режимы React Router и архитектурная граница

Декларативный режим даёт клиентскую маршрутизацию, режим данных добавляет загрузчики и действия, а режим фреймворка — соглашения, генерацию типов, SSR и интеграцию со сборкой. Выбирайте минимальный режим, покрывающий требования отрисовки и размещения. Маршрутизатор не заменяет серверную авторизацию.

Последнее утверждение стоит проверить на конкретном примере. Такой загрузчик выглядит как защита доступа, но ею не является:

// ❌ проверка только на клиенте: данные уже пришли в браузер export async function clientLoader() { const user = readUserFromLocalStorage(); if (user?.role !== 'admin') throw redirect('/403'); return fetchAllTickets(); // токен и запрос доступны из консоли браузера }

Перенаправление скрывает интерфейс, но не запрещает запрос: пользователь вызовет тот же адрес напрямую. Решение о доступе принимает сервер, а маршрутизатор лишь отражает это решение в интерфейсе.

Проверьте себя. Кратко обоснуйте выбор режима данных или фреймворка для приложения.

Частая ошибка. Добавить режим фреймворка только ради одного Link, не оценив развёртывание.

#Лаборатория

Создайте /tickets?status= и /tickets/:ticketId: родительский загрузчик получает справочники, дочерний — заявку, действие меняет статус, навигация показывает ожидание, неверный идентификатор даёт границу 404, а история восстанавливает фильтры.

Работа готова, когда прямая ссылка работает после перезагрузки, загрузчики не дублируются эффектами, переходы назад и вперёд восстанавливают URL, а ошибки локализованы границей маршрута.

#Источники для сверки

  • React Router home
  • React Router data loading
  • React Router actions

Устранение неисправностей

Далее: Архитектура состояния и данных