← назад к разделу

В обычном сайте браузер сам управляет навигацией: кликнул по ссылке — сервер вернул новую страницу. В React-приложении этого не происходит. Браузер загрузил одну HTML-страницу, дальше всё — JavaScript. Как тогда переходить между экранами? Как сделать так, чтобы кнопка «назад» работала? Как сохранить прямые ссылки на конкретные разделы?

Эти вопросы решает роутинг. Стандарт в React-экосистеме — React Router.

Что такое клиентский роутинг

В обычном сайте URL и страница — одно целое: новый URL = новый запрос на сервер = новый HTML. В React-приложении URL меняется, но запрос на сервер не идёт — React Router перехватывает переход и сам решает, что нарисовать.

Это называется клиентский роутинг. Пользователь видит разные «страницы», хотя на деле приложение не перезагружалось. URL при этом настоящий: его можно скопировать, открыть в новой вкладке, добавить в закладки — всё работает.

Чтобы подключить React Router, оборачиваем приложение в BrowserRouter:

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

ReactDOM.createRoot(document.getElementById("root")!).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Маршруты и вложенность

Маршрут — это соответствие между путём URL и компонентом, который нужно показать. Базовый вариант:

import { Routes, Route } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/products" element={<ProductList />} />
      <Route path="/about" element={<About />} />
    </Routes>
  );
}

Пока маршруты плоские. Но реальное приложение устроено иначе: есть общая шапка, боковая панель, подвал — и они не меняются при переходах. Только центральная часть экрана обновляется.

Для этого маршруты вкладывают друг в друга, а место, куда подставляется вложенный экран, обозначают компонентом Outlet:

import { Routes, Route, Outlet } from "react-router-dom";

function AppLayout() {
  return (
    <div>
      <Header />
      <nav><Sidebar /></nav>
      <main>
        <Outlet />  {/* здесь появится вложенный маршрут */}
      </main>
      <Footer />
    </div>
  );
}

function AppRoutes() {
  return (
    <Routes>
      <Route element={<AppLayout />}>
        <Route path="/products" element={<ProductList />} />
        <Route path="/products/:id" element={<ProductDetails />} />
        <Route path="/account" element={<Account />} />
      </Route>
    </Routes>
  );
}

AppLayout рисуется один раз и остаётся на экране. При переходе между /products и /account меняется только то, что внутри <main>. Шапка и подвал не мигают.

Параметры URL и программная навигация

Динамическая часть пути обозначается двоеточием: /products/:id. Это параметр маршрута — он может быть любым значением: 123, abc, some-slug.

Чтобы прочитать параметр внутри компонента, используют useParams:

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

function ProductDetails() {
  const { id } = useParams();  // id === "123" если URL /products/123
  // дальше загружаем продукт по этому id
}

Переходить между маршрутами можно двумя способами. Обычная ссылка — компонент Link (он ведёт себя как <a>, но не перезагружает страницу):

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

<Link to="/products">Все продукты</Link>

Программный переход после действия (например, после сохранения формы) — хук useNavigate:

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

function CreateProductForm() {
  const navigate = useNavigate();

  async function handleSubmit() {
    await saveProduct(formData);
    navigate("/products");  // переходим после успешного сохранения
  }
}

navigate(-1) работает как кнопка «назад» в браузере.

Загрузка данных по маршруту

Когда пользователь переходит на /products/123, нужно загрузить данные продукта. Есть два распространённых подхода.

Первый — компонент экрана сам загружает данные через хуки (например, TanStack Query). Параметр из URL становится ключом запроса:

function ProductDetails() {
  const { id } = useParams();
  const { data: product, isLoading } = useQuery({
    queryKey: ["product", id],
    queryFn: () => fetchProduct(id!),
  });

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

Этот подход прост и хорошо работает с TanStack Query — одним инструментом управляем всеми серверными данными.

Второй — data-router с loader-функциями. Данные загружаются до того, как компонент экрана отрисовывается:

const router = createBrowserRouter([
  {
    path: "/products/:id",
    loader: ({ params }) => fetchProduct(params.id!),
    element: <ProductDetails />,
  },
]);

function ProductDetails() {
  const product = useLoaderData() as Product;
  return <div>{product.name}</div>;  // данные уже есть, спиннер не нужен
}

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

Защищённые маршруты

Некоторые экраны доступны только авторизованным пользователям: кабинет, настройки, административный раздел. Чтобы неавторизованного перенаправить на страницу входа, делают компонент-обёртку:

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

function RequireAuth({ children }: { children: React.ReactNode }) {
  const user = useCurrentUser();
  if (!user) return <Navigate to="/login" replace />;
  return <>{children}</>;
}

Используют его как обёртку вокруг маршрута:

<Route
  path="/account"
  element={
    <RequireAuth>
      <Account />
    </RequireAuth>
  }
/>

Или как родительский маршрут для целой группы защищённых экранов:

<Route element={<RequireAuth><Outlet /></RequireAuth>}>
  <Route path="/account" element={<Account />} />
  <Route path="/settings" element={<Settings />} />
</Route>

Важная граница: это удобство для пользователя, не безопасность. Неавторизованный пользователь просто не увидит экран — но сервер всё равно должен проверять права на каждый запрос. Скрыть кнопку без проверки на бэкенде — недостаточно.

Коротко

  • Клиентский роутинг — URL меняется без перезагрузки страницы; браузерные кнопки «назад/вперёд» и прямые ссылки работают.
  • Routes + Route — объявляют, какой компонент показывать по какому пути.
  • Вложенные маршруты + Outlet — общий layout остаётся на экране, меняется только центральная часть.
  • :id в пути — параметр маршрута; читается через useParams.
  • Link — навигация через ссылку; useNavigate — программный переход.
  • Данные по маршруту грузит либо компонент (TanStack Query + useParams), либо loader-функция data-router.
  • Защищённый маршрут — компонент-обёртка, который перенаправляет на /login если пользователь не авторизован.
  • Проверка прав на фронте — удобство; реальная защита — на сервере.

Что почитать дальше

  • Получение данных: TanStack Query — как загружать и кэшировать серверные данные.
  • Компоненты и состояние — основы компонентного подхода в React.
  • Стилизация и дизайн-токены — как оформлять интерфейс.