UWAGA! Ten wpis dotyczy starej wersji react-router! Od tego czasu sporo się w tej bibliotece zmieniło więc napisałem nowy wpis na temat nowego podejścia!

Dzisiejszym wpisem kontynuuję omawianie najważniejszych zagadnień związanych z ReactJS. To już trzeci wpis na ten temat w tym tygodniu i możesz spodziewać się kolejnych przez następne tygodnie! Mam nadzieję, że Ci to odpowiada? No ale do rzeczy… W dzisiejszym wpisie chciałbym omówić routing ReactJS. Będzie to, jak możesz wyczytać w tytule, wprowadzenie do biblioteki react-router, która jest w zasadzie standardem w tym aspekcie.

Na potrzeby tego wpisu przyjmijmy, że mamy strukturę projektu taką samą jak ta, którą stworzyłem w ramach wpisu przedstawiającego podstawy ReactJS. Jeśli chcesz sam sobie postawić ten projekt, to odsyłam do tamtego artykułu. Na końcu tego artykułu znajdziesz też link to repozytorium GitHub zawierającego pełen kod przykładu przedstawionego w dzisiejszym wpisie. Zachęcam do zapoznania się z nim.

Instalacja

Routing ReactJS z użyciem biblioteki react-router to temat dosyć prosty. Zresztą sam się o tym za chwilę przekonasz. Zanim jednak przejdę do omawiania szczegółów konfiguracji, musimy bibliotekę tę zainstalować w naszym projekcie. Aby to zrobić, wystarczy uruchomić poniższą komendę w konsoli (będąc w głównym katalogu projektu):

npm install --save-dev react-router

Zamiast NPM możesz też równie dobrze użyć Yarn:

yarn add --dev react-router

Zarówno jedna jak i druga z tych komend finalnie spowodują zainstalowanie pakietu react-router jako zależności deweloperskiej. No dobra… skoro niezbędna biblioteka jest już zainstalowana, czas zacząć jej używać!

Konfiguracja routingu

Jak być może pamiętasz z mojego wpisu na temat podstaw ReactJS, każda aplikacja ma dokładnie jeden komponent główny (ang. root component). W naszej przykładowej aplikacji jest to AppComponent znajdujący się w pliku src/components/Main.js. Wprowadzenie routingu do aplikacji powoduje małą zmianę - komponentem głównym będzie teraz komponent Router zawierający konfigurację routingu, natomiast AppComponent stanie się komponentem głównym dla danego “route”.

Na początek spójrzmy na obecną implementację pliku src/index.js, który odpowiada za wyrenderowanie głównego komponentu wewnątrz elementu DOM o indeksie app:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/Main';

ReactDOM.render(<App />, document.getElementById('app'));

Jak widzisz, komponent zaimportowany z pliku src/components/Main.js jest tym, który jest renderowany jak główny.

No dobrze, dodajmy zatem teraz routing. Aby to zrobić wprowadźmy kilka modyfikacji w powyższym kodzie:

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, browserHistory } from 'react-router'
import App from './components/Main';

ReactDOM.render((
  <Router history={browserHistory}>
    <Route path="/" component={App} />
  </Router>
), document.getElementById('app'));

Pierwsze co rzuca się w oczy, to dodatkowy import z pakietu react-router. Importujemy z niego komponenty Router oraz Route, których za chwilę użyjemy do konfiguracji routingu. Oprócz tego importowany jest też obiekt browserHistory ale nie zaprzątajmy nim sobie na razie głowy.

Kolejna sprawa to pierwszy parametr metody render. Jak widzisz, zamiast renderować komponent App, renderujemy komponent Router. Posiada on parametr history, o którym, jak wspomniałem, za moment. To co bardziej teraz interesujące to to, że komponent Router zawiera dziecko: komponent Route. Za jego pomocą definiujemy pojedynczy “route” czyli wiążemy adres (atrybut path, zwróć uwagę, że adres jest względny) z komponentem (atrybut component). W powyższym przykładzie mamy zdefiniowany tylko jeden “route”, odpowiadający ścieżce /. Odpowiada jej komponent App czyli ten, którego implementacja znajduje się w pliku src/components/Main.js.

Atrybut history

Jeśli uruchomiłbyś teraz aplikację przekonałbyś się, że na ten moment działa ona w zasadzie tak samo jak bez skonfigurowanego routingu. Wszystko zmieni się kiedy dodamy do niej kolejne “routy”. Najpierw jednak obiecane wyjaśnienie dla atrybutu history.

Myślę, że dobrze jest to wyjaśnione w dokumentacji biblioteki react-router:

React Router is built with history. In a nutshell, a history knows how to listen to the browser’s address bar for changes and parses the URL into a location object that the router can use to match routes and render the correct set of components.

Jak więc widzisz, react-router wykorzystuje bibliotekę history do zarządzania i zapamiętywania adresów widocznych w przeglądarce użytkownika. Generalnie chodzi o to, że przechodząc pomiędzy poszczególnymi “routami” następuje podmiana wyświetlanego komponentu głównego. Cały widok zmienia się bez przeładowania strony w przeglądarce. Dzięki zastosowaniu biblioteki history możliwe jest podmienienie, przez routing ReactJS, adresu w przeglądarce na taki, który odpowiada danemu “route”. Pozwala to również przeglądarce na prawidłowe zapamiętanie odwiedzanych “routów” tak jakby to było osobne strony. To z kolei powoduje, że guzik “Wstecz” przeglądarki działa jak należy - powoduje przejście do poprzednich “routów”.

W ostatnim przykładzie, do atrybutu history komponentu Router przekazaliśmy obiekt browserHistory. W ten sposób można zdefiniować sposób obsługi historii przeglądarki. To nie jedyna dostępna możliwość. Poniżej przedstawiam wszystkie te opcje wraz z wyjaśnieniem:

  • browserHistory - Jest to rekomendowany sposób obsługi historii przeglądarki. Wykorzystuje on API historii wbudowane w przeglądarkę dzięki czemu linki wyglądają standardowo, na przyład: example.com/some/path. Minusem wykorzystania tej metody jest to, że wymaga ona odpowiedniego skonfigurowania serwera. Więcej na ten temat znajdziesz w dokumentacji.
  • hashHistory - Wykorzystuje on znak # w adresie, na przykład: example.com/#/some/path czyli w zasadzie tak jak w starym Angularze. Nie wymaga do działania odpowiedniej konfiguracji serwera, więc jest przydatne głównie do przeprowadzania testów na szybko itp., itd. Adres taki nie wygląda jednak za dobrze, więc dąż do wykorzystania browserHistory, tak jak radzą autorzy biblioteki.
  • createMemoryHistory - Jak sama nazwa wskazuje, działa w pamięci i nie wykorzystuje ani w żaden sposób nie zmienia adresu przeglądarki. Może być to przydatne w środowiskach innych niż przeglądarka (na przykład kiedy tworzysz projekt React Native).

Dodajemy kolejny route

Ok, skoro podstawową konfigurację mamy już wyjaśnioną, czas rozszerzyć nasz routing ReactJS i dodać kolejny “route”. W tym celu najpierw utwórzmy nowy komponent (znajdziesz go w pliku src/components/About.js):

import React from 'react';

class About extends React.Component {
  render () {
    return (
      <div className="about">
        <p>Super Duper Example App!</p>
      </div>
    );
  }
}

export default About;

Jedna uwaga: celowo definiuję ten komponent z użyciem zapisu klasowego a nie funkcyjnego. Komponenty podpinane bezpośrednio pod routing z założenia są zwykle kontenerami, z logiką aplikacji itd. Zapis funkcyjny, tak jak pisałem w poście na temat podziału odpowiedzialności komponentów ReactJS, przeznaczony jest dla bezstanowych komponentów prezentacyjnych.

Skoro mamy już komponent, pora skonfigurować dla niego routing! Po niezbędnych zmianach, kod pliku src/index.js będzie wyglądał następująco:

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, browserHistory } from 'react-router'
import App from './components/Main';
import About from './components/About';

ReactDOM.render((
  <Router history={browserHistory}>
    <Route path="/" component={App} />
    <Route path="/about" component={About} />
  </Router>
), document.getElementById('app'));

Na początku oczywiście musimy zaimportować stworzony przed chwilą komponent. To co ciekawe dzieje się wewnątrz komponentu Router. Jak możesz zauważyć, doszedł nam kolejny komponent Route. Wiąże on ścieżkę /about z komponentem About. Jeśli teraz uruchomiłbyś lokalnie swoją aplikację i wpisał w przeglądarce adres: http://localhost:8000/about na ekranie wyświetliłby się tekst “Super Duper Example App!”, tak jak to zdefiniowaliśmy w nowym komponencie.

Nawigacja pomiędzy routami

Jak dotąd myślę, że routing ReactJS może wydawać się bardzo prosty. I tak jest w istocie… Lećmy więc dalej z tym koksem!

Napisałem przed momentem, że aby przetestować nowy “route” należy wpisać jego adres w pasku adresu przeglądarki internetowej. Zamiast tego lepiej jest użyć odpowiednich komponentów nawigacyjnych dostarczanych przez bibliotekę react-router. Zmodyfikujmy co nieco plik src/components/Main.js:

import React from 'react';
import { Link } from 'react-router';

class AppComponent extends React.Component {
  render() {
    return (
      <div className="index">
        <p>This is home!</p>
        <Link to="/about">About</Link>
      </div>
    );
  }
}

export default AppComponent;

Generalnie wywaliłem trochę niepotrzebnego nam kodu. To co ważne w powyższym przykładzie to import komponentu Link należącego do pakietu react-router. Jego użycie jest bardzo proste, co widać w metodzie render. Wystarczy do atrybutu to przypisać odpowiedni adres “route”.

W zasadzie to powyższy przykład użycia komponentu Link tożsamy jest z użyciem standardowego:

<a href="/about">About</a>

Komponent Link posiada jednak większe możliwości. Jedną z nich jest możliwość zdefiniowania klasy dla aktywnego linku:

<Link to="/about" activeClassName="active">About</Link>

Routing ReactJS zawsze wie w jakim jest w danym momencie “route”. Kiedy więc renderowane są komponenty, rozpoznaje on link odpowiadający aktualnemu “route” i może nadać mu odpowiednią klasę. Myślę, że jest to całkiem przydatne dla różnego rodzaju menu nawigacyjnego itp.

Ogólnie rzecz biorąc należy zawsze używać komponentu Link zamiast elementu a ponieważ zapewnia to, że routing ReactJS zadziała poprawnie, a historia przeglądarki zostanie odpowiednio zaktualizowana.

Zagnieżdżanie route

Routing ReactJS pozwala na łatwe zagnieżdżanie kilku “routów”. Jest to przydatne na przykład do tworzenia komponentów layoutu. Zresztą najlepiej będzie pokazać to na przykładzie.

Komponent layoutu

Na początek dodajmy nowy komponent (plik src/components/LayoutWrapper.js):

import React from 'react';
import { Link } from 'react-router';

const LayoutWrapper = (props) => {
  return (
    <div className="index">
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <div className="container">
        {props.children}
      </div>
    </div>
  )
};

export default LayoutWrapper;

Jak widzisz, tym razem użyłem notacji funkcyjnej. To dlatego, że ten komponent odpowiada jedynie za zaprezentowanie layoutu strony, razem z menu. Jest on “wrapperem” na pozostałe komponenty.

To co istotne znajduje się wewnątrz elementu div posiadającego klasę container. Użycie {props.children} oznacza, że w tym miejscu renderować się ma komponent - dziecko tego komponentu. Na przykład, jeżeli użyjemy powyższego komponentu w poniższy sposób…

<LayoutWrapper>
  <p>Hello world!</p>
</LayoutWrapper>

… to dziecko tego komponentu czyli element p zostanie wyrenderowany właśnie w miejscu {props.children}.

Konfiguracja routingu z użyciem layoutu

No ale wracając do tematu - użyjmy komponentu LayoutWrapper w konfiguracji routingu:

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, browserHistory } from 'react-router'
import App from './components/Main';
import About from './components/About';
import LayoutWrapper from './components/LayoutWrapper';

// Render the main component into the dom
ReactDOM.render((
  <Router history={browserHistory}>
    <Route component={LayoutWrapper}>
      <Route path="/" component={App}/>
      <Route path="/about" component={About} />
    </Route>
  </Router>
), document.getElementById('app'));

W powyższej implementacji pliku src/index.js możesz zauważyć, że konfiguracje “routów” dla strony domowej oraz strony “about” zostały owinięte przez konfigurację “route” dla komponentu LayoutWrapper. Dzięki temu, gdy wejdziesz na stronę / wyświetli Ci się widok komponentu LayoutWrapper, a w miejsce jego {props.children} wstrzyknięty zostanie widok komponentu App. Analogicznie zadzieje się dla “route” komponentu About.

Opisany mechanizm można dowolnie wykorzystywać do wyświetlania odpowiedniej “kaskady” komponentów w zależności od wybranego adresu przeglądarki więc jest to bardzo przydatne narzędzie.

Mała uwaga: z plików src/components/Main.js oraz src/components/About.js można teraz usunąć komponenty Link ponieważ znajdują się już one w LayoutWrapper.

Route indexu

Dla strony indeksu (tej o adresie /) routing ReactJS przewiduje dodatkowy sposób konfiguracji. Nie ma tutaj co się rozwodzić, lepiej spojrzeć na przykład:

import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router'
import App from './components/Main';
import About from './components/About';
import LayoutWrapper from './components/LayoutWrapper';

// Render the main component into the dom
ReactDOM.render((
  <Router history={browserHistory}>
    <Route path="/" component={LayoutWrapper}>
      <IndexRoute component={App}/>
      <Route path="/about" component={About} />
    </Route>
  </Router>
), document.getElementById('app'));

Na wstępie importujemy dodatkowy komponent IndexRoute z pakietu react-router. Kolejna sprawa to zmiany w konfiguracji routingu. Zauważ, że definicja ścieżki / przeniesiona została poziom wyżej, natomiast komponent App jest teraz powiązany z komponentem IndexRoute. Z punktu widzenia naszego przykładu nic się nie zmienia i w większości przypadków można tego używać zamiennie. Istnieją szczególne przypadki kiedy ma to znaczenie. Możesz o tym przeczytać w dokumentacji.

Przekazywanie parametrów

Ostatnia sprawa, którą chciałbym poruszyć w ramach tego artykułu to obsługa, przez routing ReactJS, parametrów przekazywanych w adresie. Do zobrazowania tego zagadnienia, dodajmy jeszcze jeden komponent (plik src/components/User.js):

import React, { PropTypes } from 'react';

class User extends React.Component {
  static propTypes = {
    params: PropTypes.object.isRequired
  };

  render () {
    return (
      <p>{`User id is: ${this.props.params.userId}`}</p>
    );
  }
}

export default User;

W powyższym przykładzie doszło sprawdzenie typu parametru params. Jest to parametr dodawany do “propsów” przez react-router. Zawiera on parametry przekazane w adresie. Jak widzisz, wykorzystujemy ten parametr do wyświetlenia go na ekranie.

Jak teraz skonfigurować routing? Spójrz na kolejną iterację naszej przykładowej konfiguracji:

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router'
import App from './components/Main';
import About from './components/About';
import LayoutWrapper from './components/LayoutWrapper';
import User from './components/User';

// Render the main component into the dom
ReactDOM.render((
  <Router history={browserHistory}>
    <Route path="/" component={LayoutWrapper}>
      <IndexRoute component={App}/>
      <Route path="/about" component={About} />
      <Route path="/user/:userId" component={User}/>
    </Route>
  </Router>
), document.getElementById('app'));

Oczywiście doszło nam kolejne użycie komponentu Route. Wiąże on komponent User z adresem /user/:userId. Zapis :userId przekazuje do react-router informację, że w jego miejsce należy spodziewać się parametru, który przekazać należy do komponentu poprzez “propsy”.

Aby to przetestować, wpisz w przeglądarce adres http://localhost:8000/user/1. Na ekranie wyświetli się tekst “User id is: 1”.

Routing ReactJS - podsumowanie

Mam nadzieję, że udało mi się pokazać Ci, że routing ReactJS to nic specjalnie skomplikowanego. Myślę, że to podstawa każdej większej aplikacji z użyciem ReactJS dlatego prędzej czy później będziesz musiał dotknąć tego tematu w swojej pracy. W najbliższym czasie planuję kolejne wpisy na temat “ekosystemu” React tak, aby dopełnić całości jego obrazu jako pełnoprawnego frameworka JavaScript.

Kod przykładu zawartego w artykule

Pełen kod przedstawionych dziś przykładów dostępny jest w repozytorium GitHub. Wystarczy, że sklonujesz repozytorium react-router-example. Po jego sklonowaniu, nie zapomnij uruchomić npm install w celu zainstalowania wszystkich wymaganych zależności!

A może chciałbyś lepiej poznać react-router?

Powyższy wpis to tylko niezbędne minimum, które trzeba znać aby zacząć pracować z biblioteką react-router. Jeśli chciałbyś dogłębnie i od podstaw poznać tę bibliotekę to specjalnie dla Ciebie przygotowałem specjalne kursy on-line, dzięki którym od podstaw nauczysz się react-routera, ale też Reacta oraz Reduxa! Kliknij czerwony guzik w “boksie” poniżej aby dowiedzieć się więcej:

React, Redux, react-router - kursy on-line