Les 3: Single Page Applications
Tot nu toe hebben we enkel React applicaties ontwikkeld met één pagina, dit is natuurlijk niet voldoende voor een realistische website. De meeste website bestaan uit meerdere pagina's. Deze les bekijken we hoe we navigatie kunnen toevoegen aan een React applicatie. Daarnaast integreren we een CSS-framework in de webapplicaties.
De CodeSandbox voorbeelden op deze pagina kunnen af en toe errors vertonen, dit is geen probleem met de code maar met CodeSandbox. Als je een foutmelding krijgt dat een bepaalde import niet werkt, is deze meestal weg nadat de embedded preview herladen wordt.
Waarschuwing
Op woensdag 3/11 is versie 6 van React Router uitgekomen. Dit is een major release en bevat breaking changes. Aangezien deze les gegeven is voor 3/11, is de tekst dan ook gericht op versie 5. Dit wordt niet aangepast tot na de herexamens, versie 5 blijft voorlopig nog updates krijgen en er zijn geen onmiddellijke plannen om hiermee te stoppen. Binnenkort brengt React Router een compatibility layer uit zodat de code voor versie 5 ook werkt in versie 6. Tot het zover is kan je gebruik maken van onderstaand npm commando om versie 5 te installeren in je eigen projecten (voor de lesvoorbeelden en oplossingen is dit normaal niet nodig).
npm install react-router-dom@^5.3.0
De geïnteresseerde lezer kan meer informatie vinden op https://remix.run/blog/react-router-v6.
UPDATE - 3/12: React Router Bootstrap is vanaf nu ook geüpdatet via onderstaand pakket kan je de versie voor React Router 5 installeren.
npm install -S react-router-bootstrap@rr-v4
React Router installeren
Een React project, aangemaakt via npx create-react-app, bevat de nodige bibliotheken voor routing nog niet. Om routing te integreren in een React app zijn twee nieuwe bibliotheken nodig.
De eerste bibliotheek is react-router, deze implementeert de core routing functionaliteiten, ongeacht het platform waarop de React app draait. Vervolgens is ook react-router-dom nodig. Deze bibliotheek implementeert routing specifiek voor een webapplicatie. Er bestaat een alternatief pakket react-router-native dat gebruikt kan worden om mobile applications te ontwikkelen met React Native.
Het is niet nodig om zowel react-router als react-router-dom te installeren, deze laatste bibliotheek installeert de eerste automatisch als een dependency.
npm install --save react-router-dom
React Bootstrap installeren
Bootstrap is een eenvoudig CSS-framework voor het ontwikkelen van responsieve websites, dat al bekend is uit andere vakken. Het is mogelijk om de standaard Bootstrap versie te gebruiken is een React project, maar dit is niet echt ideaal. De componenten die via pure CSS code gebouwd worden zijn geen probleem, maar Bootstrap maakt voor verschillende componenten gebruik van JavaScript code. Deze JavaScript is niet geschreven voor React en kan problemen geven. Daarom voorziet React Bootstrap een volledige implementatie van Bootstrap via React componenten.
npm install --save react-bootstrap
React Bootstrap steunt nog steeds op de standaard Bootstrap CSS-regels, de nodige stylesheets kunnen via een CDN toegevoegd worden of geïnstalleerd worden via NPM. In deze cursus maken we gebruik van de tweede optie.
npm install --save bootstrap
Vervolgens kan de Bootstrap CSS geïmporteerd worden in index.js.
import "bootstrap/dist/css/bootstrap.min.css";
Navigatie met React Router
We zullen een applicatie bouwen die 4 pagina's bevat, Home, Stuff, Contact, en Class. Voor we de routing kunnen implementeren moet het mogelijk zijn om naar deze pagina's te navigeren. Hiervoor gebruiken we een eenvoudige navigatiebalk.
const NavBarNoBootstrap = (props) => {
const liStyle = {
display: "inline-block",
margin: "1em"
}
const ulStyle = {
listStyle: "none"
}
return (
<ul style={ulStyle}>
<li style={liStyle}><a href={"/"}>Home</a></li>
<li style={liStyle}><a href={"/stuff"}>Stuff</a></li>
<li style={liStyle}><a href={"/contact"}>Contact</a></li>
<li style={liStyle}><a href={"/class"}>Class</a></li>
</ul>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Als je het voorbeeld zelf wil uitwerken, kan je hier de startbestanden voor elk van deze componenten downloaden.
BrowserRouter
Momenteel hebben de links in de navigatiebalk nog geen effect, alles wat deze links doen is de pagina herladen. Via BrowserRouter, Switch en Route, 3 componenten die aangeboden worden door react-router-dom, kunnen we specifiëren welke componenten getoond moeten worden voor elke route.
De BrowserRouter component moet rond de volledige applicatie staat, of toch rond alle delen die invloed hebben op, of beïnvloed worden door de routing. Een footer, die op elke pagina hetzelfde is, en geen link bevat naar andere pagina's in de React applicatie, kan eventueel buiten de BrowserRouter component geplaatst worden. De navbar moet binnen deze component staan omdat deze links bevat en dus invloed heeft op de routing.
import {BrowserRouter} from "react-router-dom";
ReactDOM.render(
<BrowserRouter>
<NavBarNoBootstrap/>
</BrowserRouter>,
document.getElementById('root')
);
2
3
4
5
6
7
8
Switch & Route
De Switch component bevat één of meerdere Route componenten, via de Route componenten kan aangegeven worden welke componenten getoond moeten worden voor welke route.
Een Route component gebruikt de property path om de route aan te geven.
<Route path="/stuff">
Deze route wordt dus aangesproken wanneer de route '/stuff' ingegeven wordt in de adresbalk. De tweede belangrijke property is component deze geeft aan welke component geladen moet worden als de opgegeven route in de adresbalk ingevuld wordt. De route
<Route path="/stuff" component={Stuff}>
toont dus de component Stuff als de route '/stuff' ingegeven wordt in de adresbalk. We kunnen deze syntax gebruiken om elke route te definiëren.
import Container from 'react-bootstrap/Container';
import 'bootstrap/dist/css/bootstrap.min.css';
const Main = (props) => {
return (
<Container className="mt-4">
<Switch>
<Route path="/" component={Home}/>
<Route path="/stuff" component={Stuff}/>
<Route path="/contact" component={Contact}/>
<Route path="/class" component={Class}/>
</Switch>
</Container>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Werkt de navigatiebalk niet naar behoren?
Check of je de juiste import hebt. Om het zo verwarrend mogelijk te maken heeft niet alleen react-router-dom een import voor Switch, maar bootstrap ook. Om de correcte werking van de applicatie te krijgen gebruiken we de import van react-router-dom. Controleer of jouw import gelijk is aan onderstaande.
import {Switch} from "react-router-dom";
In bovenstaande code gebruiken we de React Bootstrap Container component. Deze component wordt, net zoals alle andere Bootstrap componenten, individueel geïmporteerd. Dit zorgt voor een kleinere bundle, en hoe kleiner de bundle, hoe sneller de website. Daarnaast gebruiken we de Bootstrap klasse 'mt-4' om een top-marge toe te voegen. De Main component kan vervolgens toegevoegd worden aan de render functie.
ReactDOM.render(
<BrowserRouter>
<NavBarNoBootstrap/>
<Main/>
</BrowserRouter>,
document.getElementById('root')
);
2
3
4
5
6
7
De routes zijn wel gedefinieerd, maar op onderstaande preview is duidelijk te zien dat, ongeacht de aangeklikte route, de Home component getoond wordt.
Het probleem is dat we de route '/' als eerste gedefinieerd hebben, de Route component kiest de eerste route die overeenkomt. Elke route begint met een '/' en de Route component ziet een gedeeltelijke overeenkomst ook als een match.
Dit is eenvoudig op te lossen door de route '/' naar onder te verhuizen. Als alternatief kunnen we ook gebruik maken van de boolean property exact, dit attribuut geeft aan dat de route voor 100% overeen moet komen. Deze twee aanpakken kunnen gecombineerd worden, we kunnen alle routes behalve '/' het exact attribuut geven en '/' onderaan plaatsen. Zo maken we een catch-all route. Wanneer een url zoals '/sfsfljqfhqlhflh' ingegeven wordt, zal de gebruiker steeds naar de Home component gebracht worden, een soort 404 pagina dus.
const Main = (props) => {
return (
<Container className="mt-4">
<Switch>
<Route exact path="/stuff" component={Stuff}/>
<Route exact path="/contact" component={Contact}/>
<Route exact path="/class" component={Class}/>
<Route path="/" component={Home}/>
</Switch>
</Container>
);
}
2
3
4
5
6
7
8
9
10
11
12
SPA Links
Nu werkt de routing, alle links werken (Class is nog leeg). Het duurt wel te lang voor we een nieuwe pagina zien. In bovenstaande preview, krijg je even een wit scherm te zien voor de juiste component geladen wordt.
Het probleem is de structuur van de NavBarNoBootstrap component, deze is gebouwd door middel van anchor (<a>) tags. Een anchor tag is niet geschikt voor SPA's omdat dit tag de gebruiker naar een nieuwe pagina stuurt. Een single page application bestaat uit één pagina, de content van de pagina wordt door JavaScript afgehandeld. Anchor tags zijn echter bedoeld voor navigatie op klassieke websites, met meerdere HTML-pagina's. Dit betekent dat de volledige website dus herladen wordt.
React Router bevat een Link en NavLink component, beide componenten passen de URL in de adresbalk aan, maar doen dit zonder de pagina te herladen. Het verschil tussen de twee componenten is de opmaak. Aan een NavLink component kan CSS code of een CSS klasse toegevoegd worden die gebruik wordt als de link actief is. Verder is er geen enkel verschil tussen de twee componenten. Elk van deze componenten heeft een to property dat gebruikt kan worden om het pad door te geven.
<Link to="The link to navigate to">Link naam op website</Link>
<NavLink to="The link to navigate to" activeClassName="Een CSS klasse">
Link naam op website
</NavLink>
<NavLink to="The link to navigate to" activeStyle={"Een object met CSS styling"}>
Link naam op website
</NavLink>
2
3
4
5
6
7
De NavBarNoBootstrap component wordt dan
const NavBarNoBootstrap = (props) => {
// CSS weggelaten
return (
<ul style={ulStyle}>
<li style={liStyle}>
<Link to="/">Home</Link>
</li>
<li style={liStyle}>
<Link to="/stuff">Stuff</Link>
</li>
<li style={liStyle}>
<Link to="/contact">Contact</Link>
</li>
<li style={liStyle}>
<Link to="/class">Class</Link>
</li>
</ul>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Nu is de navigatie aanzienlijk sneller, de pagina moet niet herladen en de gebruikerservaring is beter.
Navigatie parameters
In de startbestanden is een klasse StudentApi te vinden, deze klasse definieert een array van dummy data. Deze array bevat een aantal studenten en hun score voor een bepaald vak.
class StudentApi {
constructor() {
this.students = [
{id: 565, name: "Annelies Gevers", grade: "A-"},
{id: 11, name: "Ben Pauwels", grade: "A+"},
{id: 91, name: "Elien Stevens", grade: "F"},
{id: 23, name: "David Van Mol", grade: "D"},
{id: 4002, name: "Paul Verstraeten", grade: "F"},
{id: 8, name: "Sandra Wouters", grade: "D-"}
];
}
getAllStudents = () => {
return this.students;
}
getStudentById = (id) => {
return this.students.find((s) => s.id === id);
}
}
export default StudentApi;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Elk van de studenten heeft een id, via dit id kunnen we een detailview bouwen voor één bepaalde student. We beginnen met een overzicht te genereren van elke student in de klas. Hiervoor kunnen we het pad '/class' gebruiken, we moeten het exact attribuut wel verwijderen. Dit attribuut geeft aan dat de URL exact gelijk moet zijn aan '/class', maar we willen het pad uitbreiden met een optionele parameter.
const Main = (props) => {
return (
<Container className="mt-4">
<Switch>
<Route exact path="/stuff" component={Stuff}/>
<Route exact path="/contact" component={Contact}/>
<Route path="/class" component={Class}/>
<Route path="/" component={Home}/>
</Switch>
</Container>
);
}
2
3
4
5
6
7
8
9
10
11
12
Door het exact attribuut te verwijderen worden alle paden die beginnen met '/class' doorgestuurd naar de component Class. Binnen deze component moet er een onderscheid gemaakt worden tussen een pad met en zonder parameter. Hiervoor kunnen we opnieuw gebruik maken van de Switch en Route componenten. Binnen de Class component kunnen wel gebruik maken van het exact attribuut, er zijn geen dieper geneste routes meer.
Concept: Navigatie parameter
Een navigatie parameter wordt aan een route toegevoegd via een dubbelpunt. Stel dat we een parameter willen toevoegen aan het pad '/voorbeeld', dan kunnen we dit noteren als '/voorbeeld/:param1'. Deze notatie kan uitgebreid worden naar meerdere parameters, '/voorbeeld/:param1/:param2'. Hier zijn param1 en param2 de namen die gebruikt moeten worden om de waarde van de parameter uit te lezen.
const Class = (props) => {
return(
<Switch>
<Route exact path={props.match.path + "/:id"} component={Student}/>
<Route exact path={props.match.path} component={Students}/>
</Switch>
);
}
export default Class;
2
3
4
5
6
7
8
9
10
In bovenstaande code wordt gebruik gemaakt van props.match.path, deze property die aanwezig is op directe kinderen van een Route component, bevat het pad waarlangs de code in deze component gekomen is, in dit geval dus '/class' (gedefinieerd in index.js).
Het startbestand voor de Students component is bijna volledig, we moeten enkel de lijst van studenten uitprinten en navigatie naar de detailpagina voorzien. Om de studenten uit te printen kunnen we de map functie gebruiken. De navigatie naar de detailpagina kan opnieuw geïmplementeerd worden via props.match. Dit keer gebruiken we props.match.url, we moeten hier enkel het id van de student bijvoegen.
Concept: props.match
React router voorziet zowel een path als url property. De eerste property bevat het gematchte path, zoals het in een Route component gedefinieerd is. Dit betekent dat de parameter er in staat als :paramNaam.
De tweede property is de URL die de gebruiker bezoekt, dit betekent dat de parameters een specifieke waarde krijgen. Het dubbelpunt wordt niet meer getoond.
const Students = (props) => {
const api = new StudentApi();
return (
<Card>
<Card.Header>Class</Card.Header>
<ListGroup>
{api
.getAll()
.map(s =>
<ListGroupItem key={s.id}>
<Link to={`${props.match.url}/${s.id}`}>{s.name}</Link>
</ListGroupItem>)}
</ListGroup>
<Card.Footer className="text-muted">
<Link to={"/class"}>Back</Link>
</Card.Footer>
</Card>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Als we nu op de naam van een student klikken krijgen we detailpagina te zien, momenteel bevat deze nog dummy data en wordt de navigatie parameter nog niet gebruikt.
Om de parameter uit te lezen kunnen opnieuw gebruik maken van props.match, deze property is opnieuw enkel beschikbaar voor directe kinderen van een Route component. In de les over hooks, bespreken we hoe we deze property kunnen gebruiker in dieper geneste componenten. De eigenschap props.match bevat een object met de naam params, binnen dit object worden de navigatie parameters bewaard. In de component Student kunnen we dus, als volgt, de juiste student ophalen. Let op, een navigatie parameter is steeds een string, we moeten dit dus nog converteren naar een number.
const student = api.getById(Number(props.match.params.id));
We moeten nog een link toevoegen om terug te keren naar de vorige pagina, dit kan natuurlijk via het hardgecodeerde pad '/class'. Maar we kunnen ook hier gebruiken maken van een property. De Route component geeft aan zijn directe kinderen steeds een property history mee. Hierin is een methode goBack() beschikbaar die de bezoeker naar de vorige pagina brengt. We kunnen deze methode als volgt gebruiken.
<Card.Footer>
<div onClick={props.history.goBack}>Back</div>
</Card.Footer>
2
3
Navigatie met Bootstrap NavBar
In de vorige voorbeelden hebben we de NavBarNoBootstrap component gebruikt. Natuurlijk willen we de navigatiebalk ook via React Bootstrap opbouwen. In de documentatie vinden we een voorbeeld dat eenvoudig aan te passen is voor onze website.
const NavBarWithBootstrap = (props) => {
return (
<Navbar bg="dark" expand="lg" variant="dark">
<Navbar.Brand href="/">SPA</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav"/>
<Navbar.Collapse id="basic-navbar-nav">
<Nav>
<Nav.Link href={"/"}>Home</Nav.Link>
<Nav.Link href={"/stuff"}>Stuff</Nav.Link>
<Nav.Link href={"/contact"}>Contact</Nav.Link>
<Nav.Link href={"/class"}>Class</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Onderstaande preview toont dat dit niet ideaal is, de website wordt opnieuw herladen als er op een link gedrukt wordt.
Dit probleem doet zich voor omdat React Bootstrap een implementatie is van Bootstrap in React. Dit betekent dat alle componenten onderliggend klassieke Bootstrap code produceren, de Nav.Link component gebruikt dus <a> tags en geen Link componenten. We kunnen het probleem proberen op te lossen op dezelfde manier als in de originele navbar.
const NavBarWithBootstrap = (props) => {
return (
<Navbar bg="dark" expand="lg" variant="dark">
<Navbar.Brand href="/">SPA</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav"/>
<Navbar.Collapse id="basic-navbar-nav">
<Nav>
<Link to={"/"}>Home</Link>
<Link to={"/stuff"}>Stuff</Link>
<Link to={"/contact"}>Contact</Link>
<Link to={"/class"}>Class</Link>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
De navigatie is nu wel in orde, maar de Bootstrap opmaak is verloren.
We moeten dus een andere oplossing zoeken. De bibliotheek React Router Bootstrap biedt een oplossing. We kunnen dit pakket installeren via NPM.
npm install --save react-router-bootstrap
Deze bibliotheek introduceert de component LinkContainer die ronder een Bootstrap link geplaatst kan worden, maar via de React Router afgehandeld wordt. Deze component moet rond elke React Bootstrap component, die een link naar een andere pagina op de website bevat, geplaatst worden.
import {LinkContainer} from 'react-router-bootstrap'
const NavBarWithBootstrap = (props) => {
return (
<Navbar bg="dark" expand="lg" variant="dark">
<LinkContainer to={"/"}>
<Navbar.Brand>SPA</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="basic-navbar-nav"/>
<Navbar.Collapse id="basic-navbar-nav">
<Nav>
<LinkContainer exact to="/">
<Nav.Link>Home</Nav.Link>
</LinkContainer>
<LinkContainer to="/stuff">
<Nav.Link>Stuff</Nav.Link>
</LinkContainer>
<LinkContainer to="/contact">
<Nav.Link>Contact</Nav.Link>
</LinkContainer>
<LinkContainer to="/class">
<Nav.Link>Class</Nav.Link>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Properties doorgeven via de Route component.
De Class component werkt momenteel perfect, maar als we de code anders structureren zijn er nog een aantal problemen. Stel dat de studenten in de state van een hoger liggende component bewaard moeten worden. Dan hebben we een probleem, we kunnen de studenten niet doorgeven naar de Class, Student, of Students componenten. Laten we deze situatie simuleren door een lijst van alle studenten op te halen in de Class component. Vervolgens gebruiken we in de Students en Students componenten, een property students, waarin een array van alle studenten bewaard wordt.
De Student component moet deze property dan filteren, zodat enkel de juiste student getoond wordt.
const Student = (props) => {
const student = props.students.find(s => s.id === Number(props.match.params.id));
if (!student) {
return <div>Id does not exist</div>
}
return (
<Card>
<Card.Header>{student.name}</Card.Header>
<Card.Body>
<Card.Text>Id: {student.id}</Card.Text>
<Card.Text>Grade: {student.grade}</Card.Text>
</Card.Body>
<Card.Footer>
<div onClick={props.history.goBack}>Back</div>
</Card.Footer>
</Card>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In de Students component moet de oproep naar de API klasse vervangen worden.
const Students = (props) => {
return (
<Card>
<Card.Header>Class</Card.Header>
<ListGroup>
{props.students
.map(s =>
<ListGroupItem key={s.id}>
<Link to={`${props.match.url}/${s.id}`}>{s.name}</Link>
</ListGroupItem>)}
</ListGroup>
<Card.Footer className="text-muted">
<Link to={"/"}>Back</Link>
</Card.Footer>
</Card>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
De Class component moet de API dan aanspreken. Om de properties door te geven kunnen de componenten Student en Students als kind van een Route definiëren.
const Class = (props) => {
const api = new StudentApi();
const students = api.getAll();
return(
<Switch>
<Route exact path={props.match.url + "/:id"}>
<Student students={students}/>
</Route>
<Route exact path={props.match.url}>
<Students students={students}/>
</Route>
</Switch>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Alhoewel we de properties correct hebben doorgeven krijgen we toch een foutmelding als we het pad '/class' bezoeken.
Het probleem is dat de Student en Students componenten nu geen toegang meer hebben tot de navigatie properties die in de vorige versies doorgegeven worden aan door de Route componenten. Ook dit probleem is oplosbaar, via de property render van de Route componenten. Dit attribuut krijgt als waarde een functie met de navigatie properties (match, params, history, location) als argument en geeft de component die gerenderd moet worden terug.
const Class = (props) => {
const api = new StudentApi();
const students = api.getAll();
return(
<Switch>
<Route exact path={props.match.url + "/:id"}
render={navProps => <Student students={students} {...navProps}/>}/>
<Route exact path={props.match.url}
render={navProps => <Students students={students} {...navProps}/>}/>
</Switch>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14