~$ git clone git@github.com:HOGENT-Web/frontendweb-budget.git (of git pull als het niet de eerste keer is) ~$ cd frontendweb-budget ~/frontendweb-budget$ git checkout -b les5 44ab9b6 ~/frontendweb-budget$ yarn install
~$ npx create-react-app routing-demo ~$ yarn add react-router-dom@^5.3.0 react-router@^5.2.1 ~$ yarn start
Deze slides gebruiken v5 van React Router, v6 wijkt hiervan af
(zie documentatie).
Je mag zeker updaten in jouw project.
> src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// ...
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);
// ...
De BrowserRouter is een van de twee mogelijkheden in react-router-dom om te gebruiken als router. Dit type router zal functioneren zoals je verwacht dat een router funtioneert: hij gebruikt het deel na de / om naar een pagina te navigeren. Dit is zeer gelijkaardig aan hoe server-side gerenderde pagina's werken.
Een probleem hierbij is dat browser standaard gaan refreshen wanneer de URL na de / wijzigt, react-router-dom zal dit in dit geval opvangen en voorkomen.
Het voordeel met dit soort routers is dat je webapplicatie werkt zoals een old-school website, met alle features die een URL te bieden heeft.
Het tweede type routes is de HashRouter. Deze router gebruikt de hash (of dus de #) in de url om te navigeren tussen pagina's.
Dit heeft als voordeel dat de browser by default niet zal refreshen. Het nadeel is dat je de hash niet meer kan gebruiken om te scrollen naar een element in de pagina.
Dit soort routers wordt typisch weinig gebruikt, we raden aan om BrowserRouter te gebruiken.
> src/pages.jsx
import { LoremIpsum } from 'react-lorem-ipsum';
export const Home = () => (
<div>
<h1>Welkom!</h1>
<p>Hier is nog niet zoveel te zien.</p>
</div>
);
export const About = () => (
<div>
<h1>Over ons</h1>
<LoremIpsum p={2} />
</div>
);
export const Contact = () => (
<div>
<h1>Contact</h1>
<LoremIpsum p={2} />
</div>
);
export const NotFound = () => {
return (
<div>
<h1>Pagina niet gevonden</h1>
<p>
Er is geen pagina met op deze url, probeer iets anders.
</p>
</div>
);
};
> src/App.jsx
import { Switch, Route } from 'react-router-dom'
import { Home, About, Contact } from './pages';
function App() {
return (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/over" render={() => <About/>} />
<Route exact path="/contact">
<Contact />
</Route>
</Switch>
);
}
export default App;
> src/pages.jsx
import { Link } from 'react-router-dom';
export const Home = () => (
<div>
<h1>Welkom!</h1>
<p>Kies één van de volgende links:</p>
<ul>
<li>
<Link to="/over">Over ons</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</div>
);
Om eigenschappen over routes op te vragen bestaat een hook: useLocation
> src/pages.jsx
import { useLocation } from 'react-router-dom';
// ...
export const NotFound = () => {
const { pathname } = useLocation();
return (
<div>
<h1>Pagina niet gevonden</h1>
<p>
Er is geen pagina met op dezeals url {pathname}, probeer iets anders.
</p>
</div>
);
};
useLocation geeft het location object terug. Dit bevat informatie over de huidige route. Je kan deze hook vergelijken met een useState die telkens een location object teruggeeft wanneer hier iets aan wijzigt.
Een voorbeeld hiervan is:
{
key: 'ac3df4',
pathname: '/somewhere',
search: '?some=search-string',
hash: '#howdy',
state: {
[userDefined]: true
}
}
Dit object bevat volgende properties:
Let op: bij hash en search staat respectievelijk een # of ? voor de data die je eigenlijk wil. Hou hier rekening mee tijdens het ontwikkelen van je applicatie.
Voor query parameters kan je gebruik maken van het npm-package qs. Dit helpt bij het parsen van de query parameters, maar dit is uiteraard niet verplicht.
Je kan location objecten ook gebruiken bij de prop to van de Link component. Zo hoef je geen correct opgebouwde string mee te geven, maar kan je het meer in stukjes opdelen. Let op: ook hier moet je de # of ? voor de hash of search plaatsen.
> src/App.jsx
// ...
import { Home, About, Contact, NotFound } from './pages';
function App() {
return (
<Switch>
{/* ... */}
<Route path="*">
<NotFound />
</Route>
</Switch>
);
}
// ...
> src/pages.jsx
// ...
const About = () => (
<div>
<h1>Over ons</h1>
<LoremIpsum p={2} />
<ul>
<li>
<Link to="/over/services">Onze diensten</Link>
</li>
<li>
<Link to="/over/history">Geschiedenis</Link>
</li>
<li>
<Link to="/over/location">Locatie</Link>
</li>
</ul>
</div>
);
// ...
> src/pages.jsx
import { Switch, Route } from 'react-router-dom';
import { useRouteMatch } from 'react-router';
// ...
export const AboutModule = () => {
const { path } = useRouteMatch();
return (
<Switch>
<Route exact path={path}>
<About />
</Route>
<Route exact path={`${path}/services`}>
<Services />
</Route>
<Route exact path={`${path}/history`}>
<History />
</Route>
<Route exact path={`${path}/location`}>
<Location />
</Route>
</Switch>
);
};
// ...
> src/App.jsx
// ...
import { AboutModule } from './pages';
function App() {
return (
<Switch>
{/* andere routes */}
<Route path="/over" render={() => <About />}/>
<Route path="/over">
<AboutModule />
</Route>
{/* 404 hier */}
</Switch>
);
}
export default App;
De extra component bevatten louter een titel met wat lorem ipsum tekst:
const Services = () => (
<div>
<h1>Our services</h1>
<LoremIpsum p={2} />
</div>
);
const History = () => (
<div>
<h1>History</h1>
<LoremIpsum p={2} />
</div>
);
const Location = () => (
<div>
<h1>Location</h1>
<LoremIpsum p={2} />
</div>
);
Stel: we willen dat gebruikers die naar /services navigeren naar /over/services doorgestuurd worden.
> src/App.jsx
// ...
import { Redirect } from 'react-router-dom';
function App() {
return (
<Switch>
{/* ... */}
<Redirect from="/services" to="/over/services" />
{/* ... */}
</Switch>
);
}
export default App;
URL parameters zijn stukjes uit de URL die ingevuld moeten worden,
zoals bijvoorbeeld het id van een entiteit.
Merk op: route nesting kan ook hier gebruikt worden voor de routes onder /products. Probeer dit zelf eens.
> src/App.jsx
// ...
function App() {
return (
<Switch>
{/* ... */}
<Route exact path="/products">
<Products />
</Route>
<Route exact path="/products/:id">
<Product />
</Route>
{/* ... */}
</Switch>
);
}
// ...
Maak een nieuwe component in het pages-bestand.
> src/pages.jsx
// ...
const products = [{
id: 1,
name: 'Confituur',
price: 2.50
}, {
id: 2,
name: 'Choco',
price: 3.50
}, {
id: 3,
name: 'Coco-cola',
price: 3.20
}, {
id: 4,
name: 'Fanta',
price: 3.00
}, {
id: 5,
name: 'Sprite',
price: 2.90
}];
export const Products = () => (
<ul>
{products.map(({ name }) => (
<li>
{name}
</li>
))}
</ul>
);
> src/pages.jsx
import { useLocation, useParams } from 'react-router';
// ...
export const Product = () => {
const { id } = useParams();
const idAsNumber = Number(id);
const product = products.find((p) => p.id === idAsNumber);
if (!product) {
return (
<div>
<h1>Product niet gevonden</h1>
<p>
Er is geen product met id {id}.
</p>
</div>
)
}
return (
<div>
<h1>{product.name}
<p><b>Price:</b> € {product.price}</p>
</div>
)
};
De useParams hook retourneert een object van key/value pairs. Dit object bevat alle URL parameters met hun waarde uit de huidige url.
Deze hook zal een nieuw object retourneren telkens wanneer een URL parameter wijzigt.
Stel, we hebben volgende routes gedefinieerd:
function App() {
return (
<Switch>
<Route exact path="/products/:id">
<Product />
</Route>
<Route exact path="/posts/:year/:month">
<Posts />
</Route>
</Switch>
);
}
Wanneer we navigeren naar /products/263, dan zal de Product component getoond worden. Zijn useParams zal volgend object retourneren:
{
id: '263'
}
Merk op dat elke value een string is en niet een number in dit geval. We zullen de string dus zelf nog moeten parsen via de Number-functie.
Wanneer we navigeren naar /posts/2021/1, dan zal de Posts component getoond worden. Zijn useParams zal volgend object retourneren:
{
year: '2021',
month: '1'
}
> src/ScrollToTop.jsx
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
In moderne browsers kan gebruik gemaakt van een smooth scroll i.p.v. plots de pagina naar boven te laten springen. Dit wordt echter niet ondersteund in bv. Internet Explorer en moet dus opgevangen worden. Dit kan met volgende code:
try {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
} catch {
// Fallback voor oude browsers
window.scrollTo(0, 0);
}
Als de moderne window.scrollTo niet ondersteund wordt, zal een error optreden aangezien de browsers een getal verwacht als eerste argument. In dat geval wordt deze error opgevangen, genegeerd en wordt de oude versie van de functie gebruikt.
Je kan deze code gebruiken in de useEffect van de vorige slide.
> src/App.jsx
// ...
import ScrollToTop from './ScrollToTop';
function App() {
return (
<>
<ScrollToTop />
<Switch>
{ /* ... */ }
</Switch>
</>
);
}
> src/App.jsx
// ...
import { useHistory } from 'react-router-dom';
import { useCallback } from 'react';
function App() {
const history = useHistory();
const handleGoHome = useCallback(() => {
history.push('/');
}, [history]);
return (
<>
<ScrollToTop />
<Switch>
{ /* ... */ }
</Switch>
</>
<button onClick={handleGoHome}>Go home!</button>
);
}
De useHistory hook werkt achterliggend met het npm-package history. Het laat eenvoudig beheer van session history toe in JavaScript.
Achter de schermen wordt een stack gebruikt in combinatie met een pointer. Deze pointer wijst naar het huidige element in de stack, dit is niet noodzakelijk het bovenste. Dit is misschien atypisch in vergelijking met een pure stack.
Het history object bevat volgende properties en functies:
In de praktijk worden push en replace het meest gebruikt. Er is een duidelijk verschil tussen beide met een reden: ze bepalen of de gebruiker terug kan keren naar de url van waarop de functies aangeroepen worden.
Check uit op deze commit van onze voorbeeldapplicatie
~$ git clone git@github.com:HOGENT-Web/frontendweb-budget.git (of git pull als het niet de eerste keer is) ~$ cd frontendweb-budget ~/frontendweb-budget$ git checkout -b oplossing-h4 c982569 ~/frontendweb-budget$ yarn install