Routers en Paginas

Tot nu toe hebben we altijd gewerkt met 1 pagina, maar uiteraard bestaan de meeste web applicaties uit verschillende pagina's. Je hebt bijvoorbeeld een lijst pagina waar de data in een lijst wordt getoond, als je dan doorklikt wil je naar een detail pagina gaan. Je wil ook verschillende pagina's gaan aanbieden op andere url's. Zo zal een lijst bijvoorbeeld op het pad /list staan en de detail op /detail/{id}. Om dit te doen gaan we gebruik maken van een externe library uit de npm registry: react-router-dom

Lifting state up

Het eerste wat we gaan doen is onze games list pagina in een apart component plaatsen, zodat deze code niet meer in de App component staat. Deze component krijgt later de verantwoordelijkheid voor het tonen van de verschillende pagina's, niet alleen de List pagina. We maken een nieuwe directory pages en maken daar een nieuwe directory ListPage aan. We kopieren de inhoud van het App.tsx bestand naar een nieuw bestand genaamd ListPage.tsx en doen hetzelfde met de CSS-module van dit component. Vergeet niet de naam van het component aan te passen de default export.

Momenteel zit de lijst van games nog in het ListPage component. We willen dit component beschikbaar maken voor alle pagina's dus we gaan de state een niveau hoger plaatsen. Wat wil zeggen dat we deze gaan terug plaatsen in het App component. We moeten er voor zorgen dat de ListPage de games list via props laten binnen krijgen. We definiëren de volgende props voor de ListPage

interface ListPageProps {
  games: Game[],
  handleAdd: (game: Game) => void
}

Je merkt op dat we hier ook de handleAdd callback handler hebben opgegeven als property van onze ListPage. De handleAdd callback handler die hier wordt meegegeven wordt gewoon doorgeven aan het InputView component.

import InputView from 'components/InputView/InputView';
import List from 'components/List/List';
import React from 'react';
import { Game } from 'types';
import styles from './ListPage.module.css';

interface ListPageProps {
  games: Game[],
  handleAdd: (game: Game) => void
}

const ListPage = ({games,handleAdd} : ListPageProps) => {
  return (
    <div>
      <List games={games} />
      <InputView onAdd={handleAdd} />
    </div>
  );
}

export default ListPage;

En het App component zal er als volgt uit zien.

import ListPage from 'pages/ListPage/ListPage';
import Header from 'components/Header/Header';
import React, { useState } from 'react';
import { Game } from 'types';
import styles from './App.module.css';

const App = () => {
  const [games, setGames] = useState<Game[]>([
    ...
  ]);

  const handleAdd = (game: Game) => {
    let max = Math.max(...games.map((g) => g.id!),0) + 1;
    game.id = max;
    setGames([...games, game])
  }

  return (
    <div>
      <Header />
      <div className={styles.container}>
        <ListPage games={games} handleAdd={handleAdd}/>
      </div>
    </div>
  );
}

export default App;

Merk op dat we de Header component in de App component hebben gelaten. We willen de header op elke pagina plaatsen dus we zetten deze dan op dit niveau.

Nu hebben we er voor gezorgd dat we een aparte pagina hebben voor onze lijst van games en dat het App component verantwoordelijk is voor de lijst van games te beheren (de state bevind zich dus op het hoogste component)

React Router

Nu we een aparte pagina hebben voor de lijst gaan we nu de library installeren die het mogelijk maakt om eenvoudig nieuwe pagina's op verschillende routes te introduceren. We installeren react-router met het volgende commando:

npm install --save react-router-dom@5
npm install --save-dev @types/react-router-dom@5

Ondertussen is react-router v6 uitgebracht. Dit betekent dat deze instructies niet allemaal nog gelden voor de laatste nieuwe versie. Daarom kan je best v5 installeren.

Omdat we momenteel maar pagina hebben maken we een heel eenvoudige HomePage.

import styles from './HomePage.module.css';

const HomePage = () => {

  return <p className={styles.welcomeText}>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. In hendrerit mi nec elit congue, et fringilla tellus pharetra. Cras commodo, est quis hendrerit porta, ligula arcu egestas arcu, vitae volutpat ex libero tristique augue. Sed pellentesque urna nec eros pellentesque, quis interdum tellus convallis. Cras maximus fringilla magna, a viverra nulla malesuada eget. Integer erat eros, eleifend a pulvinar sed, aliquam sit amet quam. Pellentesque bibendum tortor eget faucibus varius. Nunc dictum est enim, ac suscipit quam ultricies mattis. Sed ut quam in elit rhoncus accumsan eget sed nibh. Donec tincidunt, nulla quis viverra mattis, quam arcu posuere sapien, sit amet luctus magna nisl quis nisl. Nulla scelerisque dapibus interdum. Nulla ultrices non erat vitae consequat. Donec sit amet est quis tellus auctor rutrum ut vel felis. Cras egestas euismod vestibulum. Ut aliquam porttitor nisi, in gravida justo interdum quis. Vivamus quis lectus ac metus sollicitudin vulputate in nec diam. Proin vel cursus nunc.
    </p>
}

export default HomePage;

De CSS style van de welcomeText mag je zelf kiezen. Zorg ervoor dat beide bestanden in een src/Pages/HomePage directory zitten.

Nu gaan we onze App component aanpassen zodat het de React router library gebruikt om de routes te vast te leggen en te kiezen welke page moet getoond worden bij welke route.

import ListPage from 'pages/ListPage/ListPage';
import React, { useState } from 'react';
import { Game } from 'types';
import styles from './App.module.css';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import HomePage from 'pages/HomePage/HomePage';
import Header from 'components/Header/Header';

const App = () => {
  ...
  return (
    <Router>
      <div>
        <Header />
        <div className={styles.container}>
          <Switch>
            <Route path="/list">
              <ListPage games={games} handleAdd={handleAdd} />
            </Route>
            <Route path="/" exact>
              <HomePage />
            </Route>
          </Switch>

        </div>
      </div>
    </Router>
  );
}

export default App;

Als je ergens React Router wil gebruiken dan moet het parent component altijd een Router component zijn.

Er zijn een aantal varianten op deze Router component zoals BrowserRouter en HashRouter. Deze hebben invloed over hoe de urls die worden gegenereerd voor de paden zullen getoond worden. We bekijken deze niet in detail, maar deze zijn belangrijk als je uiteindelijk je applicatie op een externe webserver wil laten lopen.

Route en Switch noemen we route matching componenten. Als een Switch wordt gerendered, wordt er in zijn kind elementen gezocht achter Route elementen en wordt er gezocht achter het pad dat overeenkomt met de huidige URL. Als het er een vindt, dan rendered hij die Route en negeert hij alle anderen. Dit betekent dat je altijd de meest specifieke (vaak de langste) eerst moet plaatsen voor de minder specifieke routes.

In ons geval hebben we nu twee routes. De route voor de lijst op /list en de route voor / die de HomePage zal laten zien aan de gebruiker. In de Route componenten plaatsen we hier de componenten die hij zal moeten tonen als naar die route gegaan wordt in je browser.

React Router biedt een <Link> component aan om links te maken in je applicatie. Overal waar je een Link rendered wordt een anchor (<a>) tag gerendered in je HTML document.

<Link to="/">Home</Link>
// <a href="/">Home</a>

Je hebt ook nog een speciale versie van de <Link> component die zichzelf als 'actief' kan renderen als de to prop overeenkomt met de huidige locatie. Als je de exacte locatie wilt matchen moet je hierbij ook nog exact als prop meegeven.

Zo kunnen we nu een navigatie balk aanmaken in onze header component:

import React from "react";
import { NavLink } from "react-router-dom";
import styles from "./Header.module.css"

const Header = () => {
  return (
    <div className={styles.header}>
        <div className={styles.headerName}>H2O Store</div>
        <ul>
          <li><NavLink exact to="/" activeClassName={styles.activeLink}>Home</NavLink></li>
          <li><NavLink exact to="/list" activeClassName={styles.activeLink}>Games</NavLink></li>
        </ul>
    </div>
  );
}

export default Header;
.header { 
  background-color: #333;
  font-size: 12pt;
  flex-direction: row;
  display: flex;
}

.headerName {
  display: block;
  color: white;
  text-align: center;
  padding: 14px 16px;
  text-decoration: none;
}

.activeLink {
  font-weight: bolder;
}

ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

li {
  float: left;
}

li a {
  display: block;
  color: white;
  text-align: center;
  padding: 14px 16px;
  text-decoration: none;
}

li a:hover {
  background-color: #111;
}

URL Parameters

Tot nu toe hebben we altijd routes gebruikt die exacte paden voorstellen. Soms wil je ook aan de hand van de url bepaalde parameters gaan meegeven. Zo willen wij in ons voorbeeld een pad aanmaken waarmee je het detail van een game kan bekijken. We zouden hier graag een url zoals /detail/:id toelaten waarbij de :id dus een parameter is. Deze kan bijvoorbeeld /detail/1 of /detail/2 zijn en hiervoor moet altijd hetzelfde component gerendered worden.

Laten we eerst een eenvoudige DetailPage maken voor deze route:

import styles from './DetailPage.module.css';

interface DetailPageProps {
  games: Game[]
}

const DetailPage = ({games} : DetailPageProps) => {
  return (
    <div>Detail Page</div>
  )
}

export default DetailPage;

We moeten dan deze DetailPage toevoegen aan onze Router

<Switch>
  <Route path="/list">
    <ListPage games={games} handleAdd={handleAdd} />
  </Route>
  <Route path="/" exact>
    <HomePage />
  </Route>
  <Route path="/detail/:id">
    <DetailPage games={games}/>
  </Route>
</Switch>

Een parameter in een route wordt aangeven met de naam van de parameter en een : ervoor. In dit geval is dit /detail/:id waarvan id de naam van de parameter is.

Om deze parameters uit te kunnen lezen in de DetailPage kunnen we de useParams hook gebruiken om deze parameters te kunnen uitlezen.

import { useParams } from 'react-router-dom';
import { Game } from 'types';
import styles from './DetailPage.module.css';

interface ParamTypes {
  id: string
}

interface DetailPageProps {
  games: Game[]
}

const DetailPage = ({games} : DetailPageProps) => {
  let { id } = useParams<ParamTypes>();

  let game = games.find((game: Game) => game.id === parseInt(id));

  return (
    <div>
      <div>{game?.name}</div>
      <div>{game?.releaseYear}</div>
      <div>{game?.sales}</div>
    </div>
  )
}

export default DetailPage;

We kunnen ons ListItem component aanpassen aan de hand van het Link component zodat als we op het de game klikken dat we dan naar de detailpagina verwezen worden.

const ListItem = ({ game }: ListItemProps) => {
  return (
    <Link to={`/detail/${game.id}`}>
      <div key={game.id} className={styles.gameCard}>
        <div>{game.name}</div>
        <div>{game.releaseYear}</div>
        <div>{game.sales}</div>
      </div>
    </Link>
  )
}

Last updated

Was this helpful?