Примеры 👀
- Примеры
- Основные компоненты
- API
- Рендеринг на стороне сервера
- Разделение кода
- Восстановление прокрутки
В приведенном ниже примере у нас имеется 3 страницы, обрабатываемые роутером: главная (home), контакты (about) и страница с пользователями (users). При клике по <Link>
(ссылке) роутер рендерит соответствующий <Route>
(маршрут, путь).
Обратите внимание: за сценой <Link>
рендерит <a>
с настоящим href
, так что люди, использующие клавиатуру для навигации или экранные считываюющие устройства (screen readers), смогут без проблем пользоваться приложением.
import React from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from 'react-router-dom'
export const App = () => (
<Router>
<header>
<nav>
<ul>
<li>
<Link to="/">Главная</Link>
</li>
<li>
<Link to="/about">Контакты</Link>
</li>
<li>
<Link to="/users">Пользователи</Link>
</li>
</ul>
</nav>
</header>
<main>
{/* <Switch> рендерит первый <Route>, совпавший с URL */}
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</main>
</Router>
)
const Home = () => <h2>Главная</h2>
const About = () => <h2>Контакты</h2>
const Users = () => <h2>Пользователи</h2>
Ниже приводится пример вложенного роутинга. Маршрут /topics
загружает компонент Topics
, который, в свою очередь, рендерит дальнейшие <Route>
на основе значения :id
.
import React from "react"
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useRouteMatch,
useParams
} from "react-router-dom"
export const App = () => (
<Router>
<header>
<nav>
<ul>
<li>
<Link to="/">Главная</Link>
</li>
<li>
<Link to="/about">Контакты</Link>
</li>
<li>
<Link to="/topics">Темы</Link>
</li>
</ul>
</nav>
</header>
<main>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</main>
</Router>
)
const Home = () => <h2>Главная</h2>
const About = () => <h2>Контакты</h2>
function Topics() {
const match = useRouteMatch()
return (
<>
<h2>Темы</h2>
<nav>
<ul>
<li>
<Link to={`${match.url}/components`}>Компоненты</Link>
</li>
<li>
<Link to={`${match.url}/props-vs-state`}>Пропы против состояния</Link>
</li>
</ul>
</nav>
{/* Страница Topics имеет собственный <Switch> с маршрутами,
основанными на URL /topics. Вы можете думать о втором
<Route> как о странице для остальных тем
или как о странице, отображаемой,
когда ни одна из тем не выбрана */}
<div>
<Switch>
<Route path={`${match.path}/:topicId`}>
<Topic />
</Route>
<Route path={match.path}>
<h3>Пожалуйста, выберите тему.</h3>
</Route>
</Switch>
</div>
</>
)
}
function Topic() {
const { topicId } = useParams()
return <h3>Идентификатор выбранной темы: {topicId}</h3>
}
В React Router
существует 3 категории компонентов:
- роутеры (routers), например,
<BrowserRouter>
или<HashRouter>
- маршруты (route matchers), например,
<Route>
или<Switch>
- и навигация (navigation), например,
<Link>
,<NavLink>
или<Redirect>
Все компоненты, используемые в веб-приложении, должны быть импортированы из react-router-dom
.
Любая маршрутизация начинается с роутера. Для веб-проектов react-router-dom
предоставляет <BrowserRouter>
и <HashRouter>
. Основное отличие между ними состоит в способе хранения URL и взаимодействия с сервером.
<BrowserRouter>
использует обычные URL. В этом случае URL выглядят как обычно, но требуется определенная настройка сервера. В частности, сервер должен обслуживать все страницы, используемые на клиенте.Create React App
поддерживает это из коробки в режиме разработки и содержит инструкции для правильной настройки сервера.<HashRouter>
хранить текущую локацию в хэш-части URL (после символа "#"), поэтому URL выглядит примерно так:http://example.com/#/your/page
. Поскольку хэш не отправляется серверу, его специальная настройка не требуется.
Для использования роутера необходимо обернуть в него компонент верхнего уровня:
import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter } from "react-router-dom"
const App = () => <h1>Привет, React Router</h1>
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
)
Существует 2 вида компонентов для поиска совпадений с URL: Switch
и Route
. При рендеринге <Switch>
определяет <Route>
, соответствующий текущему URL. При обнаружении такого маршрута, он рендерится, остальные маршруты игнорируются. Это означает, что вы должны помещать более специфические маршруты перед менее специфическими.
Если совпадения не найдено, <Switch>
ничего не рендерит (точнее, рендерит null
).
import React from "react"
import ReactDOM from "react-dom"
import {
BrowserRouter as Router,
Switch,
Route
} from "react-router-dom"
const App = () => (
<main>
<Switch>
{/* Если текущим URL является /about, рандерится данный маршрут,
остальные игнорируются */}
<Route path="/about">
<About />
</Route>
{/* Обратите внимание на порядок расположения этих двух маршрутов.
Более специфический path="/contact/:id" находится перед path="/contact" */}
<Route path="/contact/:id">
<Contact />
</Route>
<Route path="/contact">
<AllContacts />
</Route>
{/* Если ни один из предыдущих роутеров не совпал,
рендерится данный маршрут (он является резервным).
Важно: маршрут с path="/" всегда будет совпадать с
URL, поскольку все URL начинаются с /. Поэтому
мы поместили его последним */}
<Route path="/">
<Home />
</Route>
</Switch>
</main>
)
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
)
Обратите внимание, что <Route path>
ищет совпадение с началом, а не со всем URL. Поэтому <Route path="/">
всегда будет совпадать с URL. Поэтому в <Switch>
мы, обычно, помещаем его последним. Другим возможным решением является использование атрибута exact
: <Route exact path="/">
, который заставляет роутер искать полное совпадение.
React Router
предоставляет компонент <Link>
для создания ссылок в приложении. При использовании <Link>
в HTML рендерится <a>
.
<Link to="/">Главная</Link>
// <a href="/">Главная</a>
<NavLink>
- это специальный тип <Link>
, позволяющий определять стили для активного состояния ссылки.
<NavLink to="/react" activeClassName="hurray">
React
</NavLink>
// Когда URL является /react, рендерится это:
// <a href="/react" className="hurray">React</a>
// Когда URL является другим:
// <a href="/react">React</a>
Для принудительной навигации используется <Redirect>
. При рендеринге <Redirect>
выполняется перенаправление.
<Redirect to="/login">
React Router
предоставляет несколько хуков для доступа к состоянию роутера и навигации внутри компонентов.
Указанные хуки предоставляют доступ к следующим объектам:
Данный хук предоставляет доступ к экземпляру истории (history instance), который может использоваться для навигации:
import { useHistory } from 'react-router-dom'
function HomeButton() {
const history = useHistory()
function handleClick() {
history.push('/home')
}
return (
<button type="button" onClick={handleClick}>
Вернуться на главную
</button>
)
}
Термины "история" (history) и "объект истории" (history object) являются ссылками на пакет history, одну из двух зависимостей React Router
(вторая - сам React
), представляющий собой несколько реализаций, позволяющих управлять историей сессии с помощью JavaScript
в разных окружениях.
Объекты истории, как правило, имеют следующие свойства и методы:
length
: number - количество вхождений в стеке истории (history stack)action
: string - текущая операция (PUSH
,REPLACE
илиPOP
)location
: object - текущая локация. Может иметь следующие свойства:pathname
: string - адрес URLsearch
: string - строка запроса URLhash
: string - хэш-фрагмент URLstate
: object - специфичное для локации состояние, переданное, например, с помощьюpush(path, state)
при помещении локации в стек. Доступно только в браузере и истории, хранящейся в памяти
push(path, [state])
: func - помещает новое вхождение в стек историиreplace(path, [state])
: func - заменяет текущее вхождение в стеке историиgo(n)
: func - перемещает указатель в стеке истории на n вхожденийgoBack()
: func - эквивалент go(-1)goForward()
: func - эквивалент go(1)block(prompt)
: func - запрещает переход на другую страницу
Данный хук возвращает объект локации (location object), содержащий информацию о текущем URL. Вы можете думать о нем как о useState
, возвращающем новую локацию при изменении URL.
Это может быть полезным в ситуации, когда необходимо получить новое представление страницы с помощью инструмента веб-аналитики при загрузке новой страницы, как в следующем примере:
import React from "react"
import ReactDOM from "react-dom"
import {
BrowserRouter as Router,
Switch,
useLocation
} from "react-router-dom"
function usePageViews() {
let location = useLocation()
React.useEffect(() => {
ga.send(["pageview", location.pathname])
}, [location])
}
function App() {
usePageViews()
return <Switch>...</Switch>
}
ReactDOM.render(
<Router>
<App />
</Router>,
node
)
Объект локации выглядит следующим образом:
{
key: 'ac3df4', // отсутствует в HashHistory
pathname: '/somewhere',
search: '?some=search-string',
hash: '#howdy',
state: {
[userDefined]: true
}
}
Объект локации доступен в:
- Компоненте
Route
какthis.props.location
(props.location
) - Пропе
render
компонентаRoute
как({ location }) => ()
- Потомках (
children
) компонентаRoute
как({ location }) => ()
withRouter
какthis.props.location
Он также доступен в history.location
.
Объект локации, в отличие от объекта истории, никогда не мутирует, поэтому его можно использовать для определения того, выполнялась ли навигация, например, для получения данных и запуска анимации.
Вы можете использовать location
для навигации вместо строк в:
Link to
для вебаLink to
для React NativeRedirect to
history.push
history.replace
Обычно, во всех перечисленных случаях мы используем строку, однако, когда необходимо добавить некоторое "состояние локации", которое будет доступным при возвращении приложения в данную локацию, можно использовать объект локации. Это может пригодиться в случае, когда определенные части интерфейса зависят от истории навигации, а не от путей (например, модульные окна).
// обычно, это все, что нам нужно
<Link to="/somewhere"/>
// однако, вместо этого можно использовать location
const location = {
pathname: '/somewhere',
state: { fromDashboard: true }
}
<Link to={location}/>
<Redirect to={location}/>
history.push(location)
history.replace(location)
Наконец, location
можно передавать в следующие компоненты:
Route
Switch
Это предотвратит использование ими актуальной локации из состоянии роутера. Это может быть полезным для анимации и отложенной навигации, а также для замены локации, в которой рендерится компонент.
Данный хук возвращает объект с параметрами URL в формате ключ: значение
. Используется для доступа к match.params
текущего <Route>
:
import React from "react"
import ReactDOM from "react-dom"
import {
BrowserRouter as Router,
Switch,
Route,
useParams
} from "react-router-dom"
function BlogPost() {
const { slug } = useParams()
return <div>Отображается пост {slug}</div>
}
ReactDOM.render(
<Router>
<Switch>
<Route exact path="/">
<HomePage />
</Route>
<Route path="/blog/:slug">
<BlogPost />
</Route>
</Switch>
</Router>,
node
)
Данный хук пытается найти совпадение для текущего URL подобно <Route>
. Он может быть полезен для доступа к совпадающим данным без рендеринга <Route>
.
Это позволяет вместо
import { Route } from "react-router-dom"
function BlogPost() {
return (
<Route
path="/blog/:slug"
render={({ match }) => {
// работаем с match...
return <div />
}}
/>
)
}
делать так
import { useRouteMatch } from "react-router-dom"
function BlogPost() {
const match = useRouteMatch("/blog/:slug")
// работаем с match...
return <div />
}
useRouteMatch
:
- без аргументов: возвращает объект совпадения текущего
<Route>
- принимает один аргумент, эквивалентный пропу
matchPath.argument
. Это может бытьpathname
в виде строки, как в приведенном выше примере, или объект с пропами, принимаемыми<Route>
, как в следующем примере:
const match = useRouteMatch({
path: "/BLOG/:slug/",
strict: true,
sensitive: true
})
Объект совпадения (match object) содержит информацию о том, насколько <Route path>
совпадает с URL. Он имеет следующие свойства:
params
: object - пары ключ/значение, соответствующие динамической части URLisExact
: bool -true
, если имеет место совпадениеpath
: string - паттерн пути, используемый для поиска совпадения. Используется для построения вложенных<Route>
url
: string - совпавшая часть URL. Используется для создания вложенных<Link>
Доступ к объекту совпадения можно получить в:
Route
какthis.props.match
(props.match
)- Пропе
render
компонентаRoute
как({ match }) => ()
- Потомках
Route
как({ match }) => ()
withRouter
какthis.props.match
matchPath
в виде возвращаемого значенияuseRouteMatch
в виде возвращаемого значения
Если Route
не имеет пропа path
и поэтому всегда совпадает, мы получаем его ближайшего предка. Тоже самое справделиво для withRouter
.
<Route>
, имеющий проп children
, вызывает функцию данного пропа даже при отсутствии совпадения с текущей локацией. В этой ситуации match
будет иметь значение null
. Может быть полезным иметь возможность рендерить контент <Route>
при совпадении, но на этом пути существует несколько препятствий.
Обычный способ "разрешения" URL заключается в объединении строки match.url
и "относительного" пути.
const path = `${match.url}/relative-path`
Если вы попытаетесь это сделать при нулевом совпадении, будет выброшено исключение TypeError
. Это означает, что объединение "относительных" путей внутри <Route>
, использующего для этого проп children
, является небезопасным.
Похожая ситуация возникает при использовании <Route>
без path
внутри <Route>
, генерирующего нулевой объект совпадения.
// location.pathname = '/matches'
<Route path="/does-not-match"
children={({ match }) => (
// match === null
<Route
render={({ match: pathlessMatch }) => (
// pathlessMatch === ???
)}
/>
)}
/>
<Route>
без path
наследует объект совпадения от предка. Если match
предка равняется null
, то его match
также будет иметь значение null
. Это означает, что, во-первых, любой дочерний маршрут/ссылка будут абсолютными (отсутствует предок для разрешения пути), во-вторых, маршрут без пути, родительский match
которого может иметь значение null
, должен будет использовать проп children
для рендеринга.
Позволяет использовать такой же код, что и <Route>
, но за пределами нормального цикла рендеринга, например, на сервере для сбора информации об используемых данных перед рендерингом.
import { matchPath } from "react-router"
const match = matchPath("/users/123", {
path: "/users/:id",
exact: true,
strict: false
})
Принимает следующие аргументы:
pathname
: string - название пути для поиска совпадения.req.path
на сервереprops
- пропы для поиска совпадения; соответствуют пропам, принимаемым<Route>
{
path, // например, /users/:id, строка или массив строк
strict, // опционально, по умолчанию false
exact, // опционально, по умолчанию false
}
При совпадении возвращается такой объект:
matchPath("/users/2", {
path: "/users/:id",
exact: true,
strict: true
})
// {
// isExact: true
// params: {
// id: "2"
// }
// path: "/users/:id"
// url: "/users/2"
// }
В противном случае, возвращается null
.
Общий низкоуровневый интерфейс для компонентов роутера. Обычно, используется один из следующих роутеров:
- <BrowserRouter>
- <HashRouter>
- <MemoryRouter>
- <StaticRouter>
<NativeRouter>
Одним из случаев использования низкоуровневого <Router>
является синхронизация пользовательской истории с такими библиотеками для управления состоянием приложения, как Redux
или MobX
. Обратите внимание, что делать этого не рекомендуется.
history
: object - атрибут, используемый для навигацииchildren
: node - дочерний элемент
<Router>
, использующий HTML5 history API
(события "pushState", "replaceState" и "popState") для синхронизации UI с URL.
<BrowserRouter
basename={optionalString}
forceRefresh={optionalBool}
getUserConfirmation={optionalFunc}
keyLength={optionalNumber}
>
<App />
</BrowserRouter>
basename
: string - базовый URL для всех локаций. Если приложение обслуживается из поддиректории на сервере, базовый URL должен иметь значение данной поддиректории
<BrowserRouter basename="/calendar">
<Link to="/today"/> // рендерится как <a href="/calendar/today">
<Link to="/tomorrow"/> // рендерится как <a href="/calendar/tomorrow">
...
</BrowserRouter>
getUserConfirmation
: func - функция для подтверждения перехода на другую страницу (по умолчанию используетсяwindow.confirm
)
<BrowserRouter
getUserConfirmation={(message, callback) => {
// поведение по умолчанию
const allowTransition = window.confirm(message)
callback(allowTransition)
}}
/>
forceRefresh
: bool - еслиtrue
, выполняется полное обновление страницы. Используется для имитации поведения сервера при переключении страницkeyLength
: number - длинаlocation.key
(по умолчанию равняется 6)children
: node - дочерние элементы
<Router>
, использующий хэш-часть URL (т.е. window.location.hash
) для синхронизации UI с URL.
Обратите внимание, что хэш-история не поддерживает location.key
или location.state
.
<HashRouter
basename={optionalString}
getUserConfirmation={optionalFunc}
hashType={optionalString}
>
<App />
</HashRouter>
basename
: string - базовый URL для всех локаций
<HashRouter basename="/calendar"/>
<Link to="/today"/> // рендерится как <a href="#/calendar/today">
getUserConfirmation
: func - функция для подтверждения перехода на другую страницу (по умолчанию используетсяwindow.confirm
)hashType
: string - тип кодировки дляwindow.location.hash
. Доступные значения:slash
-#/
и#/sunshine/lollipops
(значение по умолчанию)noslash
-#
и#sunshine/lollipops
hashbang
-#!/
и#!/sunshine/lollipops
(признан устаревшим в Chrome)
children
: node - дочерний элемент
<Router>
, хранящий историю URL в памяти (не читает и не пишет в адресную строку). Используется для тестирования и вне браузера (например, в React Native
).
<MemoryRouter
initialEntries={optionalArray}
initialIndex={optionalNumber}
getUserConfirmation={optionalFunc}
keyLength={optionalNumber}
>
<App />
</MemoryRouter>
initialEntries
: array - массив локаций в стеке истории. Локации могут быть полноценными объектами с{ pathname, search, hash, state }
или строками
<MemoryRouter
initialEntries={["/one", "/two", { pathname: "/three" }]}
initialIndex={1}
>
<App />
</MemoryRouter>
initialIndex
: number - индекс начальной локации в массивеinitialEntries
getUserConfirmation
: func - функция для подтверждения перехода на другую страницу (по умолчанию используетсяwindow.confirm
)keyLength
: number - длинаlocation.key
(по умолчанию - 6)children
: node - дочерние элементы
<Router>
, не изменяющий локацию. Используется в сценариях рендеринга на стороне сервера и для тестирования результатов рендеринга при подключении определенной локации.
Пример node-сервера, отправляющего статус-код 302 для <Redirect>
и обычный HTML в ответ на другие запросы:
import http from "http"
import React from "react"
import ReactDOMServer from "react-dom/server"
import { StaticRouter } from "react-router"
http
.createServer((req, res) => {
// Данный объект контекста содержит результаты рендеринга
const context = {}
const html = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
// context.url будет содержать URL для перенаправления при использовании <Redirect>
if (context.url) {
res.writeHead(302, {
Location: context.url
})
res.end()
} else {
res.write(html)
res.end()
}
})
.listen(3000)
basename
: string - базовый URL для всех локацийlocation
: string - URL, принимаемый сервером (req.url
)location
: object - объект локации вида{ pathname, search, hash, state }
context
: object - обычный JS-объект. В процессе рендеринга компоненты могут добавлять свойства к этому объекту для хранения информации о рендеринге
const context = {}
<StaticRouter context={context}>
<App />
</StaticRouter>
При совпадении, <Route>
передает компоненту, подлежащему рендерингу, объект контекста как проп staticContext
. После рендеринга, эти свойства могут использоваться для настройки ответа сервера:
if (context.status === "404") {
// ...
}
children
: node - дочерние элементы
Обеспечивает декларативный и доступный способ навигации в приложении.
<Link to="/about">Контакты</Link>
to
: string - строковое представление локации, создаваемое посредством объединения свойств "pathname", "search" и "hash"
<Link to="/courses?sort=name" />
to
: object - объект, который может иметь следующие свойства:pathname
: string - название путиsearch
: string - параметры строки запросаhash
: string - хэш-часть URLstate
: object - состояние локации
<Link
to={{
pathname: "/courses",
search: "?sort=name",
hash: "#the-hash",
state: { fromDashboard: true }
}}
/>
to
: func - функция, которой в качестве аргумента передается текущая локация и которая должна вернуть представление локации в виде строки или объекта
<Link to={location => ({ ...location, pathname: "/courses" })} />
<Link to={location => `${location.pathname}?sort=name`} />
replace
: bool - еслиtrue
, тогда новая локация заменяет текущую в стеке истории, а не добавляется к ней
<Link to="/courses" replace />
component
: React.Component - для реализации собственного компонента навигации достаточно передать данный проп
const FancyLink = React.forwardRef((props, ref) => (
<a ref={ref} {...props}>💅 {props.children}</a>
))
<Link to="/" component={FancyLink} />
- другие -
title
,id
,className
и т.д.
Специальная версия <Link>
, предназначенная для добавления стилей к элементу, совпадающему с текущим URL (активному элементу).
<NavLink to="/about">About</NavLink>
activeClassName
: string - класс активного элемента. Значением по умолчанию являетсяactive
. Объединяется с пропомclassName
<NavLink to="/faq" activeClassName="selected">
Часто задаваемые вопросы
</NavLink>
activeStyle
: object - стили, применяемые к активному элементу
<NavLink
to="/faq"
activeStyle={{
fontWeight: "bold",
color: "red"
}}
>
Часто задаваемые вопросы
</NavLink>
exact
: bool - еслиtrue
, тогда активный класс/стили применяются только при точном совпаденииstrict
: bool - еслиtrue
, тогда при определении совпадения учитывается замыкающий (последний) слэшisActive
: func - дополнительная логика для определения активности ссылки
<NavLink
to="/events/123"
isActive={(match, location) => {
if (!match) {
return false
}
// событие считается активным только при условии, что его идентификатор является нечетным числом
const eventID = parseInt(match.params.eventID)
return !isNaN(eventID) && eventID % 2 === 1
}}
>
Событие 123
</NavLink>
location
: object - используется для сравнения с другой локациейaria-current
: string - значение атрибутаaria-current
активной ссылки. Возможные значения:page
(значение по умолчанию),step
,location
,date
,time
,true
,false
.
Рендерит первый дочерний <Route>
или <Redirect>
при совпадении с текущей локацией (URL).
В чем состоит особенность <Switch>
?
Особенность <Switch>
состоит в том, что он рендерит только один маршрут из набора. Без <Switch>
рендерится каждый маршрут, совпавший с локацией.
import { Route } from "react-router"
const routes = (
<div>
<Route path="/about">
<About />
</Route>
<Route path="/:user">
<User />
</Route>
<Route>
<NoMatch />
</Route>
</div>
)
В приведенном примере при значении URL, равном /about
, все 3 компонента будут совпадать с локацией и отрендерятся. Это позволяет выстраивать композицию компонентов ("сайдбары" - sidebars, "хлебные крошки" - breadcrumbs, "табы" - tabs и т.д.).
Тем не менее, обычно, мы хотим рендерить только один <Route>
. Если мы находимся в /about
, то едва ли мы хотим рендерить компонент <User />
или страницу ошибки 404. Вот как добиться этого с помощью <Switch>
:
import { Route, Switch } from "react-router"
const routes = (
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/:user">
<User />
</Route>
<Route>
<NoMatch />
</Route>
</Switch>
)
Это также может быть полезным для выполнения анимированных переходов, поскольку новый маршрут рендерится на той же позиции, что и предыдущий.
location
: object - используется для поиска совпадения вместо текущего объекта локации (текущего браузерного URL)children
: node - потомки<Switch>
должны быть элементами<Route>
или<Redirect>
.<Route>
совпадает по пропуpath
,<Redirect>
- по пропуfrom
. Указанные элементы без соответствующих пропов всегда совпадают. При включении<Redirect>
в<Switch>
, в последнем могут использоваться все пропы<Route>
(path
,exact
иstrict
). Пропfrom
- это синоним пропаpath
. Если в<Switch>
передан пропlocation
, значениеlocation
совпавшего потомка будет перезаписано.
import { Redirect, Route, Switch } from "react-router"
let routes = (
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/users">
<Users />
</Route>
<Redirect from="/accounts" to="/users" />
<Route>
<NoMatch />
</Route>
</Switch>
)
Данный компонент является ключевым в React Router
. Поэтому важно хорошо знать, как он работает. В основном, он отвечает за рендеринг определенного UI при совпадении значения его пропа path
с текущим URL.
Рассмотрим следующий код:
import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter as Router, Route } from "react-router-dom"
ReactDOM.render(
<Router>
<div>
<Route exact path="/">
<Home />
</Route>
<Route path="/news">
<NewsFeed />
</Route>
</div>
</Router>,
node
)
Если локацией приложения будет /
, то иерархия UI будет выглядеть так:
<div>
<Home />
<!-- react-empty: 2 -->
</div>
А для локации /news
так:
<div>
<!-- react-empty: 1 -->
<NewsFeed />
</div>
Комментарии "react-empty" - это детали реализации нулевого рендеринга. Технически, маршрут рендерится всегда: при совпадении с текущей локацией рендерится компонент, при несовпадении - null
. Если один и тот же компонент является потомком нескольких <Route>
, React
будет рассматривать его как один и тот же экземпляр и состояние компонента будет автономным, т.е. компонент не будет перерисовываться при изменении локации. Во избежание этого к каждому <Route>
добавляется проп key
с уникальным значением.
Рекомендуемым способом рендеринга является использование дочерних элементов как в приведенном выше примере. Тем не менее, существует и другие способы:
<Route component>
<Route render>
<Route children>
Следует придерживаться одного стиля.
Все 3 метода рендеринга принимают следующие пропы:
match
location
history
Компонент, подлежащий рендерингу при совпадении с локацией. Данному компоненту доступны все пропы маршрута:
import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter as Router, Route } from "react-router-dom"
// Все пропы маршрута (match, location и history) доступны для User
const User = (props) => <h1>Привет, {props.match.params.username}!</h1>
ReactDOM.render(
<Router>
<Route path="/user/:username" component={User} />
</Router>,
node
)
При использовании component
(вместо render
или children
), маршрут использует React.createElement
для создания нового React-элемента. Это означает, что при передаче встроенной функции в качестве пропа компонента, при каждом рендеринге будет создаваться новый компонент. Это приводит к размонтированию/монтированию нового компонента вместо обновления существующего. Поэтому при необходимости передачи встроенной функции следует использовать render
или children
.
Это позволяет реализовать встроенный рендеринг без нежелательного перемонтирования.
Вместо создания нового элемента с помощью пропа component
, можно передать функцию, вызываемую при совпадении с локацией. Данная функция имеет доступ ко всем пропам маршрута:
import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter as Router, Route } from "react-router-dom"
// встроенный рендеринг
ReactDOM.render(
<Router>
<Route path="/home" render={() => <div>Главная</div>} />
</Router>,
node
)
// композиция
// Вы можете распаковать routeProps, чтобы сделать их доступными для Component
const FadingRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={routeProps => (
<FadeIn>
<Component {...routeProps} />
</FadeIn>
)}
/>
)
ReactDOM.render(
<Router>
<FadingRoute path="/cool" component={Something} />
</Router>,
node
)
Порой нам требуется условный рендеринг. В этом случае можно использовать функцию в качестве значения пропа children
. Она работает как render
, за исключением того, что вызывается в зависимости от совпадения с локацией.
Данный проп принимает те же пропы маршрута. При несовпадении маршрута, проп match
будет иметь значение null
. Это позволяет динамически обновлять UI. В приведенном ниже примере мы динамически добавляем класс active
:
import React from "react"
import ReactDOM from "react-dom"
import {
BrowserRouter as Router,
Link,
Route
} from "react-router-dom"
const ListItemLink = ({ to, ...rest }) => (
<Route
path={to}
children={({ match }) => (
<li className={match ? "active" : ""}>
<Link to={to} {...rest} />
</li>
)}
/>
)
ReactDOM.render(
<Router>
<ul>
<ListItemLink to="/somewhere" />
<ListItemLink to="/somewhere-else" />
</ul>
</Router>,
node
)
Это также может использоваться для анимации:
<Route
children={({ match, ...rest }) => (
{/* Animate будет рендерится всегда, поэтому можно использовать жизненный цикл
для анимации монтирования/размонтирования его потомков */}
<Animate>
{match && <Something {...rest}/>}
</Animate>
)}
/>
path
: string, string[] - любой валидный URL или массив таких URL (маршрут безpath
совпадает с любой локацией)
<Route path="/users/:id">
<User />
</Route>
<Route path={["/users/:id", "/profile/:id"]}>
<User />
</Route>
exact
: bool - еслиtrue
, осуществляется поиск точного совпадения сlocation.pathname
<Route exact path="/one">
<About />
</Route>
strict
: bool - еслиtrue
, при поиске совпадения учитывается замыкающий слэшlocation
: object - используется для поиска совпадения с указанной локацией вместо текущейsensitive
: bool - еслиtrue
, при поиске совпадения учитывается регистр
Рендеринг <Redirect>
приводит к перемещению в новую локацию. Новая локация перезаписывает текущую в стеке истории, как при серверном перенаправлении (HTTP 3xx).
<Route exact path="/">
{loggedIn ? <Redirect to="/dashboard" /> : <PublicHomePage />}
</Route>
to
: string - любой валидный URLto
: object - объект локации для перенаправления
<Redirect
to={{
pathname: "/login",
search: "?utm=your+face",
state: { referrer: currentLocation }
}}
/>
В компоненте Login
объект state
доступен через props.location.state
, а ключ referrer
- через props.state.referrer
.
push
: bool - еслиtrue
, новая локация добавляется в стек истории, а не заменяет текущуюfrom
: string - любой валидный URL. Может использоваться только при включении в<Switch>
<Switch>
<Redirect from="/old-path" to="/new-path" />
<Route path="/new-path">
<Place />
</Route>
</Switch>
// Перенаправление с совпавшими параметрами
<Switch>
<Redirect from="/users/:id" to="/users/profile/:id" />
<Route path="/users/profile/:id">
<Profile />
</Route>
</Switch>
exact
: bool - точное совпадение, аналогRoute.exact
. Может использоваться только совместно сfrom
strict
: bool - учет замыкающего слэша, аналогRoute.strict
. Может использоваться только совместно сfrom
sensitive
: bool - учет регистра, аналогRoute.sensitive
Используется для обращения к пользователю перед переключением страницы. Это может быть полезным для предотвращения преждевременного или случайного переключения страницы пользователем (например, при наполовину заполненной форме).
<Prompt
when={formIsHalfFilledOut}
message="Вы уверены, что хотите покинуть страницу?"
/>
message
: string - сообщение, выводимое при попытке пользователя перейти на другую страницуmessage
: func - вызывается со следующей локацией и операцией при попытке пользователя перейти на другую страницу. Возвращает строку - сообщение для пользователя илиtrue
для выполнения перехода
<Prompt
message={(location, action) => {
if (action === 'POP') {
console.log("Возвращаемся назад...")
}
return location.pathname.startsWith("/app")
? true
: `Вы уверены, что хотите перейти к ${location.pathname}?`
}}
/>
when
: bool - вместо условного рендеринга<Prompt>
, можно рендерить его всегда, передаваяwhen={true}
илиwhen={false}
для контроля навигации
Функция generatePath
может вызываться для генерации URL для маршрутов. В ней используется библиотека path-to-regexp
.
import { generatePath } from "react-router"
generatePath("/user/:id/:entity(posts|comments)", {
id: 1,
entity: "posts"
})
// Возвращается /user/1/posts
Результаты компиляции путей кэшируются, так что генерация множества путей на основе одного шаблона не приводит к лишнему расходу памяти.
pattern
: string - шаблон, передаваемый как атрибутpath
в<Route>
params
: object - объект с параметрами для шаблона. При несовпадении параметров и пути, выбрасывается исключение
generatePath("/user/:id/:entity(posts|comments)", { id: 1 })
// TypeError: Expected "entity" to be defined (Ожидалось, что "entity" будет определено)
С помощью компонента высшего порядка withRouter
можно получить доступ к свойствам объекта history
и match
ближайшего маршрута. withRouter
передает дочернему компоненту обновленные пропы match
, location
и history
при его рендеринге:
import React from "react"
import PropTypes from "prop-types"
import { withRouter } from "react-router"
// Простой компонент, отображающий название пути текущей локации
class ShowTheLocation extends React.Component {
static propTypes = {
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}
render() {
const { match, location, history } = this.props
return <div>Вы находитесь здесь: {location.pathname}</div>
}
}
// Создаем новый компонент, который "подключен" к роутеру
const ShowTheLocationWithRouter = withRouter(ShowTheLocation)
Обратите внимание, withRouter
не подписывается на изменение локации подобно подключению React Redux
к изменению состояния. Повторный рендеринг после изменения локации "распространяется" от <Router>
. Это означает, что withRouter
перерисовывается только вслед за родительским компонентом.
Статические методы и свойства (не принадлежащие React) оборачиваемого компонента автоматически копируются в "подключенный" компонент.
Component.WrappedComponent
- оборачиваемый компонент преобразуется в статическое свойствоWrappedComponent
возвращаемого компонента, что, помимо прочего, может использоваться для тестирования компонента в изоляции.
// MyComponent.js
export default withRouter(MyComponent)
// MyComponent.test.js
import MyComponent from './MyComponent'
render(<MyComponent.WrappedComponent location={{...}} ... />)
wrappedComponentRef
: func - функция, передаваемая оборачиваемому компоненту в качестве пропаref
class Container extends React.Component {
componentDidMount() {
this.component.doSomething()
}
render() {
return (
<MyComponent wrappedComponentRef={c => (this.component = c)} />
)
}
}
Маршрутизация на стороне сервера немного отличается ввиду отсутствия состояния. Основная идея состоит в том, что компонент верхнего уровня оборачивается в <StaticRoute>
вместо <BrowserRoute>
. Затем мы передаем роутеру запрашиваемый URL и контекст.
// клиент
<BrowserRouter>
<App/>
</BrowserRouter>
// сервер (это еще не все)
<StaticRouter
location={req.url}
context={context}
>
<App/>
</StaticRouter>
При рендеринге <Redirect>
на клиенте, история браузера изменяется, и мы получаем новое представление (видим новый экран). В статическом серверном окружении мы не может менять состояние приложения. Вместо этого, мы используем проп context
для определения результата рендеринга. Если мы обнаруживаем context.url
, значит, имело место перенаправление. Это позволяет выполнять перенаправление на сервере.
const context = {}
const markup = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
if (context.url) {
// где-то был отрендерен <Redirect>
redirect(301, context.url)
} else {
// все в порядке, отправляем ответ
}
Роутер просто добавляет context.url
. Однако, вам может потребоваться указать код 301 или 302. Или вы хотите отправлять ответ 404 для некоторых частей UI, или 401 при отсутствии авторизации. Проп context
находится в вашем распоряжении, так что вы можете его изменять. Ниже приводится способ указания кодов 301 и 302:
const RedirectWithStatus = ({from, to, status}) => (
<Route
render={({ staticContext }) => {
// на клиенте отсутствует `staticContext`
// так что следует выполнить проверку
if (staticContext) staticContext.status = status
return <Redirect from={from} to={to} />
}}
/>
)
// где-то в приложении
const App = () => (
<Switch>
{/* другие маршруты */}
<RedirectWithStatus
status={301}
from="/users"
to="/profiles"
/>
<RedirectWithStatus
status={302}
from="/courses"
to="/dashboard"
/>
</Switch>
)
// на сервере
const context = {}
const markup = ReactDOMServer.renderToString(
<StaticRouter context={context}>
<App />
</StaticRouter>
)
if (context.url) {
// мы можем использовать `context.status`,
// который мы добавили в RedirectWithStatus
redirect(context.status, context.url)
}
Делаем тоже самое. Создаем компонент, добавляющий контекст, и рендерим его в приложении для получения разных статус-кодов.
const Status = ({code, children}) => (
<Route
render={({ staticContext }) => {
if (staticContext) staticContext.status = status
return children
}}
/>
)
После этого вы можете рендерить Status
где угодно для получения кода для staticContext
:
const NotFound = () => (
<Status code={404}>
<div>
<h1>Запрашиваемая страница отсутствует.</h1>
</div>
</Status>
)
const App = () => (
<Switch>
<Route path="/about" component={About} />
<Route path="/dashboard" component={Dashboard} />
<Route component={NotFound} />
</Switch>
)
Сервер:
import http from 'http'
import React from 'react'
import ReactDOMServer from "react-dom/server"
import { StaticRouter } from "react-router-dom"
import App from "./App.js"
http
.createServer((req, res) => {
const context = {}
const html = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
if (context.url) {
res.writeHead(301, {
Location: context.url
})
res.end()
} else {
res.write(`
<!doctype html>
<div id="root">${html}</div>
`)
res.end()
}
})
.listen(3000)
Клиент:
import ReactDOM from "react-dom"
import { BrowserRouter } from "react-router-dom"
import App from "./App"
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
)
Данные должны загружаться перед рендерингом. React Router
предоставляет статическую функцию matchPath
, которая используется для определения маршрута. Вы можете использовать эту функцию на сервере для определения данных, которые потребуются приложению, перед рендерингом.
Суть данного подхода заключается в использовании статических настроек для определения данных по маршруту:
const routes = [
{
path: "/",
component: Root,
loadData: () => getSomeData()
}
]
Затем этот конфиг используется для рендеринга маршрутов:
import { routes } from "./routes.js"
const App = () => (
<Switch>
{routes.map(route => (
<Route {...route} />
))}
</Switch>
)
Сервер:
import { matchPath } from 'react-router-dom'
// внутри запроса
const promises = []
// используем `some` для имитации поведения `<Switch>` при выборе
// первого совпадения
routes.some(route => {
// используем `matchPath`
const match = matchPath(req.path, route)
if (match) promises.push(route.loadData(match))
return match
})
Promise.all(promises).then(data => {
// обрабатываем данные, чтобы клиент
// имел к ним доступ при рендеринге приложения
})
Наконец, клиент должен каким-либо способом получить данные.
Одной из замечательных возможностей веба является то, что нам не нужно заставлять пользователей загружать все приложение целиком. Вы можете думать о разделении кода как о постепенной загрузке приложения. Для реализации этого мы будем использовать webpack
, @babel-plugin-syntax-dynamic-import
и loadable-components
.
Webpack
имеет встроенную поддержку динамического импорта, однако, при использовании Babel
для компиляции JSX в JavaScript требуется @babel-plugin-syntax-dynamic-import
. Данный плагин позволяет Babel разбирать (парсить) динамический импорт, чтобы вебпак мог осуществить разделение кода. Ваш файл .babelrc
должен выглядеть примерно так:
{
"presets": ["@babel/preset-react"],
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
loadable-components
- это библиотека для загрузки компонентов с помощью динамического импорта. Она значительно облегчает разделения кода за счет автоматической обработки всех крайних случаев.
import loadable from "@loadable/component"
import Loading from "./Loading.js"
export const LoadableComponent = loadable(() => import("./Dashboard.js"), {
fallback: <Loading />
})
Это все, что нужно сделать. Просто используйте данный компонент в своем приложении. fallback
- это компонент-заместитель на время загрузки настоящего компонента. loadable-components
также может использоваться при рендеринге на стороне сервера.
В большинстве случаев нам требуется выполнить прокрутку на верх страницы из-за длинного контента, который при навигации, остается "прокрученным". Этого можно добиться с помощью компонента <ScrollToTop>
, который будет выполнять прокрутку при каждой навигации:
import { useEffect } from "react"
import { useLocation } from "react-router-dom"
export default function ScrollToTop() {
const { pathname } = useLocation()
useEffect(() => {
window.scrollTo(0, 0)
}, [pathname])
return null
}
Помещаем этот компонент на верхнем уровне приложения, но внутри роутера:
const App = () => (
<Router>
<ScrollToTop />
<App />
</Router>
)
Если в вашем приложении имеются вкладки, подключенные к роутеру, тогда, возможно, прокрутка на верх страницы при каждом переключении вкладки - это не то, что вам нужно. Как насчет компонента <ScrollToTopInMount>
, выполняющего прокрутку к месту его расположения:
import { useEffect } from 'react'
function ScrollToTopOnMount() {
useEffect(() => {
window.scrollTo(0, 0)
}, [])
}
// рендерим данный компонент где-либо в приложении с помощью:
// <Route path="..." children={<LongContent />} />
const LongContent = () => (
<div>
<ScrollToTopOnMount />
<h1>Страница с длинным контентом</h1>
<p>...</p>
</div>
)