Les 3: Single Page Applications
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 websites 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.
React Router installeren
Een React project, aangemaakt via pnpm create vite, 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.
pnpm add 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 in 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.
pnpm add react-bootstrap
React Bootstrap steunt nog steeds op de standaard Bootstrap CSS-regels, de nodige stylesheets kunnen via NPM geïnstalleerd worden.
pnpm add bootstrap
Vervolgens moet de Bootstrap CSS geïmporteerd worden in main.jsx.
import 'bootstrap/dist/css/bootstrap.min.css';
Navigatie met React Router
We zullen een applicatie bouwen die 4 pagina's bevat, Home, Foo, Bar, en Class. Voor we de routing kunnen implementeren moet het mogelijk zijn om naar deze pagina's te navigeren. Hiervoor creëren we een eenvoudige navigatiebalk.
import styled from 'styled-components';
const NavUL = styled.ul`
list-style: none;
`
const NavLi = styled.li`
display: inline-block;
margin: 1em;
`
const NavBarNoBootstrap = () => {
return (
<NavUL>
<NavLi><a href={"/"}>Home</a></NavLi>
<NavLi><a href={"/foo"}>Foo</a></NavLi>
<NavLi><a href={"/bar"}>Bar</a></NavLi>
<NavLi><a href={"/class"}>Class</a></NavLi>
</NavUL>
)
}
export default NavBarNoBootstrap;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<NavBarNoBootstrap/>
</StrictMode>
)
BrowserRouter
Momenteel hebben de links in de navigatiebalk nog geen effect, alles wat deze links doen is de pagina herladen. Via BrowserRouter, Routes 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 links 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';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<BrowserRouter>
<NavBarNoBootstrap/>
</BrowserRouter>
</StrictMode>
)
Routes & Route
De Routes 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.
import {Route, Routes} from 'react-router-dom';
const Routing = () => {
return (
<Routes>
<Route path={'/foo'}/>
</Routes>
)
}
export default Routing;
Deze route wordt dus aangesproken wanneer de route '/foo' ingegeven wordt in de adresbalk. De tweede belangrijke property is element deze geeft aan welke component geladen moet worden als de opgegeven route in de adresbalk ingevuld wordt. Onderstaande code toont dus de component Foo als de route '/foo' ingegeven wordt in de adresbalk.
We gebruiken dezelfde syntax om ook de andere routes toe te voegen.
Natuurlijk moet de Routing component zelf ook nog opgeroepen worden, dit doen we vlak onder de navbar.
import {Route, Routes} from 'react-router-dom';
const Routing = () => {
return (
<Routes>
<Route path={'/foo'} element={<Foo/>}/>
<Route path={'/bar'} element={<Bar/>}/>
<Route path={'/'} element={<Home/>}/>
<Route path={'/class'} element={<Class/>}/>
</Routes>
)
}
export default Routing;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<BrowserRouter>
<Container className="mt-4">
<NavBarNoBootstrap/>
<Routing/>
</Container>
</BrowserRouter>
</StrictMode>
)
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.
404 Pagina
Natuurlijk heeft elke website nood aan 404 pagina die getoond wordt in het geval een gebruiker een niet bestaande URL. Om zo'n pad te selecteren maken we gebruik van het wildcard (*) karakter. Onderstaande route komt overeen met alle mogelijke routes. Natuurlijk moeten we ook een link toevoegen aan de NavBarNoBootstrap component.
const Routing = () => {
return (
<Routes>
<Route path={'/foo'} element={<Foo/>}/>
<Route path={'/bar'} element={<Bar/>}/>
<Route path={'/'} element={<Home/>}/>
<Route path={'/class'} element={<Class/>}/>
<Route path={'*'} element={<PageNotFound/>}/>
</Routes>
)
}
const NavBarNoBootstrap = () => {
return (
<NavUL>
<NavLi><a href={"/"}>Home</a></NavLi>
<NavLi><a href={"/foo"}>Foo</a></NavLi>
<NavLi><a href={"/bar"}>Bar</a></NavLi>
<NavLi><a href={"/class"}>Class</a></NavLi>
<NavLi><a href={"/sdfsfhsfh867h6"}>Error page</a></NavLi>
</NavUL>
)
}
De PageNotFound component is gegeven in de startbestanden (de useCountdown hook mag je voorlopig nog negeren, we bekijken volgende les hoe je deze zelf kan schrijven). Als je deze pagina opent, zie je voorlopig een countdown van 5 tot 0, maar als deze afgelopen is, gebeurt er nog niets.
Navigate
Om de PageNotFound pagina te laten werken, kunnen we gebruik maken van de Navigate component die aangeboden wordt door React Router. Deze component heeft een property to waarmee de URL waarnaar genavigeerd moet worden doorgegeven kan worden, in dit geval dus de homepage ('/').
import {Navigate} from 'react-router-dom';
const PageNotFound = () => {
const countdown = useCountdown(5);
if (countdown === 0) {
return <Navigate to={'/'}/>
}
return (
<div>
<Video404 src={gif} autoPlay={true} loop={true}/>
<CounterContainer>
Redirecting to home in {countdown} seconds
</CounterContainer>
</div>
)
}
SPA Links
De routing nu, alle pagina's kunnen bezocht worden. Het duurt wel te lang voor we een nieuwe pagina zien, als je het network tab opent in de developer tools van je favoriete browser, zie je dat de volledige website bij elke klik op een navigatie link opnieuw gedownload wordt van de server. Onderstaande video demonstreert dit.
Het probleem is het gebruik van anchor (<a>) tags in de NavBarNoBootstrap component. Een anchor tag is niet geschikt voor gebruik in 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 opgevuld. Dit betekent dat we geen nieuwe pagina willen openen, maar de chunks die de inhoud van de nieuwe pagina bevatten willen downloaden, dit moet natuurlijk door react/react-router-dom afgehandeld worden.
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 via de property style of className een functie meegegeven worden die de opmaak aanpast als de link actief is. Verder is er geen verschil tussen de twee componenten. Elk van deze componenten heeft een to property die gebruikt kan worden om het pad door te geven waarnaar genavigeerd moet worden als op de link geklikt wordt.
Begrip: SPA Links
SPA links zijn links die gebruikt kunnen worden om te navigeren binnen een SPA zonder de pagina te moeten herladen. React router biedt twee mogelijke componenten aan Link en NavLink.
import {Link, NavLink} from 'react-router-dom';
<Link to="The path to navigate to">Link text on the website</Link>
<NavLink to="The path to navigate to" className={({isActive}) => isActive ? 'activeClass' : 'standardClass'}>
Link text on the website
</NavLink>
<NavLink to="The path to navigate to" activeStyle={"Een object met CSS styling"}>
Link naam op website
</NavLink>
De NavBarNoBootstrap component wordt dan
const NavBarNoBootstrap = () => {
const activeStyle = {
color: '#49DE73',
}
const chooseStyle = ({isActive}) => {
return isActive ? activeStyle : {};
}
return (
<NavUL>
<NavLi>
<NavLink to={"/"} style={chooseStyle}>Home</NavLink>
</NavLi>
<NavLi>
<NavLink to={"/foo"} style={chooseStyle}>Foo</NavLink>
</NavLi>
<NavLi>
<NavLink to={"/bar"} style={chooseStyle}>Bar</NavLink>
</NavLi>
<NavLi>
<NavLink to={"/class"} style={chooseStyle}>Class</NavLink>
</NavLi>
<NavLi>
<NavLink to={"/sdfsfhsfh867h6"} style={chooseStyle}>Error page</NavLink>
</NavLi>
</NavUL>
)
}
Nu is de navigatie aanzienlijk sneller, de pagina moet niet herladen en de gebruikerservaring is beter. Onderstaande video demonstreert dit, enkel de eerste keer dat de pagina geladen wordt, zie je veel beweging in het network tab.
Navigatie met parameters
In de startbestanden is een klasse StudentApi te vinden, deze klasse definieert een array van dummy data die een aantal studenten en hun score voor een bepaald vak bevat.
const 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-"}
];
export const getAllStudents = () => {
return students.map(s => ({...s}));
}
export const getStudentById = (id) => {
return students.find((s) => s.id === id);
}
Elk van de studenten heeft een id, via dit id kunnen we een detail-view bouwen voor één bepaalde student. De Class component bevat reeds een overzicht van alle studenten, enkel de link naar de detailpagina moet nog toegevoegd worden.
Om de link toe te voegen hebben we twee opties, we kunnen een absoluut of relatief pad gebruiken.
Begrip: Relatieve en absolute paden in React Router
Een absoluut pad begint met een forward-slash (/) en bevat het volledige pad, van de root.
Een relatief pad begint niet met een forward-slash, maar met het volgende deel van het pad. Stel de link is gedefinieerd in een component die zich op het pad /foo/bar bevindt, dan kunnen we in deze component een link definiëren als baz. Omdat dit pad relatief is, zal het opgevuld worden ten opzichte van het huidige pad en wordt een gebruiker na het drukken op deze link dus naar /foo/bar/baz gestuurd.
const Class = () => {
const students = getAllStudents();
const studentItem = (s) => (
<ListGroupItem key={s.id}>
<Link to={`${s.id}`}>
{s.name}
</Link>
</ListGroupItem>
)
return (
<Card>
<Card.Header>Class</Card.Header>
<ListGroup>
{students.map(s => studentItem(s))}
</ListGroup>
<Card.Footer className="text-muted">
<Link to={"/"}>Back</Link>
</Card.Footer>
</Card>
)
}
Momenteel leidt de link nog naar de 404 pagina omdat we nog nergens gedefinieerd hebben dat er detail-route bestaat. Door in de Route component waarin de /class route gedefinieerd wordt een extra Route component te plaatsen kunnen we een detail-route aanmaken. De parameter, het id van de student, wordt via een dubbelpunt aangegeven.
Begrip: 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 in de component die aan de route gelinkt is.
const Routing = () => {
return (
<Routes>
<Route path={'/foo'} element={<Foo/>}/>
<Route path={'/bar'} element={<Bar/>}/>
<Route path={'/'} element={<Home/>}/>
<Route path={'/class'} element={<Class/>}>
<Route path={':id'} element={<Student/>}/>
</Route>
<Route path={'*'} element={<PageNotFound/>}/>
</Routes>
)
}
Als we nu op een detail-link klikken, zien we de URL wel wijzigen, maar wel de detail-pagina nog niet.
React Router bevat een Outlet component die gebruikt moet worden om child-routes te renderen. We kunnen deze component toevoegen aan de Class component.
import {Link, Outlet} from 'react-router-dom';
const Class = () => {
const students = getAllStudents();
const studentItem = (s) => (
// Niet relevant en leeg gelaten in dit fragment
)
return (
<>
<Card>
{/* Niet relevant en weggelaten in dit fragment */}
</Card>
<Outlet/>
</>
)
}

Het is duidelijk dat dit niet is wat we willen bereiken. De Outlet component staat, in dit geval, op de foute plaats. Als de Class component een statisch gedeelte had, dat gelijk was voor alle kinderen, zou Outlet hier wel gebruikt kunnen worden.
In dit geval, willen we geen statische delen tonen, maar een volledig nieuwe component voor de paden /class en /class:id. Om dit te bereiken passen we de Routing component nogmaals aan. We voegen een kind toe aan de /class route zonder een path maar met de index property, vervolgens koppelen we hier de Class component aan. De index property duidt de default aan, als geen van de kinderen matcht met het ingegeven pad (in de URL-balk). Het element dat gerenderd wordt door de Class component wordt tenslotte ingesteld op de Outlet component.
const Routing = () => {
return (
<Routes>
<Route path={'/foo'} element={<Foo/>}/>
<Route path={'/bar'} element={<Bar/>}/>
<Route path={'/'} element={<Home/>}/>
<Route path={'/class'} element={<Outlet/>}>
<Route index element={<Class/>}/>
<Route path={':id'} element={<Student/>}/>
</Route>
<Route path={'*'} element={<PageNotFound/>}/>
</Routes>
)
}
useParams
Momenteel toont de Students component nog steeds een foutmelding, dit is te verwachten aangezien we de navigatieparameter nog niet uitgelezen hebben. Dit kan via de useParams hook. Deze hook geeft een object terug, waarin elke parameter voor de route beschikbaar is onder dezelfde naam als in de Route component die de parameter definieert.
Navigatieparameters worden via de URL doorgegeven en zijn dus altijd strings, omdat de getStudentById een argument van het type number verwacht, moeten we de parameter eerst casten.
import {useParams} from 'react-router-dom';
const Student = () => {
const {id} = useParams();
const student = getStudentById(Number(id));
if (!student) {
return <div>Student could not be found</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>Back</div>
</Card.Footer>
</Card>
)
}
useNavigate
We maken tenslotte opnieuw gebruik van de useNavigate hook om terug te gaan naar de vorige pagina. Deze hook geeft een functie terug die exact één argument heeft. De URL waarnaar genavigeerd moet worden. Dit kan een volwaardig pad zijn, zoals '/class', maar kan ook een negatief getal zijn, zoals . Hier betekent de dat we terug willen gaan in de navigatiegeschiedenis en de dat we één pagina terug willen gaan. Een parameter zou dus betekenen dat we 5 pagina's terug willen gaan.
import {useNavigate, useParams} from 'react-router-dom';
const Student = () => {
const {id} = useParams();
const navigate = useNavigate();
const student = getStudentById(Number(id));
if (!student) {
return <div>Student could not be found</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={() => navigate(-1)}>Back</div>
</Card.Footer>
</Card>
)
}
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 NavBarBootstrap = () => {
return (
<Navbar bg="dark" expand="lg" variant="dark">
<Container fluid>
<Navbar.Brand href="/">Les 3: 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={"/foo"}>Foo</Nav.Link>
<Nav.Link href={"/bar"}>Bar</Nav.Link>
<Nav.Link href={"/class"}>Class</Nav.Link>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<BrowserRouter>
<NavBarBootstrap/>
<Container className="mt-4">
<NavBarNoBootstrap/>
<Routing/>
</Container>
</BrowserRouter>
</StrictMode>
)
Net zoals toen we hierboven anchor tags gebruikt hebben, wordt tijdens het navigeren met de React Bootstrap navbar, ook alles herladen. We kunnen hier de Nav.Link component echter niet zomaar vervangen met een Link of NavLink component. De Nav.Link component bevat namelijk bootstrap klassen die niet aanwezig zijn in de react-router componenten. Om toch SPA links te gebruiken, en geen opmaak te verliezen, kunnen we een library installeren die integreert tussen react-router en react-bootstrap.
pnpm add react-router-bootstrap
import {LinkContainer} from 'react-router-bootstrap';
const NavBarBootstrap = () => {
return (
<Navbar bg="dark" expand="lg" variant="dark">
<Container fluid>
<LinkContainer to={"/"}>
<Navbar.Brand>Les 3: SPA</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="basic-navbar-nav"/>
<Navbar.Collapse id="basic-navbar-nav">
<Nav>
<LinkContainer to={"/"}>
<Nav.Link>Home</Nav.Link>
</LinkContainer>
<LinkContainer to={"/foo"}>
<Nav.Link>Foo</Nav.Link>
</LinkContainer>
<LinkContainer to={"/bar"}>
<Nav.Link>Bar</Nav.Link>
</LinkContainer>
<LinkContainer to={"/class"}>
<Nav.Link>Class</Nav.Link>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
)
}
Samenvatting & voorbeeldcode
Volledig uitgewerkte lesvoorbeelden met commentaar
layout: Slide
<ExampleCode/>