React Router

Pieter Van Der Helst, Thomas Aelbrecht, Karine Samyn

"Sometimes it pays to stay in bed on Monday, rather than spending the rest of the week debugging Monday's code." - Christopher Thompson

overview.

  1. Probleem
    Hoe werkt routing in een SPA?
  2. React Router
    package voor routing in React
  3. Routes definiëren
    definieer URLs binnen de applicatie
  4. Navigeren tussen pagina's
    wissel van pagina
  5. Route properties
    vraag informatie over de huidige route
  6. Nesting routes
    maak niveau's in de routing
  7. Redirects
    stuur de gebruiker naar een andere URL (met een component)
  8. URL parameters
    geef info mee in de URL en haal deze op
  9. Scroll restoration
    scroll naar boven na elke navigatie
  10. Navigeren vanuit code
    wissel van pagina via code
  11. Oefening
    implementeer routing in de budget app

budget app startpunt

Als je vorige week op facebook zat (of op café), branch de voorbeeldrepo op de juiste plaats en je kan weer inpikken!

~$ 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

Probleem

  • Single Page Application (SPA):
    • slechts één index.html
    • alles gebeurt client side
    • geen statische of server side gegenereerde pagina's
  • React is een library → geen ingebouwde router
  • Oplossing: React Router

React Router

  • npm package
  • declaratieve routing voor React (o.b.v. componenten)
  • voor web installeren we: react-router-dom
  • core (react-router) ook vereist als dependency (zelf te installeren!)

React Router: achter de schermen

  • vangt wijzigingen van locatie (bv. in adresbalk) op
  • kijkt of url gekend is
  • toon, indien van toepasssing, de juiste pagina

Voorbeeldapplicatie

  • klassieke webapplicatie met pagina's:
    • Home (/)
    • Over ons (/over)
    • Contact (/contact)
    • 404 Not Found (alle andere)

~$ 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 component is nodig voor routing in webapplicaties. De BrowserRouter component maakt een context aan waardoor we verschillende routes kunnen definiëren én gebruik kunnen maken van geschiedenis. Plaats deze component rond de App component.

BrowserRouter

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.

HashRouter

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.

Routes definiëren

> 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>
                        );
                    };
                     
We gebruiken de LoremIpsum component van het package react-lorem-ipsum om wat dummy tekst te maken. Alle pagina's komen in dit ene bestand, normaal maak je één jsx-bestand per component. We maken 3 componenten: Home, About en Contact, één voor elke pagina. En ook een NotFound voor het geval een URL niet gekend is.

Routes definiëren

> 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;
                     
Importeer Switch en Route van react-router-dom. Importeer de 3 componenten. De Switch component zorgt ervoor dat hoogstens één route tegelijk actief is (eerste die matcht). Routes worden gedefinieerd als children van deze component. Je kan de component meegeven als prop. Nadeel 1: je kan geen props meegeven aan de component. Nadeel 2: maakt een nieuwe component per render indien je een inline functie zou doorgeven, bv. () => <Home /> Je kan ook een functie meegeven die de component aanmaakt. Voordeel: eenvoudig om props door te geven zonder nieuwe componenten te creëeren bij elke render Nadeel: volgt niet de conventie van children. Bij de derde (én beste) mogelijkheid plaatst de component als child van de Route component. Voordeel: rendert de children ook indien de route niet matcht, dus vergeet zeker exact niet indien nodig! exact geeft aan dat de url identiek overeen moet komen. Zo niet, wordt er niets getoond.
> 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>
                        );
                     
We importeren de Link component van react-router-dom. Gebruik deze component om links te maken binnenin de webapplicatie. Voor externe links gebruik je nog steeds <a>...</a>. Via to geef je aan naar waar de link wijst. Dit zal getoond worden als link.

Route properties

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>
                            );
                        };
                     
Haal het location object op. pathname bevat de huidige url. Dit object bevat nog andere properties: key, hash, search en state.

useLocation

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:

  • key: een uniek identificatie voor deze locatie
  • pathname: de huidige URL
  • search: de query parameters uit de URL (niet te verwarren met URL parameters!) (optioneel)
  • hash: id van een element uit de DOM tree, hiermee zal de browser standaard scrollen naar het element met dit id (optioneel)
  • state: dit kan je zelf invullen, niet vooraf bepaald

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.

Route properties

> src/App.jsx 
                        // ...
                        import { Home, About, Contact, NotFound } from './pages';


                        function App() {
                            return (
                                <Switch>
                                    {/* ... */}
                                    <Route path="*">
                                        <NotFound />
                                    </Route>
                                </Switch>
                            );
                        }

                        // ...
                    
Een path="*" zal alle niet gematchte routes opvangen (i.e. 404 Not Found). Merk op: geen exact hier! Je kan ook een reguliere expressie (als string) meegeven aan path. Merk op: deze route moet als laatste staan, anders matcht elke URL hiermee en zien we de juiste component niet

Nesting routes

> 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>
                        );

                        // ...
                     
Merk op: de About component wordt niet meer geëxporteerd. Drie extra routes die allemaal starten met de prefix /over. Oplossing: nesting van routes in een "module-component".

Nesting routes

> 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>
                            );
                        };

                        // ...
                     
Definieer en exporteer een module-component voor alle pagina's onder de url /over. React Router heeft geen notie van welke routes reeds gedefinieerd werden in componenten hoger in de tree. Vraag hier dus naar de specifieke route die gematched werd om deze component te tonen. path zal in dit geval /over bevatten. Definieer een nieuwe Switch, we willen maar één route per keer matchen. Dit is nog steeds de About component van hiervoor. Enige verschil is het dynamisch inladen van het path. Er vallen nog 3 extra routes onder /over. Elk path wordt dynamisch voorzien van de prefix /over. Als de prefix op het niveau hoger gewijzigd wordt, dan gebruikt deze component meteen de nieuwe prefix.

Nesting routes

> 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;
                     
Dit is hoe de route /over vroeger gedefinieerd werd. Plaats de nieuwe module-component onder dezelfde route. Merk op: geen exact hier! Anders zou enkel /over zonder suffix werken.

Extra componenten

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>
                );
                

Redirects

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;
                     
Daarvoor kan de Redirect component gebruikt worden. from zegt welke url doorgestuurd moet worden. to zegt naar waar doorgestuurd wordt.

URL parameters

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>
                        );
                    }

                    // ... 
Een extra route om een lijst van producten te tonen. Met een : kan je parameters in de URL aanduiden. Een parameter kan bijvoorbeeld het id van een product zijn. Deze route zal de details van één product tonen.

Nieuwe Products component

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>
                    );
                

Details van een product

> 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>
                            )
                        };
                     
Met useParams kan je URL parameters opvragen. Dit retourneert een object met alle URL parameters. Deze hebben dezelfde naam als opgegeven in de URL. Hier wordt object destructuring gebruikt. Alle parameters worden als string geretourneerd, het id moet in dit geval omgezet worden naar een number. Vervolgens wordt gezocht naar een product met hetzelfde id. Hier wordt array destructuring gebruikt. Indien er geen product gevonden werd, wordt dit getoond. Indien er wel een product bestaat, wordt dit getoond.

useParams

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.

Voorbeeld

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'
                }
                

Scroll restoration

  • Bij routing in SPA's wordt de scroll-positie niet automatisch hersteld naar linksboven in de browser.
  • Indien gewenst, moet hier zelf voor gezorgd worden met de useEffect hook.
> 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;
                    }
                     
Haal de huidige url op. Elke keer die url wijzigt, vraag de browser om naar boven te scrollen. Deze component hoeft niks te tonen.

Scroll restoration: smooth scroll

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.

Scroll restoration

> src/App.jsx 
                    // ...
                    import ScrollToTop from './ScrollToTop';

                    function App() {
                        return (
                            <>
                                <ScrollToTop />
                                <Switch>
                                    { /* ... */ }
                                </Switch>
                            </>
                        );
                    }
Importeer de nieuwe component en plaats hem in de App component. Switch is niet meer het enige child van de App component: wrappen in een React.Element. Herinner je: <> is syntactic sugar voor <React.Element>.
  • soms wil je navigeren vanuit code
  • daarvoor bestaat de useHistory hook
  • deze geeft een history object terug
  • dit object bevat de geschiedenis en werkt met een stack
  • enkele nuttige methoden:
    • push: push een nieuwe route op de stack
    • replace: vervang de huidige route op de stack

Navigeren vanuit code

> 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>
                        );
                    }
Haal het history object op. Maak een knop om terug naar de home-pagina te gaan. Definieer een callback die de url / op de history stack pusht. Indien hier push door replace vervangen wordt, kan niet teruggekeerd worden naar de vorige pagina.

History

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:

  • length: aantal items op de stack
  • location: de huidige locatie (zoals uit de useLocation hook)
  • push(path, [state]): plaats een nieuw item op de stack (optioneel kan state bijgehouden worden bij het item)
  • replace(path, [state]): vervang het huidige item op de stack (optioneel kan state bijgehouden worden bij het item)
  • go(n): ga naar item n op de stack
  • goBack(): ga één item terug, identiek aan go(-1)
  • goForward(): ga één item vooruit, identiek aan go(1)

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.

  • push kan je gebruiken als de gebruiker terug mag keren
  • replace kan je gebruiken als de gebruiker niet terug mag keren naar de ulr

Oefening

  • voorzie routing in de budget-applicatie:
    • /: doorsturen naar /transactions
    • /transactions: een lijst van transacties (Transactions component)
    • /places: een lijst van places (Places component)
    • /transactions/add: een nieuwe transactie toevoegen (TransactionForm component)

Oefening: extra

  • voorzie routing in de budget-applicatie:
    • /transactions/edit/:id: een transactie bewerken (TransactionForm component)
    • /transactions/:id: details van één transactie (nieuwe component nodig)
    • /places/:id: details van één place (nieuwe component nodig)

Oplossing

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