React Router
1. Маршрутизация
Отличающее преимущество веб-приложения от десктоп это наличие URL, при переходе по которому, пользователь оказывается в определенной части приложения. Так можно сохранить закладку или передать ссылку другому пользователю, при этом ему будет отображен один и тот же интерфейс (за исключением приватных данных).
Маршрутизация (routing) - это не побочный эффект при написании приложения, наоборот, грамотную архитектуру навигации необходимо продумывать в первую очередь.
1.1. Структура URL-строки
Аналогией URL-строки может быть адрес по которому вы проживаете: улица, дом, квартира. У каждого состояния интерфейса должен быть свой адрес, свой URL. То, что видит пользователь, состояние интерфейса, должно быть описано в URL.

https://
- протоколmysite.com/
- хостbooks/e3q76gm9lzk
- путь, то где мы находимся в приложенииe3q76gm9lzk
- url-параметр. Параметры бывают динамическими или статическими?
- символ начала строки запроса?category=adventure&status=unread
- строка запросаcategory=adventure
- пара параметр=значение&
- символ "И", разделяет параметры строки запроса#comments
- якорь (хеш), определяет положение на странице
1.2. История навигации
История навигации - то как мы переходим ссылкам, как переходы хранятся и парсятся. От типа истории зависит метод ее хранения и изменения.
Если вы хотите понять React Router, рекомендуется ознакомиться с HTML5 History API. Более конкретно, c пакетом history, который предоставляет основные функциональные возможности для React Router, позволяя одностраничным приложениям легко добавлять навигацию на стороне клиента.
Существует несколько типов истории.
Browser history - использует HTML5 History API, стандарт управления историей браузера из JavaScript.
Hash history - в старых браузерах не поддерживается HTML5 History API, поэтому для них существует эта реализация.
Memory history - позволяет использовать историю сессии в памяти, вне окна браузера. К примеру для тестирования логики без интерфейса и в средах без DOM, к примеру React Native.
2. React Router
Предоставляет набор компонентов для управления частями URL-строки и отображения различных компонетов в зависимости от текущего ее состояния. Разбит на пакеты для различных платформ, нас интересует react-router-dom
.
npm install react-router-dom
В React Router есть три типа компонентов: компонент маршрутизатора, компоненты согласования маршрутов и компоненты навигации.
2.1. BrowserRouter
В основе каждого одностраничного приложения стоит маршрутизатор. Компонент <BrowserRouter>
создает раутер и объект history
, чтобы синхронизировать интерфейс с URL-адресом. Используя контекст, передает данные о текущем URL всему поддереву компонентов.
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.querySelector('#root'),
);
2.2. Route
Компонент позволяющий связать определенный URL и компонент для рендера. Его задача заключается в том, чтобы отобразить некоторый интерфес, когда location.pathname
соответствует значению пропа path
. Компонент <Route>
можно использовать в любом месте где необходимо рендерить контент на основе текущего URL.
import React from 'react';
import { Route } from 'react-router-dom';
const App = () => (
<>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</>
);
Согласование маршрута выполняется путем сопоставления пропа path
и текущего значения location.pathname
. Если значение location.pathname
начинается на указанный путь в path
, <Route>
отрендерит указанный компонент, в противном случае вернет null
.
Когда location.pathname
начинается на '/about'
, первый и третий рауты отрендерят null
, а второй отрендерит компонент <About>
.
Проп
exact
указывает на необходимость точного совпаденияpath
иlocation.pathname
<Route>
без указанногоpath
всегда рендерит компонент
2.2.1. Проп component
Используется когда компонент необходимо отрендерить без передачи дополнительных пропсов. Стандартные пропсы match
, location
и history
будут переданы компоненту автоматически.
Для создания компонента используется React.createElement()
, это означает, что если вместо ссылки на компонент будет передана анонимная функция, для каждого ре-рендера будет создан новый компонент. Это приведет к размонтированию существующего компонента, созданию и монтированию нового на каждом рендере, вместо обновления существующего компонента.
// ✅ Хорошо
<Route path="/about" component={About} />
// ❌ Плохо
<Route
path="/about"
component={props => <About {...props} extraProp="amazing prop" />}
/>
2.2.2. Проп render
Позволяет использовать инлайн-функцию вместо компонента без нежелательного эффекта ре-рендера как в случае с component
. Используется тогда, когда компоненту необходимо передать дополнительные пропсы, кроме тех что передает <Route>
.
<Route
path="/about"
render={props => <About {...props} extraPropName="value" />}
/>
2.3. Switch
Группирует и отображает первый дочерний маршрут, path
которого соответствует текущему location.pathname
, игнорруя все последующие.
import React from 'react';
import { Switch, Route } from 'react-router-dom';
const App = () => (
<div>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route component={NotFound} />
</Switch>
</div>
);
2.4. Redirect
Позволяет декларативно отрендерить компонент который во время маунта перенаправит пользователя по указанному маршруту. Под капотом использует императивный интерфейс history
.
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
const App = () => (
<div>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Redirect to="/" />
</Switch>
</div>
);
По умолчанию подменяет текущую запись в истории, для того чтобы добавить новую запись на верх стека можно передать проп
push
.Вместо строки, проп
to
может принимать полноценный объект форматаlocation
.
2.5. Link и NavLink
Для создания навигации нельзя использовать обычный HTML-тег <a href="/about">
. При клике, вместо того чтобы изменить URL на текущей странице, и позволить раутеру выполнить навигацию на стороне клиента, браузер выполнит GET-запрос и обновит страницу, а это не то, что нам нужно.
Компоненты <Link>
и <NavLink>
используются для создания ссылок. Они рендерят HTML-тег <a>
, но с расширенным функционалом клика, используя возможности объекта history
по замене URL.
<Link to="/books">Books</Link>
<Link to="/books?category=adventure#treasure-island">Adventure books</Link>
Проп to
можно передавать в виде строки описывающей href
будущей ссылки, или как объект location
со следующими (необязательными) свойствами:
pathname
- строка, путь для ссылки.search
- строковое представление параметров запроса.hash
- хэш для добавления в конец URL.state
- объект, который будет записан вlocation.state
после перехода по ссылке.
<Link
to={{
pathname: '/books',
search: '?category=adventure',
hash: '#treasure-island',
state: { from: '/dashboard' },
}}
/>
Компонент <NavLink>
отличается от <Link>
только тем, что может иметь дополнительные стили, если текущий URL совпадает со значением пропа to
.
activeClassName
- строка классов для объеденения сclassName
когда элемент активен.activeStyle
- объект инлайн стилей для добавления к элементу когда он активен.exact
- когдаtrue
, активные классы/стили будут применяться только в том случае, если местоположение точно совпадает со значением пропаto
.
<NavLink to="/books" className="link" activeClassName="active-link">
Books
</NavLink>
2.6. Route props
Компоненту отрендеренному через <Route>
будет передано несколько специальных пропсов хранящих много полезной информации.
match
— объект с информацией о том как совпалиpath
иlocation.pathname
location
- объект хранящий информацию о текущем URLhistory
- объект истории, созданный самим раутером
2.6.1. Проп match
Объект, хранит информацию о том, как path
совпал с location.pathname
. Содержит следующие свойства.
params
- объект парключ:значение
, соответствующих динамическим параметрам URL.isExact
- указыват на точное соотвествиеpath
иlocation.pathname
.path
- паттерн пути на который замачился<Route>
. Используется для создания вложенных маршрутов.url
- совпавшая часть URL-адреса. Используется для создания вложенной навигации.
2.6.2. Проп location
Объект, свойства которого описывают текущее местоположение, путь куда будет произведен переход или откуда пришли на текущий маршрут. Можно использовать в том числе для проверки в componentDidUpdate
изменился ли текущий URL.
{
key: 'ac3df4',
pathname: '/books',
search: '?sortby=latest',
hash: '#comments',
state: {
from: '/login'
}
}
2.6.3. Проп history
Объект истории со свойствами и методами для программной навигации. Используется для перенаправлений.
history.push(path [, state])
- добавляет новую запись на стек записей истории.history.replace(path [, state])
- подменяет текущую запись на новую на стеке записей истории.
3. Строка запроса
В строке запроса можно указывать дополнительную информацию о текущем маршруте. Например, если мы просматриваем страницу новостных статей, маршрут может выглядеть так.
https://app.com/articles
Если есть категории выбора статей по жанру и времени издания, храним выбранные опции в строке запроса.
https://app.com/articles?category=health&sortBy=latest
Строка запроса содержит 2 параметра: название категории и порядок сортировки статей. При переходе по такому маршруту, можно сделать HTTP-запрос на бэкенд, получив только статьи необходимой категории, и отсортировать их по полю даты издания.
Текущее значение строки запроса хранится в пропе location
.
{
key: 'ac3df4',
pathname: '/articles',
search: '?category=health&sortBy=latest',
hash: ''
}
3.1. Извлечение параметров
Чтобы получить значения параметров, можно использовать возможности нативного класса URLSearchParams, а также библиотеки qs или query-string. Любой метод позволит сделать парс строки запроса и получить объект с парами ключ:значение
.
import queryString from 'query-string';
const queryParams = queryString.parse(props.location.search);
3.2. Изменение параметров
Допустим, для выбора категории статей используется <select>
. При выборе опции необходимо обновлять URL используя метод history.push()
для добавления новой записи в журнал истории.
Берем текущее значение location.pathname
и обновляем search
.
onCategoryChange = category => {
this.props.history.push({
pathname: this.props.location.pathname,
search: `category=${category}`,
});
};
3.3. Отслеживание изменений
Если меняется строка запроса, компоненту пробрасываются новые пропсы, и в методе componentDidUpdate()
можно проверить изменилась ли категория и порядок сортировки. Если изменились, делаем HTTP-запрос или сортируем текущие статьи.
const getCategoryFromProps = props => queryString.parse(props.location.search).category;
componentDidUpdate(prevProps) {
const prevCategory = getCategoryFromProps(prevProps);
const nextCategory = getCategoryFromProps(this.props);
if (prevCategory !== nextCategory) {
this.fetchArticles(nextCategory);
}
}
4. Редиректы
Перенаправления это удобный инструмент навигации пользователя между маршрутами приложения. Для их реализации используется компонент <Redirect>
или методы history.push()
и history.replace()
.
4.1. Проп history
Важно знать раличие методов push
и replace
.
history.push()
- добавит новую запись в журнал истории, пользователь может вернуться на тот маршрут с которого пришел.history.replace()
- перезапишет текущую запись в журнале истории затерев текущую, пользователь не сможет вернутся на маршрут с которого пришел.
Продолжим работать со статьями. При рендере раута /articles
есть категории статей, но при первом рендере маршрута еще нет строки запроса с параметром category
. Именно поэтому необходимо сделать редирект, при этом перезаписав текущую страницу истории, чтобы пользователь не мог вернуться на страницу без параметров запроса.
componentDidMount() {
/*
* При маунте компонента проверяем наличие параметра `category` в строке запроса.
*/
const category = getCategoryFromProps(this.props);
/*
* Если параметра нет, делаем `history.replace` с текущим маршрутом,
* но добавляем категорию `all`, чтобы забрать все статьи с бекенда.
* При этом перезаписываем текущую страницу истории.
*/
if (!category) {
this.props.history.replace({
pathname: this.props.location.pathname,
search: `category=all`,
});
return;
}
/*
* Если при рендере такой параметр уже есть, например пользователь зашел по
* сохраненной ссылке, делаем HTTP-запрос за статьями.
*/
fetchArticlesByCategory(category).then(articles =>
this.setState({ articles }),
);
}
4.2. Свойство location.state
Позволяет передавать кастомные данные между маршрутами. Например список статей это ссылки, при клике в ссылку переходим на новый маршрут - страницу статьи.
https://app.com/articles/:articleId
Тогда, если пользователь пришел с https://app.com/articles?category=sports
, и захочет вернуться на сраницу всех статей, нажав стрелку "Обратно", он попадет куда надо. Но что если необходимо реализовать кнопку "Обратно к статьям" в интерфейсе приложения, как узнать откуда мы пришли?
Каждую статью сделаем ссылкой, добавив в свойство state
информацию о текущем маршруте.
<Link
to={{
pathname: `/articles/${id}`,
state: { from: this.props.location },
}}
>
Article title
</Link>
Тогда в компоненте статьи можем получить эту информацию и при клике на кнопку "Обратно к статьям" перенаправить пользователя на предыдущий маршрут.
handleGoBack = () => {
this.props.history.push(this.props.location.state.from);
};
Нужно учитывать ситуацию когда пользователь перешел на страницу статьи по сохраненный ссылке, и в текущей сессии не был на странице всех статей. В свойстве location.state
не будет объекта, и попытка доступа к state.from
приведет к ошибке выполнения скрипта.
Поэтому необходимо проверить location.state
. Если state
есть - перенаправляем пользователя туда, откуда он пришел. Если state
нет - перенаправляем на /articles
, при этом параметр category
назначаем по свойству статьи (если в объекте статьи есть такое свойство).
handleGoBack = () => {
const { state } = this.props.location;
const { category } = this.state;
if (state) {
this.props.history.push(state.from);
return;
}
this.props.history.push({
pathname: '/articles',
search: `?category=${category}`,
});
};
Last updated
Was this helpful?