React Router è una libreria che permette di creare applicazioni React con più pagine in cui la transizione fra una pagina e l’altra avviene in maniera dinamica tramite Javascript, senza dover ogni volta ricaricare la pagina.
Questa è certamente una delle funzionalità indispensabili per una Single Page Application. Potremmo creare il nostro Router da zero usando per esempio la History API di HTML5, ma React Router ci agevola il lavoro mettendoci a disposizione un’API completa e semplice da usare. React Router è arrivata ormai alla versione 4.x.x che si differenzia abbastanza rispetto alle precedenti versioni. Possiamo iniziare a includere React Router nei nostri progetti. Ovviamente, come avete già immaginato, basterà aggiungere la libreria tra le dipendenze del nostro progetto all’interno del file package.json. E per far questo ci occorrerà semplicemente eseguire il comando npm install nella directory base del nostra applicazione. Da ora in poi quando useremo il termine React Router, faremo riferimento alla versione 4. Al momento in cui questa guida è stata realizzata, è disponibile su NPM la versione 4.1.1.
Scegliere fra i diversi Router Component
React Router mette a disposizione diversi tipi di <Router> a seconda del genere di applicazione che vogliamo realizzare o delle funzionalità di cui abbiamo bisogno. Per esempio il componente StaticRouter può essere usato se si realizza un’applicazione in cui si effettua il rendering delle pagine direttamente sul server. Esiste un componente NativeRouter per le applicazioni realizzate con React Native. MemoryRouter può essere utile invece per la fase di test dell’applicazione.
In questo articolo invece useremo il componente BrowserRouter che utilizza la History API di HTML5, per ulteriori dettagli sugli altri <Router> potete consultare la documentazione.
Semplice applicazione con React Router
Vediamo ora come integrare React Router in un’applicazione. Inizializziamo per prima cosa la nostra applicazione.
$ create-react-app esempio-react-router
$ npm install --save react-router-dom
All’interno del file App.js importiamo due componenti: BrowserRouter che rinominiamo per semplicità Router e Route. Creiamo quindi un nuovo componente App come mostrato nel codice sottostante.
// ./components/Home.js
import React from 'react';
const Home = () => <h1>Pagina iniziale</h1>;
export default Home;
// App.js
import React, { Component } from 'react';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import Home from './components/Home';
class App extends Component {
render() {
return (
<Router>
<div>
<Route exact path="/" component={Home} />
<Route strict path="/team/" render={() => <h1>Team</h1>} />
<Route path="/stagione" children={({match, location}) => match && <h1>Stagione </h1>} />
</div>
</Router>
);
}
}
export default App;
Abbiamo usato un elemento <Router> avente come elementi discendenti una serie di elementi <Route> racchiusi in un <div>. È necessario racchiudere più elementi <Route> all’interno di un <div> altrimenti nella console per sviluppatori riceveremo il seguente errore che ci segnala che l’elemento <Router> dovrebbe avere un solo elemento discendente.
Abbiamo impiegato tre elementi <Route> ognuno dei quali ha un attributo path che indica per quale URL deve essere mostrato un certo componente. Nel primo elemento <Route> abbiamo usato anche la proprietà exact (<Route exact path="/" component={Home} /> è equivalente a <Route exact=true path="/" component={Home} />) ad indicare che dovrà essere mostrato il componente Home solo se il valore dell’attributo path è uguale al valore della proprietà location.pathname. Se non avessimo usato exact React Router avrebbe mostrato il componente Home anche per il path "/team". Riassumiamo il funzionamento dell’attributo exact nella tabella sottostante.
Path prop | Indirizzo | location.pathname | Exact | match |
---|---|---|---|---|
/ | example.com/team | /team | false | true |
/ | example.com/team | /team | true | false |
Potete vedere la differenza anche nelle immagini sottostanti. Nel caso in cui è stata usata la prop exact e quindi non viene mostrato il componente Home.
Oppure nel caso in cui exact=false in cui invece abbiamo due match e abbiamo il seguente risultato.
Nel secondo elemento <Route> abbiamo invece usato l’attributo strict che farà in modo che ci sia un match solo se location.pathname è esattamente uguale a "/team/" ovvero solo se è presente la slash finale. Riassumiamo anche in questo caso il funzionamento della prop strict in una tabella.
Path prop | Indirizzo | location.pathname | Strict | match |
---|---|---|---|---|
/team/ | example.com/team | /team | true | false |
/team/ | example.com/team/ | /team/ | true | true |
/team/ | example.com/team/piloti | /team/piloti | true | true |
/team/ | example.com/team/piloti/ | /team/piloti/ | true | true |
<Route> può anche ricevere uno dei seguenti attributi: component, render, children. In tutti e tre i casi verranno passate al componente o alle funzioni (nel caso di render e children) le proprietà history, location e match che sono degli oggetti. Con la proprietà component specifichiamo un componente che vogliamo venga mostrato in caso ci sia un match (Ovvero React Router determina che quel componente deve essere mostrato per un certo valore di location.pathname). Possiamo invece usare la prop render se vogliamo che venga invocata una certa funzione. La prop children riceve anche una funzione che però verrà sempre eseguita. Nel caso dell’esempio esposto, usiamo, all’interno della funzione passata all’attributo children, la prop match. Se l’oggetto match non è nullo ovvero se location.pathname è uguale a /stagione, verrà inserito nella pagina l’elemento <h1>Stagione </h1>. Come detto la funzione passata alla prop children viene sempre invocata qualsiasi sia l’indirizzo nella barra del browser. Se però location.pathname differisce dal valore contenuto nell’attributo path dell’elemento <Route>, l’oggetto match sarà null. Nel caso in cui invece l’indirizzo è pari a example.com/stagione, match sarà un oggetto come mostrato nell’immagine sottostante. Se non avessimo usato l’oggetto match per decidere se mostrare o meno l’elemento <h1>, quest’ultimo verrebbe sempre mostrato al di là del valore di location.pathname.
I componenti <Link> e <NavLink>
A questo punto però l’unico modo per passare da una pagina all’altra è attraverso la barra degli indirizzi del browser. Vediamo allora come aggiungere dei link all’interno della nostra applicazione per passare da una pagina all’altra senza dover effettuare il refresh del browser. In React Router, al posto dei tag <a href=""> useremo due componenti forniti dalla libreria, ovvero Link e NavLink.
Il componente Link accetta due attributi: to che può essere una stringa o un oggetto e replace. Per capire come funziona quest’ultimo, dobbiamo considerare che React Router mantiene in memoria la cronologia delle pagine visualizzate. Immaginate la cronologia come una pila, ogni volta che si visita una pagina, viene aggiunto un nuovo elemento alla pila. Se l’attributo replace del componente Link è uguale a true, si sostituisce l’ultimo elemento della pila invece di aggiungerne uno nuovo. L’attributo to può essere una stringa o un oggetto. Per capire come funzionano i componenti Link e NavLink. modifichiamo l’esempio visto sopra.
// App.js
import React, { Component } from 'react';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import Home from './components/Home';
import Team from './components/Team';
import Nav from './components/Nav';
import './App.css';
class App extends Component {
render() {
return (
<Router>
<div>
<Nav />
<Route exact path="/" component={Home} />
<Route strict path="/team/" render={() => <Team />} />
<Route path="/stagione" children={({match, location}) => match && <h1>Stagione </h1>} />
</div>
</Router>
);
}
}
export default App;
Nel file App.js avremo sempre tre componenti <Route>, abbiamo però cambiato il valore restituito dalla funzione render() nel secondo componente <Route>. Abbiamo infatti creato un componente <Team /> all’interno del file ‘./components/Team’ che abbiamo importato all’inizio della pagina.
// components/Team.js
import React, {Component} from 'react';
import {Link} from 'react-router-dom';
class Team extends Component {
render() {
return (
<div>
<h1>Team</h1>
<p><Link to="/stagione">Stagione 2017</Link></p>
<p><Link
to={{
pathname: '/',
search: '?query=aaa',
hash: '#hash-value',
state: {
fromTeam: "informazioni passate al componente Home"
+ "attraverso l'oggetto location"
}
}}>Home</Link>
</p>
</div>
);
}
}
export default Team;
// components/Home.js
import React from 'react';
const Home = ({match, location}) => {
console.dir(location);
return <h1>Pagina iniziale</h1>;
};
export default Home;
All’interno del componente Team, inseriamo due Link. Il primo ci porterà all’indirizzo localhost:3000/stagione, il secondo ci porterà alla pagina iniziale. In entrambi i casi, non verrà ricaricata la pagina del browser e la transizione avverrà in maniera dinamica. L’oggetto passato all’attributo to del secondo link, ci permette di specificare oltre al percorso (pathname) altre informazioni che saranno passate al componente Home tramite l’oggetto location.
All’interno della console per sviluppatori, possiamo vedere la struttura dell’oggetto location.
NavLink è una versione particolare del componente Link. Fra le proprietà che NavLink può ricevere, vedremo in particolar modo activeClassName e activeStyle. All’interno del file ‘components/Nav.js’ abbiamo realizzato un componente Nav in cui abbiamo inserito tre elementi NavLink.
Con activeStyle possiamo passare un oggetto attraverso il quale possiamo specificare lo stile che verrà applicato all’elemento NavLink quando l’URL corrente è uguale al valore dell’attributo ‘to‘ cioè quando location.pathname è uguale al percorso specificato nell’attributo ‘to‘.
import React from 'react';
import {NavLink} from 'react-router-dom';
const Nav = () => (
<nav>
<NavLink exact activeClassName="current" to="/">Pagina Iniziale</NavLink>
<NavLink activeClassName="current" to="/team/">Team</NavLink>
<NavLink activeStyle={{color: 'green'}} to="/stagione/">Stagione</NavLink>
</nav>
);
export default Nav;
Possiamo ottenere un risultato simile con la prop activeClassName a cui passeremo il nome di una classe da applicare nel momento in cui l’elemento sarà attivo.
Nel nostro caso, abbiamo definito lo stile della classe ‘current’ all’interno del file App.css.
/* App.css */
a, a:visited {
color: blue;
}
a.current {
color: green;
font-style: italic;
font-weight: 800;
}
Potete vedere il risultato ottenuto nell’immagine sottostante.
Componente Route
All’inizio di questo articolo, abbiamo visto che l’elemento <Route> riceve un’attributo path che possiamo usare per passare dei parametri al componente specificato nell’attributo component o alle funzioni render e children. Modifichiamo nuovamente l’esempio appena visto.
import React, { Component } from 'react';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import Home from './components/Home';
import Team from './components/Team';
import Nav from './components/Nav';
import './App.css';
class App extends Component {
render() {
return (
<Router>
<div>
<Nav />
<Route exact path="/" component={Home} />
<Route strict path="/team/" render={() => <Team />} />
<Route path="/stagione" children={({match, location}) => match && <h1>Stagione </h1>} />
<Route path="/storia/:anno?/:dettagli?" render={({match, location}) => (
<div>
<h1>{match.params.anno || 'Informazioni non disponibili'}</h1>
<h2>{match.params.dettagli}</h2>
</div>
)} />
</div>
</Router>
);
}
}
export default App;
Abbiamo aggiunto un nuovo elemento <Route> con un attributo path="/storia/: anno?/: dettagli?" in cui : anno? e : dettagli? rappresentano due parametri non obbligatori (? sta ad indicare proprio che i parametri sono opzionali, se volessimo dei parametri obbligatori basterà rimuovere ‘?’ e specificare solo il nome del parametro preceduto da ‘: ‘) che verranno passati alla funzione render() all’interno dell’oggetto match. Se digitiamo nella barra degli indirizzi "localhost:3000/storia/2016", match.params.anno sarà uguale a 2016.
Se invece visitiamo l’indirizzo "localhost:3000/storia/2016/vittorie", vedremo una pagina simile a quella mostrata nell’immagine sottostante.
È possibile inoltre specificare, attraverso le espressioni regolari, il tipo e formato dei parametri che aspettiamo vengano passati. Per far ciò, specifichiamo il formato immediatamente dopo il nome del parametro. Modifichiamo allora l’esempio appena visto. Vorremmo che il parametro anno fosse un valore numerico di quattro cifre e dettagli una stringa.
<Route path="/storia/:anno(d{4})?/:dettagli(w+)?" render={({match, location}) => (
<div>
<h1>{match.params.anno || 'Informazioni non disponibili'}</h1>
<h2>{match.params.dettagli}</h2>
</div>
)} />
Componenti multipli per un determinato percorso
In React Router è possibile mostrare più componenti diversi per uno stesso path. Nell’esempio che segue passeremo due componenti Intro e Dettagli a due elementi <Route> che hanno come attributo path lo stesso valore. Useremo poi il valore del parametro ": componenti" all’interno di Intro e Dettagli attraverso match.params.componenti.
import React, { Component } from 'react';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import Home from './components/Home';
import Nav from './components/Nav';
import Intro from './components/Intro';
import Dettagli from './components/Dettagli';
import './App.css';
const TeamRoutes = () => (
<div>
<Route path="/team/:componenti" component={Intro} />
<Route path="/team/:componenti" component={Dettagli} />
</div>
);
class App extends Component {
render() {
return (
<Router>
<div>
<Nav />
<Route exact path="/" component={Home} />
{/* Inseriamo l'elemento <TeamRoutes /> */}
<TeamRoutes />
<Route path="/stagione" children={({match, location}) => match && <h1>Stagione </h1>} />
<Route path="/storia/:anno(d{4})?/:dettagli(w+)?" render={({match, location}) => (
<div>
<h1>{match.params.anno || 'Informazioni non disponibili'}</h1>
<h2>{match.params.dettagli}</h2>
</div>
)} />
</div>
</Router>
);
}
}
export default App;
Abbiamo ripreso l’esempio visto sopra e abbiamo creato un componente TeamRoutes in cui abbiamo incluso due elementi <Route>. Se a questo punto digitiamo nella barra degli indirizzi del browser "localhost:3000/team/piloti", i due componenti Intro e Dettagli riceveranno un oggetto match da cui possiamo estrarre il valore di match.params.componenti che sarà uguale alla stringa "piloti". I due componenti Intro e Dettagli sono due semplici Functional Component.
import React from 'react';
const Intro = ({match}) => {
return (
<section>
<h2>Intro {match.params.componenti}</h2>
<p>
Cras mattis consectetur purus sit amet fermentum.
</p>
</section>
);
}
export default Intro;
import React from 'react';
const Dettagli = ({match}) => {
return (
<section>
<h2>Dettagli {match.params.componenti}</h2>
<p>
Cum sociis natoque penatibus et magnis dis
parturient montes, nascetur ridiculus mus.
Aenean eu leo quam.
Pellentesque ornare sem lacinia quam venenatis vestibulum.
</p>
</section>
);
}
export default Dettagli;
Potete vedere il risultato finale nelle immagini riportate qui sotto in cui abbiamo visitato prima l’indirizzo "localhost:3000/team/piloti" e poi "localhost:3000/team/meccanici".
Annidare i componenti <Route>
Un’altra funzionalità estremamente utile, è la possibilità di annidare gli elementi <Route>. Riprendiamo il solito esempio e modifichiamolo leggermente. Con la configurazione usata finora, se proviamo a visitare l’indirizzo "localhost:3000/team/", verrà mostrato solo il componente Nav contenente i link alle diverse pagine. Modifichiamo allora il comportamento per il percorso "/team".
import React, { Component } from 'react';
import {BrowserRouter as Router, Route, Link} from 'react-router-dom';
import Home from './components/Home';
import Nav from './components/Nav';
import Intro from './components/Intro';
import Dettagli from './components/Dettagli';
import './App.css';
const TeamRoutes = () => (
<div>
<Link to="/team/piloti">Piloti</Link>
<Link to="/team/meccanici">Meccanici</Link>
<Route path="/team/:componenti" component={Intro} />
<Route path="/team/:componenti" component={Dettagli} />
</div>
);
class App extends Component {
render() {
return (
<Router>
<div>
<Nav />
<Route exact path="/" component={Home} />
<Route path="/team" component={TeamRoutes} />
<Route path="/stagione" children={({match, location}) => match && <h1>Stagione </h1>} />
<Route path="/storia/:anno(d{4})?/:dettagli(w+)?" render={({match, location}) => (
<div>
<h1>{match.params.anno || 'Informazioni non disponibili'}</h1>
<h2>{match.params.dettagli}</h2>
</div>
)} />
</div>
</Router>
);
}
}
export default App;
In questo caso, abbiamo passato il componente TeamRoutes all’elemento <Route>, per il percorso "/team", attraverso l’attributo component. Visitando "localhost:3000/team" verranno quindi mostrati due link. Cliccando su ognuno di questi verranno inseriti nella pagina i due componenti Intro e Dettagli.
Il componente <Switch>
<Switch> ci permette di mostrare soltanto il primo elemento <Route> o <Redirect> (ne parleremo a breve), presente fra i suoi elementi figli, per cui l’attributo path combacia con location.pathname. Per capire meglio come funziona, facciamo un altro semplice esempio. Supponiamo di avere il seguente codice.
import React, { Component } from 'react';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import Nav from './components/Nav';
import './App.css';
const Componente404 = () => <h1>Erroe 404 - Pagina non disponibile</h1>
class App extends Component {
render() {
return (
<Router>
<div>
<Nav />
<Route strict path="/stagione/calendario" render={({match}) => <h1>Calendario </h1>} />
<Route path="/stagione/:classifica"
render={({match}) => <h1>Classifica {match.params.classifica} </h1>} />
<Route component={Componente404} />
</div>
</Router>
);
}
}
export default App;
Se visitiamo l’indirizzo "localhost:3000/stagione/calendario", avremo un match per ognuno degli elementi <Route>. Infatti, l’ultimo elemento non presenta un’attributo path per cui verrà sempre mostrato il Componente404. L’attributo path del primo elemento <Route> ovviamente corrisponde al valore di location.pathname. Nel caso del secondo elemento on cui abbiamo usato un parametro ": classifica", la funzione passata all’attributo render riceverà un oggetto match in cui match.params.classifica è uguale a "calendario". Potete vedere nell’immagine sottostante quanto abbiamo appena descritto.
Racchiudendo i tre elementi <Route> all’interno di un elemento <Switch>, verrà invece mostrato soltanto l’elemento <h1> restituito dalla funzione passata all’attributo render del primo elemento <Route>.
import React, { Component } from 'react';
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom';
import Nav from './components/Nav';
import './App.css';
const Componente404 = () => <h1>Erroe 404 - Pagina non disponibile</h1>
class App extends Component {
render() {
return (
<Router>
<div>
<Nav />
<Switch>
<Route strict path="/stagione/calendario"
render={({match}) => <h1>Calendario </h1>} />
<Route path="/stagione/:classifica"
render={({match}) => <h1>Classifica {match.params.classifica} </h1>} />
<Route component={Componente404} />
</Switch>
</div>
</Router>
);
}
}
export default App;
Per prima cosa abbiamo importato il componente Switch e abbiamo poi racchiuso i tre elementi <Route> all’interno dell’elemento <Switch>. In questo modo avremo il risultato mostrato nell’immagine seguente.
Il componente <Redirect>
Per concludere, vediamo come usare il componente Redirect per reindirizzare da una pagina a un’altra. La nuova pagina sostituirà la vecchia nella cronologia delle pagine visitate. Se vogliamo salvare entrambe, dovremo aggiungere una prop push all’elemento <Redirect>.
Vediamo un esempio anche in questo caso.
import React, { Component } from 'react';
import {BrowserRouter as Router, Route, Redirect} from 'react-router-dom';
import Nav from './components/Nav'
import './App.css';
class App extends Component {
render() {
return (
<Router>
<div>
<Nav />
<Route exact path="/" render={() => <h1>Pagina iniziale</h1>} />
<Route path="/gallery" render={() => <h1>Gallery</h1>} />
<Route path="/immagini" render={() => (
<Redirect to={{
pathname: '/gallery',
search: '?ord=desc',
state: { info: "info passate attraverso Redirect"}
}} />
)} />
</div>
</Router>
);
}
}
export default App;
Come potete vedere dal codice, per l’elemento <Redirect> abbiamo usato un’attributo to che in questo caso è un oggetto. Avremo potuto passare anche una semplice stringa per indicare la nuova destinazione.
In questo caso, quando clicchiamo sul link "immagini" viene eseguito un redirect da "/immagini" a "/gallery". All’interno della funzione passata all’attributo render dell’elemento <Route> avente come path ‘/gallery’, avremmo potuto stampare nella console degli strumenti il contenuto dell’oggetto location ricevuto. Avremmo ottenuto un risultato come quello mostrato nell’immagine sottostante.
Conclusioni
In questo articolo ci siamo limitati a fare una panoramica delle funzionalità della libreria React Router. In ogni caso, se volete conoscere ulteriori dettagli di quella che è certamente una delle librerie più usate per la realizzazione di applicazioni in React, potete consultare la documentazione ufficiale.