Что такое SPA?

SPA (single page application) переводится как “одностраничное приложение”, очень распространенный способ программирования веб-сайтов в наши дни: идея в том, что сайт загружает весь нужный для пользовательского опыта HTML/JS сразу же при первом посещении главной страницы, а при последующих переходах по страницам браузер лишь просматривает содержимое заново, не обновляя сайт.

Одностраничные приложения на JavaScript-фреймворках улучшают удобство веб-сайта для посетителя благодаря непрерывности пользовательского опыта; перейдя на страницу веб-сайта вы сразу сможете определить  —  это SPA или многостраничное приложение: всего лишь быстро нажмите несколько ссылок в навигации. Многостраничное приложение будет перезагружаться, заставляя весь пользовательский интерфейс быстро мигать в зависимости от содержимого, происходит подобное из-за обновления сайта. Напротив, SPA плавно отображается всегда, в любой момент, поскольку такое приложение просто показывает пользователю другой контент без обновления страницы веб-сайта в его браузере.

В руководстве рассмотрим, как написать single page application с помощью React Router

Оглавление:

  1. Что такое SPA?
  2. Приложение на React с нуля.
  3. React Router: маршрутизация в React.
  4. UseParams: передача данных в ссылку.
  5. UseLocation: передача данных через React Router.
  6. UseHistory: программная маршрутизация.
  7. Выводы.

Приложение на React с нуля

Сначала создайте новый файл-шаблон приложения, выполните команду:

create-react-app app

Войдите в только что созданную папку app и установите React-Router, необходимый для написания SPA.

npm i react-router-dom --save

Для начала измените только что созданное приложения: откройте файл src/App.js и замените весь код из него на следующий:

import './App.css';
import Main from './layouts/main';

function App() {
  return (
    <Main></Main>
  );
}

export default App;

Уже заметили, что функция App() возвращает компонент <Main></Main>? Соответственно, следующий шаг в разработке SPA  —  написать компонент <Main>, чтобы приложение не упало. Создайте новую папку layouts, а в ней  —  файл layouts/main.js: в данном файле будем хранить базовый макет приложения.

mkdir layouts
touch layouts/main.js

Для начала, мы создадим в файле main.js навигационную панель; она будет выглядеть недостаточно хорошо с точки зрения дизайна, но ведь вы читаете руководство ради изучения React Router, а не ради готовой CSS-вёрстки. Навигация содержит ссылки на страницу с информацией о сайте и страницу с контактами. 

Сразу под навигацией создадим блок <div></div> с контентом для отображения расширенной информации при пользовательском нажатии на выбранную ссылку; такие контент-блоки  —  это крайне распространенная настройка для одностраничного приложения на React: создаётся статичный макет сайта с навигацией и карточка, динамически меняющая свое содержимое. Рассмотрите следующий пример повнимательнее:

import Navbar from '../components/navbar/navbar'
function Main() {
    return (
        <div>
            <Navbar></Navbar>
            <div className="content">
                
            </div>
        </div>
    )
}

export default Main;

Main.js  —  это макет приложения по умолчанию

Теперь давайте расширим ссылками элемент навигационной панели, а затем посетим только что созданную страницу: создайте в существующей директории src новую директорию с названием components, в ней мы сохраним все компоненты React-приложения; затем создайте файл components/navbar/navbar.js.

mkdir -p components/navbar/
touch components/navbar/navbar.js

function Navbar() {
    return (
        <div className="navbar">
            <h1>Navbar</h1>
            <ul>
                <li><a href="/">Home</a></li>
                <li><a href="/about">about</a></li>
                <li><a href="/contact">Contact</a></li>
            </ul>
        </div>
    )
}

export default Navbar;

Navbar.js  —  простой список ссылок для навигации

Наконец, откройте файл src/index.css и вставьте следующий фрагмент CSS, ведь он поможет красиво отобразить навигационную панель в виде горизонтального списка:

.navbar ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

.navbar li {
  float: left;
}

.navbar li a {
  display: block;
  padding: 8px;
  background-color: #dddddd;
}

index.css  —  добавление горизонтальной навигационной панели

Запустите приложение и откройте в браузере ссылку localhost:3000, чтобы полюбоваться на свои труды  —  ваше первое React single page application:

npm start
Ваша первая, пока что еще не до конца рабочая панель навигации должна выглядеть как на скриншоте

Прежде чем приступить к реализации React Router, нам нужно создать три очень простых компонента: они будут только лишь выводить на экран свое название.

mkdir components/about
mkdir components/contact
mkdir components/home

function About() {
    return (
        <h1>About</h1>
    )
}

export default About;

Скопируйте и вставьте приведенный выше код в три созданных ранее файла компонентов, лишь измените в каждом из них название на соответствующее конкретной странице сайта. Таким образом, у вас должны получиться следующие компоненты React-приложения: components/about/About.js, components/contact/Contact.js и components/home/Home.js, причём код во всех этих файлах должен быть одинаково похож на пример выше, лишь с измененными названиями внутри тегов <h1></h1>.


React Router

Маршрутизация в React

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

Для этого потребуется изменить два файла: в файле layouts/main.jsh необходимо подключить собственно Router, а в файле components/navbar/navbar.js придётся заменить все теги <a> на компонент Link из React Router. Не беспокойтесь, в руководстве детально рассматривается каждый шаг разработки, так что вы сможете досконально изучить, что делает каждая строчка кода и что представляет собою каждый из компонентов приложения на React.

Начнем с добавления в main.js следующего импорта:

import  { HashRouter, Switch, Route } from "react-router-dom";

Импорт необходимых компонентов из react-router-dom

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

  1. Маршрутизатор (Router)  —  это общий низкоуровневый интерфейс для всех компонентов маршрутизации в приложении; по умолчанию React предоставляет несколько различных маршрутизаторов: мы применим на практике HashRouter, но есть и обычный BrowserRouter
  2. HashRouter  —  это подвид маршрутизатора с использованием символа # в URL для синхронизации пользовательского интерфейса: как пример  —  ссылка вида http://localhost:3000/#/about. Для начала подключим к приложению данный маршрутизатор, а затем вы научитесь его отключать; применяется Hash-маршрутизатор на статических веб-сайтах.
  3. BrowserRouter  —  это маршрутизатор <Router> с применением HTML5 history API (pushState, replaceState и событие popstate) для синхронизации пользовательского интерфейса с URL. Данный маршрутизатор также подключим к приложению; его следует применять тогда, когда серверу предстоит обрабатывать динамические запросы.
  4. Маршрут (Route)  —  это компонент, отображающийся всякий раз при соответствии URL-адреса заданному шаблону. Следовательно, маршрутов одновременно может быть много: вот почему так важен переключатель.
  5. Переключатель (Switch)  —  это компонент, с помощью которого контролируется отображение различных маршрутов (Routes); он отображает только первый подходящий дочерний маршрут. Отлично подходит, чтобы избежать возникновения ошибки одновременного отображения нескольких маршрутов.
  6. Ссылка (Link)  —  это компонент, позволяющий маршрутизатору изменять навигацию в приложении.
  7. Навигационная ссылка (NavLink)  —  это просто расширение компонента ссылки, применяющееся в создании стилей: оно добавляет класс .activate к текущей выбранной ссылке в навигационной панели  —  таким образом пользователь поймёт, какая именно ссылка выбрана в данный конкретный момент.

Теперь создадим HashRouter , внутрь которого переместим всё содержимое файла main.js: это необходимо, чтобы убедиться в правильном использовании компонентов, например, если компонент Route определён вне HashRouter, то он не сработает. Поэтому HashRouter необходимо сделать родителем, поместив внутрь него весь остальной код; именно по этой причине маршрутизаторы часто встречаются в App.js.

Подсказка: позже мы заменим HashRouter на BrowserRouter.

Переключатель Switch размещается внутри компонента div с контентом, поскольку именно там отображается содержимое (контент) каждой новой выбранной страницы. 

Помните: компонент div с контентом отображается при каждом совпадении маршрута Route с конкретным URL, а переключатель Switch следит, чтобы отобразилось только первое совпадение.

Все маршруты Route определены внутри переключателя Switch, поскольку одновременно на странице отображается только один компонент; однако нередко встречается вариация с определением нескольких маршрутов вне переключателя. Вот код:

import Navbar from '../components/navbar/navbar'
import  { BrowserRouter, HashRouter, Switch, Route } from "react-router-dom";
import Home from '../components/home/home'
import Contact from '../components/contact/contact'
import About from '../components/about/about'

function Main() {
    return (
        <div>
            <HashRouter>
                <Navbar></Navbar>
                <div className="content">
                
                    <Switch>
                        <Route exact path="/" component={Home}/>
                        <Route path="/contact" component={Contact}/>
                        <Route path="/about" component={About}/>
                    </Switch>
                </div>
            </HashRouter>
        </div>
    )
}

export default Main;

Main.js  —  полный пример программы с перенаправлением на нужный компонент в зависимости от URL-адреса

Далее необходимо изменить Navbar.js так, чтобы он содержал специальные компоненты NavLink вместо тегов <a>, ведь маршрутизатор будет работать только с компонентами Link.

Вы заметили, что путь / содержит ключевое слово? Оно необходимо из-за того, что такой путь по умолчанию соответствует ВСЕМ ВОЗМОЖНЫМ путям, ведь все они содержат данный символ. Помните, какую ссылку выберет переключатель Switch? Именно первое совпадение, поэтому необходимо явно указать, что выбирать путь / нужно только в случае полного, абсолютного совпадения. Пример программы:

import  { NavLink } from "react-router-dom";

function Navbar() {
    return (
        <div className="navbar">
            <h1>Navbar</h1>
            <ul>
                <li><NavLink exact to="/">Home</NavLink></li>
                <li><NavLink to="/contact">Contact</NavLink></li>
                <li><NavLink to="/about">About</NavLink></li>
            </ul>
        </div>
    )
}

export default Navbar;

Navbar.js  —  полный код навигационной панели с NavLinks вместо тегов <a></a>

По желанию измените свойство CSS .active в файле index.css, визуально выделив активную навигационную ссылку.

.active {
  color: #ffffff;
}

index.css  —  визуальное выделение активной навигационной ссылки

Откройте страницу по адресу localhost:3000, чтобы оценить результат и протестировать навигацию по сайту. Если все этапы разработки выполнены правильно, то вы должны увидеть одну особенность поведения веб-страницы: содержимое переключается, в то время как верхняя панель навигации остается неизменной.

http://localhost:3000/#/  —  обратите внимание на символ решетки, используемый в URL-адресе хэш-роутером
http://localhost:3000/#/about  —  URL и контент изменяются при выборе другой навигационной ссылки

UseParams

Передача данных в ссылку

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

Рассмотрим два способа передачи данных в маршрутизаторе.

Первый способ  —  это хук useParams из react-router, позволяющий передавать пары ключ-значение в URL. 

Теперь напишем профиль пользователя и выведем его имя на витрину.

Сначала создайте новую папку user, далее — добавьте в неё файл components/user/profile.js.

mkdir components/user
touch components/user/profile.js

В файле profile.js импортируем useParams, затем присваиваем значение имени пользователя с помощью данного хука. Вот код:

import { useParams } from "react-router";

function Profile() {
    // Для получения имени пользователя из URL примените хук useParams. 
    // Имя пользователя должно указывается в качестве именованного параметра маршрута. 
    const { username } = useParams();

    return (
        <h1>{username} Profile</h1>
    )
}

export default Profile;

profile.js  —  useParams позволяет извлечь имя пользователя из URL-адреса

Как useParams узнаёт, какое именно значение извлекать? При помощи именованных параметров из объявления маршрута, конечно же; параметры именуются путем добавления :parameter к Route. Поэтому теперь измените маршрут в main.js, добавив к нему параметр :username следующим образом:

import { useParams } from "react-router";

function Profile() {
    // Для получения имени пользователя из URL примените хук useParams. 
    // Имя пользователя должно указывается в качестве именованного параметра маршрута. 
    const { username } = useParams();

    return (
        <h1>{username} Profile</h1>
    )
}

export default Profile;

main.js  —  добавление маршрута профиля с параметром :username

Последнее, что нужно изменить  —  навигационная панель должна показывать страницу профиля и вставлять значение имени пользователя в ссылку. Для примера всё жестко закодировано, но в реальном сценарии ваша панель навигации почти всегда получает имя пользователя откуда-то ещё.

import  { NavLink } from "react-router-dom";

function Navbar() {
    return (
        <div className="navbar">
            <h1>Navbar</h1>
            <ul>
                <li><NavLink exact to="/">Home</NavLink></li>
                <li><NavLink to="/contact">Contact</NavLink></li>
                <li><NavLink to="/about">About</NavLink></li>
                <li><NavLink to="/profile/percybolmer">Profile</NavLink></li>
            </ul>
        </div>
    )
}

export default Navbar;

Navbar.js — добавление ссылки на profile/percybolmer, где percybolmer  —  значение имени пользователя, жестко закодированное для примера

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

http://localhost:3000/#/profile/percybolmer  —  ссылка на страницу профиля

useLocation

Передача данных через React Router

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

К счастью, всё возможно благодаря state, переменной состояния в ссылках: каждая ссылка может иметь свой параметр to для получения данных, опосля передающихся через переменную состояния с помощью хука useLocation.

В объекте to обязательно должны устанавливаться переменная состояния state и переменная pathname —  это конечная точка URL.

{
"pathname": "url",
"state": {
"yourstatevar": "value"
}
}

И вот теперь вы думаете: “Отлично, пора запускать!”; однако на всех форумах вроде Stack Overflow регулярно задаются вопросы, которые мы сразу же и рассмотрим в данном руководстве.

“Да, я специально допустил ошибки, чтобы убедиться, что мы их устраним.”  —  Злой Учитель Программирования.

Две самые распространенные среди новичков проблемы:

  1. HashRouter не поддерживает хук useLocation
  2. Нельзя размещать маршрутизатор Router внутри компонента, который размещает маршрутизатор в дереве.

Для начала разберёмся с первой проблемой  —  несовместимостью HashRouter и хука useLocation. Важно это знать, а также очень важно подчеркнуть это в документации. Именно из-за данной проблемы мы заменим HashRouter на BrowserRouter. Единственное изменение, которое нужно сделать  —  изменить идентификатор и убедиться, что BrowserRouter корректно импортирован.

А пока мы меняем маршрутизатор, то нужно решить проблему номер два: просто перенесите Router в файл index.js, что заставит компонент Router обернуть всё приложение и, таким образом, он перестанет быть частью компонента, добавляющего сам Router в дерево. Вот код:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import  { BrowserRouter } from "react-router-dom";
ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

// Если вы хотите измерить производительность приложения, то передайте функцию
// для регистрации результатов (например: reportWebVitals(console.log))
// или отправьте её в конечную точку аналитики.
reportWebVitals();

Index.js  —  маршрутизатор браузера оборачивает приложение

Далее удалим маршрутизатор из файла main.js:

import Navbar from '../components/navbar/navbar'
import  { Switch, Route } from "react-router-dom";
import Home from '../components/home/home'
import Contact from '../components/contact/contact'
import About from '../components/about/about'
import Profile from '../components/user/profile'
function Main() {
    return (
        <div>
            
                <Navbar></Navbar>
                <div className="content">
                
                    <Switch>
                        <Route exact path="/" component={Home}/>
                        <Route path="/contact" component={Contact}/>
                        <Route path="/about" component={About}/>
                        
                        <Route path="/profile/:username" component={Profile}/>
                    </Switch>
                </div>

        </div>
    )
}

export default Main;

Main.js  —  в файле больше нет маршрутизатора

Затем слегка перепишем код в файле navbar.js, ведь необходимо убедиться, что объект to передает больше данных, чем раньше. Код приведен ниже:

import  { NavLink } from "react-router-dom";

function Navbar() {
    return (
        <div className="navbar">
            <h1>Navbar</h1>
            <ul>
                <li><NavLink exact to="/">Home</NavLink></li>
                <li><NavLink to="/contact">Contact</NavLink></li>
                <li><NavLink to="/about">About</NavLink></li>
                <li><NavLink to={{
                    pathname: "/profile/percybolmer",
                    state: { registrationdate: "2021-07-07" },
                }}>Profile</NavLink></li>
            </ul>
        </div>
    )
}

export default Navbar;

Функция Navbar с объектом to в ссылке

И напоследок пропишем хук useLocation; из примера вы узнаете, как присвоить и переменную State, и переменную pathname. Данный сниппет почти ничем не отличается от применения хука useParams:

import { useParams, useLocation } from "react-router-dom";
import React from 'react';


const Profile = () => {
    // Используйте хук useParams для получения имени пользователя из URL. 
    // Имя пользователя должно быть применено в качестве именованного параметра в маршруте. 
    let { username } = useParams();
    // useLocation применяется для захвата состояния из входных данных в объект. 
    // Так можно захватить каждое поле в объекте, используя то же имя, что и имя переменной. 
    let { pathname } = useLocation(); 
    let { state } = useLocation();

    return (
        <div>
            <h1>{username} Profile</h1>
            <p> Registered on:{state.registrationdate} </p>
            <p> Visiting: {pathname}</p>
        </div>
    )
}

export default Profile;

Profile.js  —  хук useLocation применяется дважды

Снова перейдите по ссылке localhost:3000, проанализируйте поведение и внешний вид веб-сайта. Заметьте, как изменились URL после удаления HashRouter:

http://localhost:3000/profile/percybolmer  —  отображение данных из хука useLocation

useHistory

Программная маршрутизация

Иногда требуется программно перенаправлять пользователя по ссылке на основе какого-либо события: вам пригодится хук useHistory, возвращающий объект истории для перенаправления браузера по нужному пути. Данный хук крайне прост в применении: при одновременном использовании объекта history и push(route) произойдёт перенаправление браузера.

Теперь давайте сделаем тег <h1></h1> в навигационной панели кнопкой, перенаправляющей пользователя на домашнюю страницу при нажатии на неё.

Далее мы применим тот же самый подход, что и раньше: присвоим переменную с помощью хука, на этот раз  —  useHistory.

import  { NavLink , useHistory} from "react-router-dom";

function Navbar() {
    let history = useHistory();

    function goHome(path) {
        history.push("/");
    }

    return (
        <div className="navbar">
            <button type="button" onClick={goHome}><h1>Navbar</h1></button>
            <ul>
                <li><NavLink exact to="/">Home</NavLink></li>
                <li><NavLink to="/contact">Contact</NavLink></li>
                <li><NavLink to="/about">About</NavLink></li>
                <li><NavLink to={{
                    pathname: "/profile/percybolmer",
                    state: { registrationdate: "2021-07-07" },
                }}>Profile</NavLink></li>
            </ul>
        </div>
    )
}

export default Navbar;

navbar.js  —  пример useHistory для перенаправления браузера

Наконец-то в последний раз перейдите по ссылке localhost:3000 и посмотрите на финальный результат своих трудов. Теперь перейдите на любую страницу, кроме стартовой по адресу /, а затем нажмите на кнопку: вуаля, вас перенаправило обратно на главную!

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Percy Bolmér: How To Implement a Single Page Application Using React-Router

Предыдущая статьяКонтейнеризацию невозможно сдержать
Следующая статьяВас неправильно учили объектно-ориентированному программированию