image

Автор @pshrmn ? Оригинальная статья ? Время чтения: 10 минут

React Router v4 — это переработанный вариант популярного React дополнения. Зависимые от платформы конфигурации роутов из прошлой версии были удалены и теперь всё является простыми компонентами.

Этот туториал покрывает всё что вам нужно для создания веб-сайтов с React Router. Мы будем создавать сайт для локальной спортивной команды.

Хочешь посмотреть демку?



Установка


React Router v4 был разбит на 3 пакета:

react-router

router-dom

react-router-native

react-router предоставляет базовые функции и компоненты для работы в двух окружениях(Браузере и react-native)

Мы будем создавать сайт который будет отображаться в браузере, поэтому нам следует использовать react-router-dom. react-router-dom экспортирует из react-router все функции поэтому нам нужно установить только react-router-dom.

npm install --save react-router-dom

Router


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

Обычно предпочтительнее использовать BrowserRouter, но если ваш сайт расположен на статическом сервере(от перев. как github pages), то использовать HashRouter это хорошее решение проблемы.
Наш проект предполагает использование бекенда поэтому мы будем использовать BrowserRouter.

История — History


Каждый Router создает объект history который хранит путь к текущему location[1] и перерисовывает интерфейс сайта когда происходят какие то изменения пути.

Остальные функции предоставляемые в React Router полагаются на доступность объекта history через context, поэтому они должны рендериться внутри компонента Router.

Заметка: Компоненты React Router не имеющие в качестве предка компонент Router не будут работать, так как не будет доступен context.

Рендеринг Router


Компонент Router ожидает только один элемент в качестве дочернего. Что бы работать в рамках этого условия, удобно создать компонент <App/> который рендерить всё ваше приложение(это так же важно для серверного рендеринга).

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

ReactDOM.render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('root'))

App компонент


Наше приложение начинается с <App/> компонента который мы разделим на две части. <Header/> который будет содержать навигационные ссылки и <Main/> который будет содержать контент роутов.

// Этот компонент будет отрендерен с помощью нашего <Router>
const App = () => (
  <div>
    <Header />
    <Main />
  </div>
)

Routes


<Route/> компонент это главный строительный блок React Router'а. В том случае если вам нужно рендерить элемент в зависимости от pathname URL'ов, то следует использовать компонент <Route/>

Path — путь


<Route /> принимает path в виде prop который описывает определенный путь и сопоставляется с location.pathname.?

<Route path='/roster'/>

В примере выше <Route/> сопоставляет location.pathname который начинается с /roster[2]. Когда текущий location.pathname сопоставляется положительно с prop path то компонент будет отрендерен, а если мы не можем их сопоставить, то Route ничего не рендерит[3].


<Route path='/roster'/>
// Когда location.pathname это '/', prop path не совпадает
// Когда location.pathname это '/roster' или '/roster/2', prop path совпадает
// Если установлен exact prop. Совпадает только строгое сравнение '/roster', но не
// '/roster/2'
<Route exact path='/roster'/>

Заметка: Когда речь идет о пути React Router думает только о пути без домена. Это значит, что в адресе:

http://www.example.com/my-projects/one?extra=false

React Router будет видеть только /my-projects/one

Сопоставление пути


npm пакет path-to-regexp компилирует prop path в регулярное выражение и сопоставляет его против location.pathname. Строки path имеют более сложные опции форматирования чем объясняются здесь. Вы можете почитать документацию.

Когда пути сопоставляются создается объект match который содержит свойства:

  • url — сопоставляемая часть текущего location.pathname
  • path — путь в компоненте Route
  • isExact — path в Route === location.pathname
  • params — объект содержит значения из path которые возвращает модуль path-to-regexp

Заметка: Можете поиграться с тестером роутов и посмотреть как создается объект match.

Заметка: path в Route должен быть абсолютным[4].

Создание наших роутов


Компонент Route может быть в любом месте в роутере, но иногда нужно определять, что рендерить в одно и тоже место. В таком случае следует использовать компонент группирования Route'ов — <Switch/>. <Switch/> итеративно проходит по дочерним компонентам и рендерит только первый который подходит под location.pathname.

У нашего веб-сайта пути которые мы хотим сопоставлять такие:

  • / — Главная страница
  • /roster — Страница команд
  • /roster/:number — Страница профиля игрока по номеру
  • /schedule — Расписание игр команды

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

<Switch>
  <Route exact path='/' component={Home}/>
  {/* Оба /roster и /roster/:number начинаются с /roster */}
  <Route path='/roster' component={Roster}/>
  <Route path='/schedule' component={Schedule}/>
</Switch>

Что делает рендер компонента Route?


У Route есть 3 props'a которые описывают каким образом выполнить рендер сопоставляя prop path с location.pathname и только один из prop должен быть представлен в Route:

  • component — React компонент. Когда роут удовлетворяется сопоставление в path, то он возвращает переданный component (используя функцию React.createElement).
  • render — функция которая должна вернуть элемент React. Будет вызвана когда удовлетворится сопоставление в path. Render довольно похож на component, но используется для inline рендеринга и подстановки необходимых для элемента props[5].
  • children — в отличие от предыдущих двух props children будет всегда отображаться независимо от того сопоставляется ли path или нет.

<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
  <Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
  props.match
    ? <Page {...props}/>
    : <EmptyPage {...props}/>
)}/>

В типичных ситуациях следует использовать component или render. Children prop может быть использован, но лучше ничего не делать если path не совпадает с location.pathname.

Элементу отрендеренному Route будет передано несколько props. match — объект сопоставления path с location.pathname, location объект[6] и history объект(созданный аим роутом)[7].

Main


Сейчас мы опишем основную структуру роутера. Нам просто нужно отобразить наши маршруты. Для нашего приложения мы будем использовать компонент <Switch/> и компонент <Route/> внутри нашего компонента <Main/> который поместит сгенерированный HTML удовлетворяющий сопоставлению path внутри.

<Main/> DOM узла(node)
import { Switch, Route } from 'react-router-dom'
const Main = () => (
  <main>
    <Switch>
      <Route exact path='/' component={Home}/>
      <Route path='/roster' component={Roster}/>
      <Route path='/schedule' component={Schedule}/>
    </Switch>
  </main>
)

Заметка: Route для главной страницы содержит prop exact, благодаря которому пути сравниваются строго.

Унаследованные роуты


Профиль игрока /roster/:number не включен в <Switch/>. Вместо этого он будет рендериться компонентом <Roster/> который рендериться всякий раз когда путь начинается с /roster.

В компоненте Roster мы создадим компоненты для двух путей:

  • /roster — с prop exact
  • /roster/:number — этот route использует параметр пути, который будет отловлен после /roster

const Roster = () => (
  <Switch>
    <Route exact path='/roster' component={FullRoster}/>
    <Route path='/roster/:number' component={Player}/>
  </Switch>
)

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

К примеру <Roster/> может быть отрендерен с заголовком который будет отображаться во всех роутах которые начинаются с /roster.

const Roster = () => (
  <div>
    <h2>This is a roster page!</h2>
    <Switch>
      <Route exact path='/roster' component={FullRoster}/>
      <Route path='/roster/:number' component={Player}/>
    </Switch>
  </div>
)

Параметры в path


Иногда нам требуется использовать переменные для получения какой либо информации. К примеру, роут профиля игрока, где нам требуется получить номер игрока. Мы сделали это добавив параметр в prop path.

:number часть строки в /roster/:number означает, что часть path после /roster/ будет получена в виде переменной и сохранится в match.params.number. К примеру путь /roster/6 сгенерирует следующий объект с параметрами:

{ number: '6' // Любое переданное значение интерпретируется как строка}

Компонент <Player/> будет использовать props.match.params для получения нужной информации которую следует отрендерить.

// API возращает информацию об игроке в виде объекта
import PlayerAPI from './PlayerAPI'
const Player = (props) => {
  const player = PlayerAPI.get(
    parseInt(props.match.params.number, 10)
  )
  if (!player) {
    return <div>Sorry, but the player was not found</div>
  }
  return (
    <div>
      <h1>{player.name} (#{player.number})</h1>
      <h2>{player.position}</h2>
    </div>
)

Заметка: Вы можете больше изучить о параметрах в путях в пакете path-to-regexp

Наряду с компонентом <Player/> наш веб-сайт использует и другие как <FullRoster/>, <Schedule/> и <Home/>.

const FullRoster = () => (
  <div>
    <ul>
      {
        PlayerAPI.all().map(p => (
          <li key={p.number}>
            <Link to={`/roster/${p.number}`}>{p.name}</Link>
          </li>
        ))
      }
    </ul>
  </div>
)
const Schedule = () => (
  <div>
    <ul>
      <li>6/5 @ Спартак</li>
      <li>6/8 vs Зенит</li>
      <li>6/14 @ Рубин</li>
    </ul>
  </div>
)
const Home = () => (
  <div>
    <h1>Добро пожаловать на наш сайт!</h1>
  </div>
)

Ссылки


Последний штрих, наш сайт нуждается в навигации между страницами. Если мы создадим обычные ссылки то страница будет перезагружаться. React Router решает эту проблему компонентом <Link/> который предотвращает перезагрузку. Когда мы кликаем на <Link/> он обновляет URL и React Router рендерит нужный компонент без обновления страницы.

import { Link } from 'react-router-dom'
const Header = () => (
  <header>
    <nav>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/roster'>Roster</Link></li>
        <li><Link to='/schedule'>Schedule</Link></li>
      </ul>
    </nav>
  </header>
)

<Link/> использует prop to для описания URL куда следует перейти. Prop to может быть строкой или location объектом (который состоит из pathname, search, hash, state свойств). Если это строка то она конвертируется в location объект.

<Link to={{ pathname: '/roster/7' }}>Player #7</Link>

Заметка: Пути в компонентах <Link/> должны быть абсолютными[4].

Работающий пример


Весь код нашего веб сайта доступен по этому адресу на codepen.

Route готов!


Надеюсь теперь вы готовы погрузиться в изучение деталей маршрутизации веб приложений.

Мы использовали самые основные компоненты которые вам понадобятся при создании собственных веб приложений (<BrowserRouter.>, <Route.>, and <Link.>), но есть еще несколько компонентов и props которые здесь не рассмотрены. К счастью у React Router есть прекрасная документация где вы можете найти более подробное объяснение компонентов и props. Так же в документации предоставляются работающие примеры с исходным кодом.

Пояснения


[1] — Объект location описывает разные части URL'a

// стандартный location 
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }

[2] — Вы можете использовать компонент <Route/> без path. Это полезно для передачи методов и переменных которые храняться в context.

[3] — Если вы используете prop children то route будет отрендерен даже есть path и location.pathname не совпадают.

[4] — Сейчас ведется работа над относительными путями в <Route/> и <Link/>. Относительные <Link/> более сложные чем могут показаться, они должны быть разрешены используя свой родительский объект match, а не текущий URL.

[5] — Это stateless компонент. Внутри есть большая разница между render и component. Component использует React.createElement для создания компонента, в то время как render используется как функция. Если бы вы определили inline функцию и передали через нее props то это было бы намного медленнее чем с использованием функции render.

<Route path='/one' component={One}/>
// React.createElement(props.component)
<Route path='/two' render={() => <Two />}/>
// props.render()

[6] — Компоненты <Route/> и <Switch/> могут оба использовать prop location. Это позволяет сопоставлять их с path, который фактически отличается от текущего URL'а.

[7] — Так же передают staticContext, но он полезен только при рендере на сервере.
Поделиться с друзьями
-->

Комментарии (8)


  1. vitvad
    02.06.2017 01:30
    +2

    В третьем react-route были хуки типа `onEnter` которые можно было использовать к примеру, если есть авторизация или подгрузка бандла с кодом для соответсвующей страницы. У меня лично попытка мигрировать на 4ю версию вызвала много-много нехороших слов в ee адресс.
    Сейчас к сожалению не вспомню на что еще натыкался…

    Кроме того документация хоть и выглядит красиво, ИМХО не дает ответы на возникающие вопросы, и ведет на страничку где ребята предлагают вам свои же платные курсы…


    1. boolive
      02.06.2017 02:44
      +2

      Оберни в компонент и реализуй в нём логику подгрузки/авторизации в методе componentWillMount замен onEnter роутера.


      1. vitvad
        03.06.2017 00:45

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

        только что наткнулся на статью Progressive Web Apps with React.js от Addy Osmani, довольно занимательная, но там в комментариях есть интерестный модуль react-async-component который собственно должен решать все поднятые вопросы (сам еще не пробовал, если кто-то имел дело интерестно знать впечатления)


  1. Karkat
    02.06.2017 05:24

    Кто-нибудь знает куда делась функция match? Как теперь такой пример будет выглядеть?


  1. TheRexx
    02.06.2017 05:30

    Встречал комментарии, что React Router v4 не совместим с Redux. Как реально обстоят дела с поддержкой? Есть ли хороший туториал на примете по их совместной работе?


    1. eks1985
      02.06.2017 09:48

      Как же не совместим, вот — https://github.com/reactjs/react-router-redux
      Пример как использовать можно посмотреть в этом боилерплейте https://github.com/react-boilerplate/react-boilerplate


      1. Karkat
        02.06.2017 11:04

        А вы посмотрите в boilerplate версию react-router.
        В react-router-redux используется 3-я версия.
        This repo is for react-router-redux 4.x, which is only compatible with react-router 2.x and 3.x


    1. justboris
      02.06.2017 11:16
      +1

      ReactRouterRedux переехал и теперь живет в одном репо с ReactRouter
      https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux


      попробуйте поставить его оттуда через команду


      npm install --save react-router-redux@next