В обычном сайте браузер сам управляет навигацией: кликнул по ссылке — сервер вернул новую страницу. В 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.
- Стилизация и дизайн-токены — как оформлять интерфейс.