Урок 23: Динамические маршруты и хуки React Router

В предыдущих уроках мы с вами разобрались с основами маршрутизации. Это важный фундамент, но настоящая мощь одностраничных приложений (SPA) раскрывается, когда страницы становятся динамическими.

Сегодня, в нашем 23-м уроке, мы погрузимся в мир динамических маршрутов. Представьте себе интернет-магазин. У вас не может быть отдельного маршрута для каждого из тысяч товаров вроде /product/1/product/2 и так далее. Это неэффективно и попросту невозможно. Вместо этого мы создаем один маршрут-шаблон, например /product/:productId и в зависимости от значения productId в URL, мы подгружаем и отображаем соответствующий товар. Именно это мы сегодня и будем реализовывать.

Мы детально изучим три незаменимых хука из библиотеки react-router-dom, которые делают эту магию возможной: useParamsuseNavigate и useLocation. Они помогут нам создавать по-настоящему интерактивные и сложные приложения. И, как всегда, нас ждет много практики с живыми примерами кода.

Для чего нужны динамические маршруты?

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

Без динамической маршрутизации нам пришлось бы создавать отдельный компонент и прописывать отдельный <Route> для каждой сущности. Для десяти пользователей, десять маршрутов. Для тысячи товаров, тысяча маршрутов. Звучит абсурдно, не правда ли? Динамические маршруты решают эту проблему, позволяя определить маршрут-шаблон. В этом шаблоне часть пути обозначается специальным синтаксисом с двоеточием (:id:slug:userId). Эта часть и становится параметром URL, который мы можем получить и использовать внутри нашего компонента для загрузки нужных данных.

Такой подход делает наше приложение:

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

  • Предсказуемым. URL становится понятным и отражает структуру контента (например, /blog/my-first-post).

  • Совместимым с SEO. Поисковые системы легко индексируют такие страницы, так как у каждой из них уникальный адрес.

В этом уроке мы будем создавать простое приложение-блог, где каждый пост будет иметь свою страницу, доступную по адресу типа /post/1/post/2 и так далее.

Создание динамических маршрутов с помощью :parameter

Первым шагом на пути к динамическим страницам является настройка самих маршрутов в нашем основном файле (часто это App.js или App.jsx). Мы используем компонент Routes и Route из react-router-dom, но на этот раз в пути укажем параметр.

Давайте посмотрим на пример. Предположим, у нас есть компонент Post, который будет отвечать за отображение полной статьи блога.

Код до: Статические маршруты

jsx
import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Post from './Post'; // Наш компонент для страницы поста

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      {/* Так делать НЕЛЬЗЯ для динамических данных! */}
      <Route path="/post/1" element={<Post />} />
      <Route path="/post/2" element={<Post />} />
    </Routes>
  );
}

Код после: Динамический маршрут

jsx
import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Post from './Post';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      {/* Вот так правильно! :postId - это параметр */}
      <Route path="/post/:postId" element={<Post />} />
    </Routes>
  );
}

Обратите внимание на путь /post/:postId. Двоеточие (:) говорит React Router, что следующая часть пути это переменная. Имя postId это просто ключ, под которым мы будем получать значение из URL. Вы можете назвать его как угодно: :id:slug:articleNumber. Теперь любой путь, начинающийся с /post/, будет соответствовать этому маршруту, а часть после /post/ станет значением параметра postId. Например, для URL /post/42 параметр postId будет равен '42'.

Но как же нам получить это значение '42' внутри компонента Post? Вот здесь на сцену выходит наш первый герой, это хук useParams.

Извлечение параметров с помощью хука useParams

Хук useParams это самый простой и часто используемый инструмент для работы с динамическими маршрутами. Он не принимает никаких аргументов и возвращает объект, содержащий пары ключ-значение всех параметров текущего URL.

Давайте модифицируем наш компонент Post, чтобы он мог получать postId из URL и, например, отображать его.

jsx
// Post.jsx
import { useParams } from 'react-router-dom';

const Post = () => {
  // Извлекаем все параметры из URL
  const params = useParams();
  // Поскольку наш параметр называется `postId`, мы можем обратиться к нему напрямую.
  const { postId } = useParams();

  console.log(params); // { postId: '42' } для URL /post/42
  console.log(postId); // '42'

  return (
    <div>
      <h1>Страница поста</h1>
      {/* Отображаем postId на странице */}
      <p>ID этого поста: <strong>{postId}</strong></p>
    </div>
  );
};

export default Post;

Это уже огромный шаг вперед! Теперь наш компонент «знает», для какого именно поста он должен отображать информацию. В реальном приложении мы бы использовали этот postId для выполнения AJAX-запроса к backend-серверу или для поиска данных в массиве, чтобы получить полный объект поста (заголовок, содержание, автора и т.д.).

Хук useParams всегда возвращает строку. Даже если в URL указано число (/post/42), postId будет строкой '42'. Если вам нужно использовать это значение как число (например, для поиска в массиве по индексу), не забудьте преобразовать его с помощью Number(postId) или parseInt(postId).

Давайте создадим небольшой пример с массивом постов, чтобы увидеть все в действии.

Практический пример: Блог с постами

  1. Создадим App.js с маршрутами:

    jsx
    // App.js
    import { Routes, Route } from 'react-router-dom';
    import Home from './Home';
    import Post from './Post';
    import Blog from './Blog'; // Новый компонент для списка постов
    
    function App() {
      return (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/blog" element={<Blog />} /> {/* Список всех постов */}
          <Route path="/blog/:postId" element={<Post />} /> {/* Страница конкретного поста */}
        </Routes>
      );
    }
  2. Создадим компонент Blog для отображения списка постов:

    jsx
    // Blog.jsx
    import { Link } from 'react-router-dom';
    
    // Имитируем базу данных с постами
    const posts = [
      { id: 1, title: 'Мой первый пост', content: 'Содержание первого поста...' },
      { id: 2, title: 'Изучаем React Router', content: 'Содержание второго поста...' },
      { id: 3, title: 'Хуки это просто!', content: 'Содержание третьего поста...' },
    ];
    
    const Blog = () => {
      return (
        <div>
          <h1>Блог</h1>
          <ul>
            {posts.map(post => (
              <li key={post.id}>
                {/* Создаем ссылку на динамический маршрут */}
                <Link to={`/blog/${post.id}`}>
                  {post.title}
                </Link>
              </li>
            ))}
          </ul>
        </div>
      );
    };
    
    export default Blog;
  3. Обновим компонент Post, чтобы он использовал postId для поиска поста:

    jsx
    // Post.jsx
    import { useParams } from 'react-router-dom';
    
    // Импортируем тот же массив постов (в реальном приложении данные пришли бы извне)
    import { posts } from './Blog';
    
    const Post = () => {
      const { postId } = useParams(); // postId из URL (строка!)
    
      // Находим пост в массиве. Не забываем преобразовать id в число!
      const post = posts.find(p => p.id === Number(postId));
    
      // Если пост не найден (например, неверный ID), показываем сообщение
      if (!post) {
        return <h1>Пост с ID {postId} не найден!</h1>;
      }
    
      // Если пост найден, отображаем его
      return (
        <article>
          <h1>{post.title}</h1>
          <p>{post.content}</p>
        </article>
      );
    };
    
    export default Post;

Теперь, если вы перейдете на страницу /blog, вы увидите список постов. Кликнув на любой из них, вы попадете на уникальный URL (например, /blog/2) и компонент Post корректно отобразит заголовок и содержание именно того поста, на который вы кликнули. Это и есть основа динамических страниц!

Программная навигация с useNavigate

До сих пор мы переходили между страницами с помощью компонента Link. Это отлично работает для навигации, инициированной пользователем (клик по ссылке в меню, по кнопке и т.д.). Но что, если нам нужно перенаправить пользователя программно? Например, после успешной отправки формы, после авторизации или через определенный промежуток времени.

Для этих целей у нас есть хук useNavigate. Он возвращает функцию, которую мы можем использовать для программного изменения URL.

Базовая использование:

jsx
import { useNavigate } from 'react-router-dom';

const MyComponent = () => {
  const navigate = useNavigate();

  const handleButtonClick = () => {
    // Переход на указанный путь
    navigate('/blog');
  };

  return (
    <button onClick={handleButtonClick}>
      Перейти в блог
    </button>
  );
};

Но возможности useNavigate гораздо шире. Функция navigate может принимать не только строку с путем, но и число для навигации по истории (аналог кнопок «Назад»/»Вперед» в браузере).

jsx
const navigate = useNavigate();

// Перейти на страницу назад
navigate(-1);

// Перейти на страницу вперед
navigate(1);

// Перейти на две страницы назад
navigate(-2);

Очень мощная особенность, возможность передавать состояние вместе с переходом. Вторым аргументом в navigate можно передать объект с опциями, включая state.

jsx
const handleSubmit = (formData) => {
  // ... здесь логика отправки формы ...

  // После успешной отправки переходим на страницу успеха и передаем данные
  navigate('/success', { state: { message: 'Форма отправлена!', data: formData } });
};

Эти переданные данные мы потом сможем прочитать в целевом компоненте с помощью другого хука useLocation, о котором мы поговорим чуть позже.

Практическая задача: Кнопка «Назад»

Давайте создадим в нашем компоненте Post кнопку «Назад», которая будет возвращать пользователя на предыдущую страницу (список блога).

jsx
// Post.jsx
import { useParams, useNavigate } from 'react-router-dom';
import { posts } from './Blog';

const Post = () => {
  const { postId } = useParams();
  const navigate = useNavigate(); // Получаем функцию navigate

  const post = posts.find(p => p.id === Number(postId));

  const handleGoBack = () => {
    // Просто отправляем пользователя на одну страницу назад
    navigate(-1);

    // ИЛИ можно сделать явный переход на /blog
    // navigate('/blog');
  };

  if (!post) {
    return <h1>Пост не найден!</h1>;
  }

  return (
    <article>
      <button onClick={handleGoBack}>← Назад</button>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
};

Теперь у пользователя есть удобный способ вернуться к списку постов. Хук useNavigate дает нам полный контроль над навигацией в нашем приложении прямо из JavaScript-кода.

Получение информации о местоположении с useLocation

Третий хук, useLocation, предоставляет нам объект, содержащий информацию о текущем маршруте. Этот объект имеет следующие полезные свойства:

  • pathname: Текущий путь (например, /blog/2).

  • search: Строка запроса (то, что после ? в URL, например, ?sort=name).

  • hash: Хэш (то, что после # в URL).

  • state: Состояние, переданное при переходе на эту страницу (например, через navigate(to, { state }) или <Link to={to} state={state} />).

Давайте посмотрим на него в действии. Модифицируем наш Post, чтобы он показывал информацию о текущем местоположении.

jsx
// Post.jsx
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { posts } from './Blog';

const Post = () => {
  const { postId } = useParams();
  const navigate = useNavigate();
  const location = useLocation(); // Получаем объект location

  console.log(location);
  /*
  Для URL /blog/2?from=search#comments объект location будет таким:
  {
    pathname: "/blog/2",
    search: "?from=search",
    hash: "#comments",
    state: null,
    key: "abc123" // уникальный ключ для этого местоположения
  }
  */

  const post = posts.find(p => p.id === Number(postId));

  if (!post) {
    return <h1>Пост не найден!</h1>;
  }

  return (
    <article>
      <button onClick={() => navigate(-1)}>Назад</button>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <hr />
      <h3>Информация из useLocation:</h3>
      <p><strong>Pathname:</strong> {location.pathname}</p>
      <p><strong>Search:</strong> {location.search}</p>
      <p><strong>Hash:</strong> {location.hash}</p>
    </article>
  );
};

Самое интересное свойство, это state. Оно позволяет безопасно передавать данные между маршрутами без использования глобального состояния (вроде Redux) или пропсов. Это отлично подходит для передачи временной информации, например, сообщения об успехе после редиректа.

Пример: Передача состояния при навигации

Давайте смоделируем сценарий: на странице /blog пользователь заполняет форму поиска. После отправки формы мы хотим перенаправить его на страницу результатов поиска /search и передать введенный им поисковый запрос.

  1. На странице Blog (откуда уходим):

    jsx
    // Blog.jsx (часть компонента)
    import { useNavigate } from 'react-router-dom';
    
    const Blog = () => {
      const navigate = useNavigate();
      const [searchQuery, setSearchQuery] = useState('');
    
      const handleSearchSubmit = (e) => {
        e.preventDefault();
        // Переходим на /search и передаем запрос в состоянии
        navigate('/search', { state: { query: searchQuery } });
      };
    
      return (
        <div>
          {/* ... список постов ... */}
          <form onSubmit={handleSearchSubmit}>
            <input
              type="text"
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              placeholder="Поиск..."
            />
            <button type="submit">Искать</button>
          </form>
        </div>
      );
    };
  2. На странице Search (куда пришли):

    jsx
    // Search.jsx
    import { useLocation } from 'react-router-dom';
    
    const Search = () => {
      const location = useLocation();
      // Извлекаем переданное состояние. Если его нет, используем пустую строку.
      const searchQuery = location.state?.query || '';
    
      return (
        <div>
          <h1>Результаты поиска</h1>
          <p>Вы искали: <strong>{searchQuery}</strong></p>
          {/* Здесь можно отобразить результаты, используя searchQuery */}
        </div>
      );
    };

Таким образом, мы смогли передать данные из одного маршрута в другой, не используя глобальное хранилище и не засоряя URL. Это чисто и эффективно.

Практические задачи для закрепления

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

Задача 1: Профиль пользователя

Создайте простое приложение с двумя страницами:

  1. Страница со списком пользователей (/users). Покажите 3-5 пользователей с именами.

  2. Динамическая страница пользователя (/user/:userId).

Требования:

  • На странице списка имена пользователей должны быть ссылками, ведущими на их профили.

  • На странице профиля, используя useParams, отобразите ID пользователя и приветствие, например: «Страница пользователя с ID: 3. Привет, Аня!» (для этого вам нужно будет передавать не только ID, но и имя).

Подсказка. Вам понадобятся компоненты UsersUserProfile, а также Link и useParams.

Решение задачи 1:

jsx
// App.js
import { Routes, Route } from 'react-router-dom';
import Users from './Users';
import UserProfile from './UserProfile';

function App() {
  return (
    <Routes>
      <Route path="/users" element={<Users />} />
      <Route path="/user/:userId" element={<UserProfile />} />
    </Routes>
  );
}
jsx
// Users.jsx
import { Link } from 'react-router-dom';

const users = [
  { id: 1, name: 'Максим' },
  { id: 2, name: 'Аня' },
  { id: 3, name: 'Иван' },
];

const Users = () => {
  return (
    <div>
      <h1>Список пользователей</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            <Link to={`/user/${user.id}`}>
              {user.name}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Users;
jsx
// UserProfile.jsx
import { useParams, useNavigate } from 'react-router-dom';
import { users } from './Users'; // Импортируем массив пользователей

const UserProfile = () => {
  const { userId } = useParams();
  const navigate = useNavigate();

  // Находим пользователя по ID
  const user = users.find(u => u.id === Number(userId));

  if (!user) {
    return <h1>Пользователь не найден!</h1>;
  }

  return (
    <div>
      <button onClick={() => navigate(-1)}>Назад к списку</button>
      <h1>Страница пользователя с ID: {userId}</h1>
      <h2>Привет, {user.name}!</h2>
    </div>
  );
};

export default UserProfile;

Задача 2: Обработка несуществующих страниц (404)

Создайте механизм для отлова несуществующих маршрутов. Если пользователь ввел URL, для которого нет заданного маршрута (например, /some/nonsense), он должен увидеть кастомную страницу 404.

Требования:

  • Создайте компонент NotFound.

  • Настройте маршрут с path=»*», который будет отображать этот компонент для всех несовпадающих URL.

Решение задачи 2:

jsx
// App.js
import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import Blog from './Blog';
import Post from './Post';
import NotFound from './NotFound'; // Новый компонент

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/blog" element={<Blog />} />
      <Route path="/blog/:postId" element={<Post />} />
      {/* Этот маршрут сработает для всего, что не совпало с вышеуказанными */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}
jsx
// NotFound.jsx
import { useLocation, Link } from 'react-router-dom';

const NotFound = () => {
  const location = useLocation();

  return (
    <div style={{ textAlign: 'center', padding: '50px' }}>
      <h1>404 - Страница не найдена!</h1>
      <p>Извините, страница по адресу <code>{location.pathname}</code> не существует.</p>
      <Link to="/">Вернуться на главную</Link>
    </div>
  );
};

export default NotFound;

Задача 3: Модальное окно с использованием маршрутизации

Попробуйте реализовать открытие модального окна (например, для просмотра изображения) с привязкой к URL. Идея в том, что при клике на изображение в галерее URL меняется (например, на /img/2) и поверх страницы открывается модальное окно с этим изображением. При нажатии «Назад» или закрытии модального окна URL должен вернуться к предыдущему значению, а модальное окно закрыться.

Требования:

  • Страница галереи (/gallery) отображает список уменьшенных изображений (превью).

  • Клик на превью меняет URL на /gallery/:imageId и открывает модалку.

  • Модальное окно должно быть реализовано как часть маршрута /gallery/:imageId, но отображаться поверх галереи.

  • Закрытие модалки (через крестик, клик вне области или кнопку «Назад») должно вести на /gallery.

Подсказка 1. Исследуйте возможность использования вложенных маршрутов (Nested Routes) и рендеринга нескольких маршрутов одновременно. Это сложная, но очень интересная задача, которая покажет вам всю мощь React Router.

Подсказка 2 и примерная структура для задачи 3:

jsx
// App.js
// Используем вложенные маршруты
<Routes>
  <Route path="/gallery" element={<Gallery />}>
    {/* Этот маршрут будет отрендерен ВНУТРИ <Outlet /> в компоненте Gallery */}
    <Route path=":imageId" element={<ImageViewModal />} />
  </Route>
</Routes>
jsx
// Gallery.jsx
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';

const images = [ ... ]; // массив с данными изображений

const Gallery = () => {
  const location = useLocation();
  const navigate = useNavigate();

  // Определяем, открыта ли модалка. Если в location есть background, значит мы пришли с другой страницы.
  // Если нет, проверяем, есть ли у нас imageId в location.
  // В данном упрощенном случае, можно просто проверить, есть ли вложенный маршрут.
  const isModalOpen = location.pathname !== '/gallery';

  const handleCloseModal = () => {
    // Возвращаемся на страницу галереи
    navigate('/gallery');
  };

  return (
    <div>
      <h1>Галерея</h1>
      <div className="image-grid">
        {images.map(img => (
          <Link key={img.id} to={`/gallery/${img.id}`}>
            <img src={img.thumbnailUrl} alt={img.title} />
          </Link>
        ))}
      </div>

      {/* Outlet - это место, где будет рендериться вложенный маршрут <ImageViewModal /> */}
      {isModalOpen && (
        <div className="modal-overlay" onClick={handleCloseModal}>
          <div className="modal-content" onClick={(e) => e.stopPropagation()}>
            <button onClick={handleCloseModal}>×</button>
            <Outlet /> {/* Здесь появится <ImageViewModal /> когда маршрут /gallery/:imageId активен */}
          </div>
        </div>
      )}
    </div>
  );
};
jsx
// ImageViewModal.jsx
import { useParams, useNavigate } from 'react-router-dom';
import { images } from './Gallery';

const ImageViewModal = () => {
  const { imageId } = useParams();
  const navigate = useNavigate();
  const image = images.find(img => img.id === Number(imageId));

  if (!image) return null;

  return (
    <div>
      <h2>{image.title}</h2>
      <img src={image.fullSizeUrl} alt={image.title} />
    </div>
  );
};

Это сложная архитектура, но она позволяет сохранять состояние страницы (позицию скролла в галерее) при открытии и закрытии модалки, а также делиться прямыми ссылками на конкретное изображение.

Итоги урока

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

  1. Динамические маршруты. Мы научились создавать маршруты-шаблоны с помощью синтаксиса :parameter в компоненте Route.

  2. useParams. Мы узнали, как с помощью этого хука извлекать значения параметров из URL внутри наших компонентов, чтобы показывать контент в зависимости от адресной строки.

  3. useNavigate. Мы освоили программную навигацию, которая позволяет перенаправлять пользователя по различным сценариям (отправка формы, таймер, кнопка «Назад») и передавать состояние между маршрутами.

  4. useLocation. Мы познакомились с этим хуком, который дает нам детальную информацию о текущем местоположении (путь, строка запроса, хэш, состояние), что особенно полезно для анализа URL и приема переданных данных.

Комбинируя эти три хука, вы можете создавать невероятно сложные и интерактивные пользовательские интерфейсы с полноценной маршрутизацией, которые будут чувствоваться как «нативные» приложения.

Если вы хотите освоить React от самых основ до профессионального уровня, приглашаю вас изучить полный курс с уроками по React для начинающих. Там вас ждут структурированные материалы, еще больше практических заданий и углубленное изучение всех аспектов этой мощной библиотеки. Удачи в обучении!

Поделиться статьей:
Поддержать автора блога

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

Персональные рекомендации
Оставить комментарий