arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 26

[G_PRO] Web Frameworks

Loading...

Loading...

Loading...

Fundamenten

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Extra

Loading...

Opdrachten

OPDRACHTEN

Loading...

Loading...

Loading...

Loading...

Loading...

Componenten

Componenten zijn de bouwblokken van elke React applicatie. Tot nu toe hebben we altijd gebruik gemaakt van 1 component: de App component. We zouden kunnen blijven werken met 1 component en daar alles in plaatsen, maar zoals alle software systemen is dit geen goed idee. We willen de applicatie opsplitsen in kleine herbruikbare blokjes. Deze herbruikbare blokjes noemen we in react: Componenten.

We gaan even terug naar onze lijst applicatie kijken. We gaan hier onze eerste opsplitsing maken in een nieuw component: de List component.

import React from 'react';

const List = () => {
  return (
      <div>{games.map((game: Game) => {
        return <div key={game.id}>
                  <div>{game.name}</div>
                  <div>{game.releaseYear}</div>
                  <div>{game.sales}</div>                  
               </div>
      })}
      </div>
  );
}

Je ziet hier dat we gewoon terug een nieuwe functie aanmaken die een stuk van de JSX-code teruggeeft als return waarde. We hebben deze gewoon verplaatst van het App component naar de List component. In react noemen we een functie die JSX-code teruggeeft een component.

Omdat we deze code hebben verplaatst naar de nieuwe component moeten we ook nog aangeven in onze Appcomponent dat we deze component willen tonen. Dit doen we aan de hand van de <ComponentName/> syntax. Je merkt hier op dat we hier blijven werken met de HTML-achtige syntax. Het lijkt een beetje op een eigen HTML tag hebben aangemaakt.

Heel gelijkaardig kunnen we nu ook de header als een eigen component gaan maken als we dit willen.

en dan passen we de App component verder aan zodat deze ook de Header component zal gebruiken.

In javascript zijn er meerdere manieren om een functie aan te maken. Als je door het internet zoekt naar voorbeeld code zal je ongetwijfeld ook de function syntax tegen komen. Deze kan in ons geval meestal exact gebruikt worden als vervanging van de arrow notatie. Zo kan het Header Component ook geschreven worden als

Lijsten

hashtag
Lijsten in React

Tot nu toe hebben we met JSX telkens maar 1 ding per keer getoond. Vaak wordt er gebruik gemaakt van arrays en deze kunnen we niet uitdrukken in 1 HTML-element. We willen dus een HTML-element renderen per element van de array. Dit gaan we doen aan de hand van de map functie die elk element van de array zal mappen naar een HTML-element.

We plaatsen nu een array bovenaan ons App.tsx bestand:

Eerste React applicatie

hashtag
Create React App

In deze cursus gebruiken we de create-react-app om onze eerste applicatie op te zetten. Het is een starter kit aangeboden door Facebook om snel een React applicatie aan te maken. Het vereist geen configuratie en wordt aangeraden voor beginnende React gebruikers.

Voordat we kunnen werken met de create-react-app kijk je best ook nog na of je npx

const App = () => {
  return (
    <div>
      <h1>Welcome to the H2O Game shop</h1>
      <List/>
    </div>
  );
}
const Header = () => {
  return (
    <h1>Welcome to the H2O Game shop</h1>
  );
}
Dit is een eenvoudige array van strings met 3 computer spelletjes in.

De map functie zet een lijst van elementen om naar een lijst van nieuwe elementen. Hier zetten we eigenlijk de lijst van strings om naar een lijst van <div> elementen met daarin de naam van de persoon van de array.

Omdat React gebruik maakt van geoptimaliseerde technieken voor het tonen van lijsten moet je altijd bij de elementen in de map constructie een property key plaatsen zodat deze weten of een element moet gerenderd worden als de array wijzigt. Omdat we hier zelf geen ids hebben voor de personen gebruiken we hier even de naam zelf als key.

We kunnen hier ook gebruik maken van arrays van objecten. Zoals hieronder wordt getoond, als het object dan een unieke ID heeft kan je deze gebruiken voor de key.

en dan kan je deze array ook tonen in je JSX code als volgt:

circle-info

Tot nu toe zijn onze elementen nog geen hoogvliegers op vlak van styling. Hier zullen we in een verder deel zeker verbeteren bij het toevoegen van stylesheets.

let games : string[] = [
  "World of Warcraft",
  "Valheim",
  "Minecraft"
]
const App = () => {
  return (
    <div>
      <Header/>
      <List/>
    </div>
  );
}
function Header() {
  return (
    <h1>Welcome to the H2O Game shop</h1>
  );
}
const App = () => {
  return (
    <div>
      <h1>Welcome to the H2O Game shop</h1>
      <div>{games.map((game: string) => {
        return <div key={game}>{game}</div>
      })}</div>
    </div>
  );
}
const App = () => {
  return (
    <div>
      <h1>Welcome to the H2O Game shop</h1>
      <div>{games.map((game: string) => {
        return <div key={game}>{game}</div>
      })}</div>
    </div>
  );
}
interface Game {
  id?: number
  name: string,
  releaseYear: number,
  sales: number
}

let games: Game[] = [
  {
    id: 0,
    name: "World of Warcraft",
    releaseYear: 2004,
    sales: 0
  },
  {
    id: 1,
    name: "Valheim",
    releaseYear: 2021,
    sales: 0
  },
  {
    id: 2,
    name: "Minecraft",
    releaseYear: 2011,
    sales: 0
  }
]
const App = () => {
  return (
    <div>
      <h1>Welcome to the H2O Game shop</h1>
      <div>{games.map((game: Game) => {
        return <div key={game.id}>
                  <div>{game.name}</div>
                  <div>{game.releaseYear}</div>
                  <div>{game.sales}</div>                  
               </div>
      })}</div>
    </div>
  );
}
hebt geinstalleerd op jouw machine. Normaal wordt deze mee geinstalleerd met
node
en
npm

Je kan dit nakijken door

te doen.

npx laat toe om tooling te gebruiken zonder die eerst te installeren op jouw machine. Zo heb je altijd de laatste nieuwe versie van deze tool. Je zou eventueel deze ook kunnen installeren op jouw machine met npm install -g maar dan zou je zelf verantwoordelijk zijn voor het updaten van deze tool.

Nu we dit klaar hebben staan kunnen we eindelijk onze eerste react applicatie aanmaken door

uit te voeren.

triangle-exclamation

Let op dat je het stuk --template typescript niet vergeet. We werken in deze cursus met typescript. Anders wordt er een react applicatie gemaakt in javascript.

We kunnen nu deze folder open doen in Visual Studio Code en eens naar de inhoud van het project kijken.

Hier merk je de volgende bestanden en folders in op:

  • node_modules deze folder bevat alle packages die werden geïnstalleerd bij het runnen van het create-react-app commando. Alle packages die je zelf installeert met npm install zullen ook in deze folder terecht komen.

  • package.json beschrijft welke dependencies (packages) er moeten geïnstalleerd worden voor deze applicatie uit te kunnen voeren. Het npm install commando zal deze dependencies dan installeren in de node_modules folder.

  • .gitignore deze file bevat alle bestanden en folders die niet mogen toegevoegd worden aan je git repository. node_modules is hiervan een goed voorbeeld. Deze folder moet nooit mee in git geplaatst worden want deze folder met alle packages wordt aangemaakt door npm install uit te voeren.

  • src bevat de eigenlijke bestanden die rechtstreeks te maken hebben met react. Hier zal jouw broncode in komen. Het belangrijkste bestand op dit moment is het App.tsx bestand. Daar zullen we eerst onze eerste react code schrijven. Later zullen we dit uiteraard opsplitsen in verschillende bestanden.

  • public bevat het html bestand waarin de react applicatie zal getoond worden. Hier moet je meestal niets voor aanpassen want alle visuele componenten worden in react zelf gerendered. Deze folder bevat ook andere dingen zoals de favicon voor als je het icoontje wil veranderen van de web applicatie.

Je kan nu de applicatie opstarten door

uit te voeren in je terminal venster. Als alles in orde is zal je browser automatisch openen op de eerste en enige pagina van jouw webapplicatie:

De reden dat je npm start kan uitvoeren en dat al deze acties dan automatisch gebeuren hebben we te danken aan het volgende stuk in het package.json bestand:

We gaan voorlopig nog niet verder op deze commando's in.

hashtag
Je eerste component

Alle React applicaties zijn gebouwd uit verschillende React componenten. Componenten worden uitgedrukt in functies. Ze accepteren willekeurige invoerwaarden (deze noemen we “props”) en geven React elementen terug die beschrijven wat er op het scherm moet verschijnen. Met componenten splits je de UI in onafhankelijke, herbruikbare delen.

Het eerste component waar je altijd mee in aanraking zal komen kan je vinden in het src/App.tsx bestand. Deze zal er een beetje zoals het onderstaand bestand uitzien:

Tijdens het genereren van de code via de create-react-app is er gebruik gemaakt van het function keyword. Een component kan ook evengoed geschreven worden met de arrow notatie voor functies. Je mag zelf kiezen welke notatie je gebruikt. Een component met de arrow notatie zal er als volgt uit zien:

We zullen nu even het component herleiden tot de essentie door wat overbodige boilerplate code weg te halen. Zo bekomen we tot een heel eenvoudige Hello world applicatie:

We zien hier een aantal dingen:

  • We zien hier eigenlijk gewoon een eenvoudige functie. We noemen deze in React geen functie maar een function component.

  • Deze function component heeft tot nu toe geen parameters maar in verdere delen zullen we zien dat we ook parameters kunnen meegeven aan componenten aan de hand van properties (of props).

  • Deze functie returned een HTML achtige syntax terug. Deze HTML achtige syntax noemen we JSX en zal het mogelijk maken om JavaScript en HTML eenvoudig te combineren.

Uiteraard kunnen we in een functional component ook meer doen dan HTML renderen. Boven de return kunnen we variabelen declareren en complexere logica gaan toevoegen aan onze componenten.

Willen we nu deze title gaan gebruiken in onze HTML achtige syntax gaan we gebruik maken van JSX. Zo kunnen we de output gaan mengen met javascript code. Als we de title variabele willen renderen tussen de h1 tags kunnen we deze als volgt toevoegen

Je kan naast variabelen ook eender welke expressie tussen deze {} haakjes plaatsen. Bijvoorbeeld:

of

en je kan zelfs functies uitvoeren en het resultaat hiervan laten zien:

hashtag

npx --version
npx create-react-app our-first-react-app --template typescript
npm start
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
import React from 'react';
import logo from './logo.svg';
import './App.css';


function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
const App = () => {
    return {
        ...
    }
}
import React from 'react';

const App = () => {
  return (
    <div>
      <h1>Hello World</h1>
    </div>
  );
}

export default App;
import React from 'react';

const App = () => {
  const title = "World";

  return (
    <div>
      <h1>Hello World</h1>
    </div>
  );
}

export default App;
import React from 'react';

const App = () => {
  const title = "World";

  return (
    <div>
      <h1>Hello {title}</h1>
    </div>
  );
}

export default App;
<h1>Hello {title.toUpperCase()}</h1>
<h1>1+1 = {1+1}</h1>
import React from 'react';

const getGreeting = () => {
  return "Hello World";
}

const App = () => {
  return (
    <div>
      <h1>{getGreeting()}</h1>
    </div>
  );
}

export default App;

Initial page

Single Page Applications

Zoals de cursusnaam zegt gaan we in dit opleidingsonderdeel dieper ingaan op web frameworks. We gaan voornamelijk kijken naar het meest gebruikte Single Page Applications framework namelijk React.js (door facebook). Er zijn nog vele andere SPA-frameworks zoals Angular (door Google) en Vue.js. Deze frameworks laten het toe om eenvoudig uitgebreidere applicaties te maken dan met gewone JavaScript en jQuery.

Voordat we verder gaan met SPAs, gaan we even terug in de tijd voordat SPAs bestonden. In het verleden werden pagina's nog volledig opgebouwd op de server zelf. Zo had je bijvoorbeeld PHP of node.js in combinatie met Express en een view framework (zie webontwikkeling). In deze traditionele aanpak bezocht de gebruiker een URL om een pagina in te laden en werd het opbouwen van de pagina voornamelijk op de server zelf gedaan. Elke nieuwe pagina die de gebruiker wou bezoeken had dus ook een nieuwe GET request nodig. JavaScript werd toen voornamelijk gebruikt voor simpele animaties en interacties of geavanceerde styling die niet met CSS alleen te doen was. Hiervoor was de JavaScript library jQuery uiterst geschikt.

Bij single page applicaties gebeurt dit op een hele andere manier. De focus wordt verlegd van de server naar de client. De gebruiker bezoekt maar 1 URL en krijgt maar 1 HTML-bestand terug met bijbehorende JavaScript files. Nadat deze allemaal ingeladen zijn wordt de pagina getoond aan de gebruiker en worden alle andere pagina's volledig opgebouwd vanuit JavaScript. Alles voor het tonen van pagina's zit dus volledig vervat in die gedownloade JavaScript bestanden. Uiteraard hebben deze pagina's nog externe data nodig. Deze worden volledig via API calls (via AJAX) vanuit de client naar een server uitgevoerd. Als je bijvoorbeeld een lijst van boeken zou willen tonen vanuit je SPA, zal deze een API call doen naar de boeken API en zal het renderen (opbouwen) van de pagina volledig in je browser verlopen via JavaScript.

Tools

hashtag
Editor en Terminal

Voor deze cursus zullen we Visual Studio Code gebruiken als editor. Dit is een volledig geïntegreerde editor met ingebouwde terminal. Dit is ook de meest populaire keuze bij web ontwikkelaars. Zorg ervoor dat je de laatste nieuwe versie van vscode hebt.

Daarnaast is het ook de bedoeling dat je git hebt geïnstalleerd en git bash hebt ingesteld als default terminal in visual studio code. Op een mac of linux machine is dit automatisch bash dus je hoeft hier niets te veranderen.

circle-info

Heb je Windows Subsystem Linux (WSL) geïnstalleerd mag je uiteraard deze ook gebruiken als default bash op Windows 10

hashtag
Node en NPM

Voordat we kunnen beginnen moeten we ook Node en NPM hebben geïnstalleerd op onze machine. We gebruiken deze om packages te installeren en onze applicatie te kunnen uitvoeren. Je kan nakijken welke versie van node je hebt geinstalleerd door het commando

en

uit te voeren.

Zorg ervoor dat je alijd de laatste nieuwe LTS versie hebt geinstalleerd van Node.

node --version

State

State en Props zijn een van de meest essentiële concepten die je moet begrijpen in React. Props dienen om informatie door een componenten structuur te geven, en state wordt gebruikt om applicaties interactief te maken. State wordt gebruikt om informatie bij te houden en deze aan te passen over de looptijd van je applicatie.

We zullen dat laatste demonstreren aan de hand van een voorbeeld. We gaan hiervoor terug naar ons InputView voorbeeld. Stel dat we elke keer de gebruiker iets intypt in de input box, dat we deze text willen laten tonen ergens anders in de applicatie. Dit is dus informatie die aangepast wordt over de looptijd van de applicatie.

const InputView = () => {
  const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
    console.log(event.target.value);
  }
  
  ...

  return (
    <div>
      <input type="text" id="name" onChange={handleChange} />
    </div>
  )
}

We zouden foutief kunnen veronderstellen dat we dit probleem kunnen oplossen door een variabele te maken waar we de tekst van de input in kunnen opslagen. Dit zou er ongeveer als volgt kunnen uitzien.

Als je dit zou uitproberen dan ga je merken dat er niets gebeurt als je het input veld aanpast. Dit is omdat het rendering mechanisme hier niet wordt getriggerd. Dit wil zeggen dat het component niet opnieuw getekend zal worden, en dus de naam niet aangepast zal worden op je scherm.

Hoe lossen we het dan wel correct op? De oplossing hierboven is in principe niet ver van de juiste oplossing. We gaan hier in plaats van een variabele, een state aanmaken waar we de name in kunnen opslagen. Als deze state aangepast wordt zal het component wel opnieuw getekend worden (gerendered). We gaan hier gebruik maken van de useState hook om deze state aan te maken.

De useState functie heeft als argument een initiële state. Dit is de start waarde die de state zal krijgen als het component voor de eerste keer gerendered wordt. De functie geeft een array terug met twee elementen in: het eerste element is de huidige state en het tweede element is een functie waarmee je de state kan aanpassen. We geven ook aan welk type onze state zal bevatten door <string> mee te geven aan de useState functie.

Als we nu ons voorbeeld weer aanpassen komen we op de volgende code

Als de gebruiker nu in het invoerveld iets ingeeft zal de handleEvent functie aangeroepen worden. Daarin wordt de setName methode opgeroepen die de waarde van de name state aanpast. Nadat deze is aangepast wordt het component terug gerendered. Dit wil zeggen dat de code van dit component terug uitgevoerd wordt, maar op dat moment zal de state met de name de nieuwe waarde bevatten en dus zal dit ook getoond worden aan de gebruiker.

Als je graag wil weten wanneer je component terug opnieuw wordt gerendered kan je in de code van de InputView component een console.log() statement zetten die afprint dat het component is gerenderd. Zo kan je in je chrome developer tools zien op welk moment het component terug opnieuw gerenderd wordt.

Voor de volledigheid passen we nu ook nog heel de input component aan zodat alle inputvelden event handlers hebben

We hebben hier er ook voor gezorgd dat alle invoer velden nu ook de value attribuut gebruiken. Deze wordt gezet op de huidige waarde van de states. Zo zorgen we ervoor dat het inputveld altijd up-to-date is met de huidige waarde van de state. Dit noemen ze in react controlled components.

npm --version
// Opgepast: Deze code is FOUT! Doe dit nooit op deze manier
const InputView = () => {
  let name = ""; // Dit is fout!

  const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
    name = event.target.value; // Dit is fout!
  }

  return (
    <div>
      <input type="text" id="name" onChange={handleChange} />
      <p>
        The name you typed is {name}
      </p>
    </div>
  )
}
const [name, setName] = useState<string>('');
const InputView = () => {
  const [name, setName] = useState<string>('');

  const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
    setName(event.target.value);
  }

  return (
    <div>
      <input type="text" id="name" onChange={handleChange} />
      <p>
        The name you typed is {name}
      </p>
    </div>
  )
}
const InputView = () => {
  const [name, setName] = useState<string>('');
  const [year, setYear] = useState<number>(1990);

  const handleNameChange: ChangeEventHandler<HTMLInputElement> = (event) => {
    setName(event.target.value);
  }

  const handleYearChange: ChangeEventHandler<HTMLInputElement> = (event) => {
    setYear(parseInt(event.target.value));
  }

  const handleClick: MouseEventHandler<HTMLInputElement> = (event) => {
    // does not do anything yet
  }

  return (
    <div>
      <input type="text" id="name" onChange={handleNameChange} value={name}/>
      <input type="number" id="year" name="year" min="1990" max="2021" onChange={handleYearChange} value={year}/>
      <input type="button" id="submit" onClick={handleClick} value="Add" />
      <p>
        You have typed {name} from the year {year}
      </p>
    </div>
  )
}

Callback handlers

Tot nu hebben we props en state gezien om informatie bij te houden en door te geven tussen verschillende componenten. Maar we missen nog 1 belangrijk concept om communicatie tussen componenten te laten gebeuren. Op dit moment kunnen we onze informatie invoeren in onze invoer velden in de InputView⁣, maar maar kunnen we nog op geen enkele manier deze informatie doorgeven aan het App component die onze lijst bevat.

Props kunnen alleen informatie naar beneden doorgeven. Dus we moeten zelf iets maken dat dit wel mogelijk maakt. Dit noemen we een callback handler (A). Deze geven we door aan onze component aan de hand van props (B)

const App = () => {
  ...
  // A
  const handleAdd = (game: Game) => {
    console.log(game);
  }

  return (
    <div>
      <Header />
      <List games={games} />
      {/* // B */}
      <InputView onAdd={handleAdd}/>
    </div>
  );
}

Voordat we deze callback functie kunnen gebruiken in onze InputView moeten we deze toevoegen aan de properties van dat component, dus we maken weer een interface voor dit component.

Vervolgens voegen we deze properties toe aan ons InputView component zelf.

Nu kunnen we in onze handler van de button click deze onAdd callback handler oproepen. We maken hier een Game object aan gebruik makende van de waarden die in de state zijn opgeslagen. Als de gebruiker nu op de add knop klikt dan zal de onAdd callback handler aangeroepen worden die werd meegegeven uit het App component.

Om nu onze applicatie volledig functioneel te maken willen we nu er voor zorgen dat het game in kwestie wordt toegevoegd aan de lijst die zich bevindt in het App component. Momenteel wordt de lijst van games nog bijgehouden in een variabele, omdat deze nu interactief gaat worden en dus gaat veranderen over de tijd is het de bedoeling om ook deze te gaan bijhouden in een state.

Je eerste gevoel is waarschijnlijk om rechtstreeks in de games array je nieuwe game te gaan pushen.

Maar zoals we al in het onderwerp over State hebben behandeld mag je NOOIT iets rechtstreeks aanpassen aan een state variabele. Je moet hier de setGames functie gebruiken die je hebt aangemaakt via de useState hook. We gebruiken hier de spread operator (drie puntjes) om de inhoud van de games array te nemen en daar achter een element te plakken.

Als je nu op de add knop drukt dan wordt de games state aangepast en dan zal ook de List component opnieuw gerendered worden. Dit komt omdat de games daar worden meegegeven als props. Als de props updaten van een component dan wordt het component zelf ook opnieuw gerendered.

Momenteel hebben we nog wel geen id aan deze game gegeven want op het moment dat we het formulier invulden wisten we nog niet wat de id ging worden. We kunnen hier het maximum berekenen van alle id waarden van alle games.

Props

Op dit moment hebben we nog altijd onze data die we gebruiken voor onze lijst in een globale variabele gestoken in de huidige applicatie. Het is uiteraard niet houdbaar om alles in globale variabelen te steken en deze zo te delen onder verschillende componenten. De juiste manier om dit te doen in React is deze lijst of andere variabelen door te geven als props of properties. We geven dus informatie door van het ene component naar de andere.

Voordat we props voor de eerste keer gaan gebruiken in onze applicatie gaan we eerst onze lijst van games verplaatsen naar het App component zelf.

const App = () => {
  let games: Game[] = [
    ...
  ]
  
  return (
    ...
  );
}

Nu geeft uiteraard de List component een foutmelding omdat de games variabele niet meer globaal staat en dit component dus geen toegang heeft tot deze lijst. We gaan dit oplossen door de games variabele door te geven aan de List component aan de hand van een react prop (property). Het lijkt alsof we een nieuwe html attribuut aanmaken voor de List component.

return (
  <div>
    <Header/>
    <List games={games}/>
    <InputView/>
  </div>
);

De properties van de List component moeten dan nog wel beschreven worden aan de hand van een interface. We geven deze interface meestal de naam van het component gevolgd door Props.

Als je deze interface dan wil gebruiken in je component kan je deze als volgt gebruiken.

Als je niet elke keer props.games wil typen om de games property aan te spreken kan je ook gebruikmaken van destructuring om de props al uit te pakken in aparte variabelen. Dit doe je op de volgende manier:

en dan kan je de games variabele gebruiken net zoals ervoor. Als je niet voor elk component een interface wil aanmaken kan je ook de interface in de functie definitie zelf beschrijven

Nu kunnen we onze kennis over props gaan gebruiken om onze List component nog verder op te splitsen in kleinere componenten als we dit willen. Zo kunnen we een ListItem introduceren die verantwoordelijk is voor het tonen van 1 item van de lijst.

en kunnen we het List component nog meer vereenvoudigen:

Het gebeurt vaak dat props doorheen meerdere componenten worden doorgegeven. Indien je componenten structuur dieper en dieper wordt, wordt het ook altijd maar minder praktisch om dingen te gaan blijven doorgeven aan de hand van properties. We zien later nog hoe we dit probleem kunnen oplossen.

interface InputViewProps {
  onAdd: (game: Game) => void
}
interface ListProps {
  games: Game[]
}
const List = (props : ListProps) => {
  return (
      <ul>{props.games.map((game: Game) => {
        ....
      })}
      </ul>
  );
}
const InputView = ({onAdd} : InputViewProps) => {
    ...
}
const handleClick: MouseEventHandler<HTMLInputElement> = (event) => {
  onAdd({
    name: name,
    releaseYear: year,
    sales: 0
  })
}
const [games, setGames] = useState<Game[]>([
  {
    id: 0,
    name: "World of Warcraft",
    releaseYear: 2004,
    sales: 0
  },
  {
    id: 1,
    name: "Valheim",
    releaseYear: 2021,
    sales: 0
  },
  {
    id: 2,
    name: "Minecraft",
    releaseYear: 2011,
    sales: 0
  }
]);
const handleAdd = (game: Game) => {
  games.push(game); // FOUT!
}
const handleAdd = (game: Game) => {
  setGames([...games, game])
}
const handleAdd = (game: Game) => {
  let max = Math.max(...games.map((g) => g.id!),0) + 1;
  game.id = max;
  setGames([...games, game])
}
const List = ({games} : ListProps) => {
  return (
      ...
  );
}
const List = ({games} : {games: Game[]}) => {
    ...
}
interface ListItemProps {
  game: Game
}

const ListItem = ({ game }: ListItemProps) => {
  return (
    <div key={game.id}>
      <div>{game.name}</div>
      <div>{game.releaseYear}</div>
      <div>{game.sales}</div>
    </div>
  )
}
const List = ({ games }: ListProps) => {
  return (
    <ul>{games.map((game: Game) => {
      return <ListItem game={game}/>
    })}
    </ul>
  );
}

Side-Effects

Soms willen we dat onze componenten iets doen dat niet binnen het takenpakket van een component hoort. Componenten moeten in principe alleen maar bezig zijn met het renderen van de UI. Maar Componenten moeten vaak wel bepaalde acties gaan uirvoeren die iets veroorzaken in de buitenwereld. Zo'n actie noemen we in React een side-effect. Een aantal voorbeelden van een side-effect kunnen zijn:

  • Het zetten van een title aan de hand van document.title

  • Het gebruiken van timers met setInterval en setTimeout

  • Het meten van de hoogte, breedte of positie van bepaalde elementen in de DOM

  • Het zetten of lezen van waarden uit de local storage

  • Data inlezen of schrijven naar externe web services.

Dit zijn allemaal dingen die we nooit rechtstreeks gaan doen in onze component code.

Als eerste voorbeeld van wanneer we side-effects moeten gebruiken gaan we eens zien naar het lezen en schrijven uit local storage. Local storage laat het toen bepaalde configuraties of user settings bij te houden in de browser van de gebruiker.

We gaan er nu voor zorgen dat alles wat er in het name veld wordt ingegeven wordt opgeslagen in de localstorage van de browser, zodat deze waarde volgende keer deze pagina wordt aangeroepen terug wordt getoond aan de gebruiker. We willen dus een bepaald stuk code laten lopen elke keer de name veranderd. Op het eerste gezicht zouden we het als volgt kunnen doen:

en kunnen we deze waarde uitlezen bij het zetten van de default waarde van onze state:

Merk op dat we hier de ?? operator gebruiken van typescript. Dit maakt het mogelijk om een default waarde mee te geven als het deel voor de ?? undefined of null is.

Deze code zal op zich wel werken, maar er is 1 groot nadeel. We kunnen de functie handleNameChange nergens anders meer hergebruiken. Dus elke keer deze functie wordt aangeroepen zal de localstorage aangepast worden. We willen dit dus loskoppelen van elkaar. Hiervoor gebruiken we de useEffect hook om de side-effect te triggeren telkens de name state wordt aangepast:

De useEffect hook neemt twee argumenten aan: het eerste is een functie die de side-effect uitvoert. In dit geval is de side effect het opslaan van de name in de localStorage. Het tweede argument is een array van dependencies. Dit is een lijst van variabelen (of states of props) die wanneer ze veranderen de side-effect zullen triggeren. In ons geval zal dus elke keer name aangepast wordt, de localstorage waarde geüpdatet worden.

Twee belangrijke dingen die je zeker moet weten:

  • Als je de array argument weglaat dan zal de side-effect elke keer de component gerendered wordt opnieuw uitgevoerd worden.

  • Als je een lege array meegeeft dan zal de side-effect enkel getriggered worden als het component voor de eerste keer gerendered wordt. Dit kan handig zijn voor het inlezen van data van een API

Wil je bijvoorbeeld de title van de pagina updaten op het moment dat de component voor de eerste keer gerendered wordt kan je dit doen aan de hand van:

Hoewel het lukt willen we nooit document.title aanpassen vanuit de code van het component zelf.

Handler Functies

We gaan nu eens kijken hoe we onze applicatie iets interactiever kunnen maken door de hand van invoer velden en handler functies. We gaan nu een nieuw component introduceren genaamd InputView Deze gaat de volgende invoer velden bevatten:

const InputView = () => {
  return (
    <div>
      <input type="text" id="name">
      <input type="number" id="year" name="year" min="1900" max="2021"/>
      <input type="button" id="submit" value="Add"/>
    </div>
  )
}

Hier hebben we 1 text invoer veld voor de naam van de film en een numeriek invoerveld voor het jaartal. We zetten hier een maximum 2021 en een minimum van 1900. Tot nu toe wordt er nog niets gedaan met deze invoervelden.

Om hier iets mee te doen gaan we een functie aanmaken om iets te doen als er iets ingetypt wordt in het invoerveld.

const handleChange = (event) => {
  console.log(event.target.value);
}

Je compiler zal hier direct een waarschuwing over geven dat event nog geen type heeft. TypeScript kan hier niet automatisch afleiden welk type dit event heeft. We zullen dit zo snel mogelijk oplossen.

Als we nu deze event listener functie willen gebruiken moeten we deze toevoegen als property van de input tag.

We kunnen de interface van de handleChange functie hierboven te weten komen door onze muis over de onChange property te plaatsen. Je krijgt dan iets gelijkaardig aan:

We kunnen nu het type van de handleChange functie aanpassen om de waarschuwing van hierboven op te lossen.

We kunnen op dezelfde manier een functie maken om een button click af te handelen:

Het type van de handleClick komen we op dezelfde manier te weten.

Componenten opsplitsen

Tot nu toe hebben wel alles in hetzelfde bestand gezet. Het App.tsx bestand waar momenteel alle componenten in zitten. Dit is natuurlijk geen best practice, zeker bij applicaties die complex zijn. Daarom gaan we nu zoveel mogelijk elk component in een apart bestand zetten, zodat het gemakkelijker wordt het juiste component te vinden in de bestandsstructuur. We gaan hier gebruik maken van javascript modules in aparte bestanden. React heeft geen vaste file structuur, wat je vaak wel hebt in andere frameworks. Dit maakt het soms wat ingewikkeld als je in een nieuw project stapt met bestaande code base.

hashtag
Aparte bestanden per component

const handleNameChange: ChangeEventHandler<HTMLInputElement> = (event) => {
  setName(event.target.value);
  localStorage.setItem('name', name);
}
const [name, setName] = useState<string>(localStorage.getItem('name') ?? '');
  useEffect(() => {
    localStorage.setItem('name', name);
  },[name]);
useEffect(() => {
  document.title = 'Games Store'
},[]);
Het eerste wat we kunnen doen is gewoon al onze componenten in aparte bestanden zetten. Dit zal de projectstructuur al zeker ten goede komen. We denken terug aan de export syntax uit vorige cursussen om onze componenten beschikbaar te maken vanuit andere bestanden.

We bekomen een structuur die er als volgt uitziet:

We bekijken even de List component om te bezien hoe zo'n component bestand er kan uitzien.

We zorgen met de laatste lijn dat we de List component exporteren zodat andere bestanden die kunnen importeren. Je ziet hierboven ook hoe dat importeren in zijn werk zal gaan. Je ziet dat hier het keyword default bij de export wordt gezet. Dit is nodig zodat je de componenten kan importeren op de volgende manier.

Soms zie je ook de syntax met { } in de import. Dit wil zeggen dat er meerdere functies, variabelen,... worden geëxporteerd in een bestand. Een goed voorbeeld hiervan is een nieuw bestand dat we hebben aangemaakt voor alle types in te beschrijven. We hebben alle types die met het model te maken hebben in 1 bestand gezet: types.ts . Op dit moment staat daar 1 interface in, maar het is uiteraard niet ondenkbaar dat daar meerdere interfaces zullen zitten. We exporteren dus elke interface apart.

Daarna moeten we deze interfaces dan apart importeren met de volgende syntax

Je zou er ook voor kunnen kiezen de componenten op dezelfde manier te exporteren. Dit is helemaal afhankelijk van je persoonlijke voorkeur.

hashtag
Van component files naar React folders

Bij het groeien van je project zullen ook je componenten alsmaar complexer worden. Er worden styles toegevoegd en tests. Je zou eventueel de vorige structuur blijven volgen en deze bestanden gewoonweg naast de component bestanden plaatsen. Vaak worden deze bestanden ook nog in een folder components geplaatst.

Je ziet dat dit op lange termijn niet echt houdbaar is. Op deze manier vinden we uiteindelijk nooit meer de componenten die we nodig hebben omdat die in een soep van bestanden terecht is gekomen. Daarom verkiezen we bij grotere projecten ook een directory per component.

Het noemen van deze bestanden is uiteraard totaal afhankelijk van allerlei aspecten. Zo kan je bijvoorbeeld een ander test framework gebruiken. Zo zal je ook later zien dat vaak in React CSS in typescript zelf wordt gedaan aan de hand van styled components. Dan heb je uiteraard geen css bestanden maar gewoon typescript bestanden. Het voordeel van deze manier van werking met folders is dat je tijdens het ontwikkelen van je applicatie de niet relevante componenten kan dichtklappen en dus je niet meer verzeild geraakt in een berg van bestanden.

Naast tests en css bestanden kan je ook bepaalde utility functies die relevant zijn voor dat component in aparte bestanden plaatsen. Een voorbeeld hiervan zijn hooks en utility functies.

We hebben ook een apart bestand gemaakt voor het ListItem component. Dit is eigenlijk een onderdeel van het List component want dit kan eigenlijk niet gebruikt worden zonder zijn ouder component. Meestal worden zo'n componenten samen bij zijn ouder component in de folder geplaatst.

Je zou eventueel dit component ook nog in een aparte folder onder List kunnen plaatsen. Maar pas altijd op dat je niet te veel folders gaat nesten in elkaar. De meeste richtlijnen geven de raad om nooit dieper dan 2 folders diep te nesten.

Uiteraard zijn er nog veel andere manieren om je project te structureren. En je zal nog andere manieren tegenkomen naarmate je werkt op grotere projecten.

hashtag
Absolute imports

Je zal al wel opgemerkt hebben tijdens het verplaatsen van alle components dat je structuur van je imports altijd met relatieve paden werken. Je herkent dit door imports zoals

je ziet dat je doorheen verschillende niveaus moet gaan aan de hand van ../ dit geeft aan dat het bestand een folder niveau hoger staat. Dit is niet zo'n handige manier van werken, zeker als je dingen begint te verplaatsen kan dit nogal onoverzichtelijk worden. Gelukkig is hier een oplossing voor. Open je tsconfig.json bestand en voeg de volgende lijn toe aan je compilerOptions

dit bestand zal er dan ongeveer als volgt uitzien

Herstart hierna visual studio code en hij zal dan automatisch oppikken dat je met absolute paden wil werken als je iets automatisch importeert.

<input type="text" id="name" onChange={handleChange}/>
<input type="number" id="year" name="year" min="1990" max="2021" onChange={handleChange}/>
const handleChange : ChangeEventHandler<HTMLInputElement> = (event) => {
  console.log(event.target.value);
}
const handleClick : MouseEventHandler<HTMLInputElement> = (event) => {
  console.log("button clicked");
}

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

hashtag
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

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.

En het App component zal er als volgt uit zien.

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)

hashtag
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:

triangle-exclamation

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.

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.

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.

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:

hashtag
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:

We moeten dan deze DetailPage toevoegen aan onze Router

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.

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.

Movie Web Service

hashtag
Express project

We gaan voor onze webapplicatie een web service bouwen aan de hand van node en express. We plaatsen uit gemak gewoon de code van onze api in de src folder van onze web applicatie zodat we toegang hebben tot alle bestanden van onze react applicatie. Zo kunnen we de types hergebruiken en moeten we deze dus niet dubbel definiëren.

In deze api folder voer je het volgende commando uit om de tsconfig.json file aan te maken.

door dit commando wordt de volgende tsconfig.json file aangemaakt

vervolgens doe je npm init om een package.json file te genereren.

Voor onze kleine API moeten we de volgende libraries installeren

  • De Express library laat toe om eenoudig en declaratief een web service te bouwen in node.

  • De cors library laat toe een bepaalde security check voor je browser te deactiveren.

  • Omdat we typescript gebruiken moeten we @types/express en @types/node

Voor we alle routes beginnen opstellen zetten we de simpele express applicatie op. We maken een index.ts bestand aan met de volgende inhoud.

De express applicatie wordt aangemaakt aan de hand van de express() functie (A).

(B) Je browser laat standaard nooit toe om web service calls te doen vanuit een javascript bestand naar een domein of poort dat verschilt van het domein of poort van waar het javascript bestand is gehosted.

Bv. als je javascript bestand gehosted is op http://localhost:3000/index.js kan je bijvoorbeeld geen API calls doen naar http://localhost:3001/ omdat de poort verschillend is. Je zal een error krijgen die sterk lijkt op:

om deze error te vermijden moeten we de cors package installeren. Deze laat toe om de Access-Control-Allow-Origin *header te zetten voor elke request zodat je browser dit wel toelaat.

(C) zorgt ervoor dat de body van http requests worden omgezet naar json objecten zodat die rechtstreeks kunnen aangesproken worden vanuit je code.

(D) Deze lijnen code zorgen ervoor dat er geluisterd wordt naar de poort 3001 en dat requests kunnen afgehandeld worden.

hashtag
Data inlezen

We maken een bestand games.json aan die de begin dataset zal voorstellen voor onze API.

We kunnen deze dan eenvoudig inlezen door na het starten van onze express server dit bestand in te lezen:

We moeten dan nog wel een globale variabele games moeten voorzien. We gebruiken hier de types die we voor onze React applicatie hebben gemaakt.

hashtag
Routes

De eerste route die we gaan aanmaken is een GET route voor/games Deze route gaat de array van games teruggeven als json.

We maken nu ook een route aan voor /games/:id zodat we ook een game kunnen opvragen met een id. Eerst halen we de id uit de parameters en zetten we deze om naar een getal aan de hand van de parseInt functie. We gebruiken de find methode op de games array om de juiste game te vinden. Als deze gevonden wordt geven we deze terug met de res.json() methode. Als deze niet gevonden wordt geven we een 404 Not Found terug.

Om de mogelijkheid toe te staan om ook games toe te voegen maken we een POST methode voor /games om een game toe te voegen. We zoeken eerst de hoogste id in de games array, want deze kan de gebruiker van de API niet zelf bepalen. Daarna gebruiken we het Game object in de body en vervangen we de id van dit object. Vervolgens voegen we het object toe aan de array. Na het toevoegen geven we de volledige array terug mee als json zodat de client geen extra request moet doen om de aangepaste lijst terug te krijgen.

circle-info

Voor de eenvoudigheid gaan we er vanuit dat er geen foutieve data aanwezig kan zijn dus er is geen server side input validatie aanwezig.

De volgende route die we gaan aanbieden is er een die het mogelijk maakt om een Game aan te passen. Daarvoor moeten we een PUT op /games/:id aanmaken. We gebruiken hier weeral de id van de parameters die worden doorgeven. Vervolgens gebruiken we de map functie om de games array om te zetten naar een nieuwe array maar waar het element vervangen is.

We konden in plaats van de map functie ook gewoon een eenvoudige for loop schrijven maar dit is veel uitgebreider.

Heel gelijkaardig kunnen we een POST methode maken voor /games/:id/sell om de sales counter te verhogen voor een game.

Nu kunnen we de API opstarten aan de hand van

Je kan via een tool zoals Postman nagaan of de routes werken zoals het hoort.

Context

In een React applicatie wordt data van het bovenste component doorgegeven naar het onderste component aan de hand van props. Dit is mogelijk in een applicatie waar de component structuur niet heel diep is, maar naar mate de applicatie groeit, groeit meestal ook de diepte van je componenten structuur. Je zal dan vaak een prop verschillende niveau's diep moeten doorgeven.

Stel dat we een light theme en een dark theme willen ondersteunen in onze applicatie. Elk component heeft deze prop nodig want elk component moet zijn UI aanpassen als de theme light of dark is.

Schematisch ziet dat er als volgt uit:

Je ziet dat zelfs voor een kleine applicatie, zoals als degene die we in deze cursus hebben gemaakt, dat deze theme prop al moeten doorgeven doorheen 3 niveaus van componenten. Bij grotere applicaties gaat dit nog veel erger worden. Daarom heeft React voor de Context

Styling

Er zijn veel manieren om stylesheets toe te passen op React applicaties. We gaan hier twee veel gebruikte manieren bekijken:

  • CSS-in-CSS (CSS Modules)

  • CSS-in-JS

Elk heeft zijn eigen voor en nadelen. Het is belangrijk om te weten dat geen van deze manieren beter is dan de andere.

- src
--- App.tsx
--- Header.tsx
--- InputView.tsx
--- List.tsx
--- ListItem.tsx
--- types.ts
import React from "react";
import ListItem from "./ListItem";
import { Game } from "./types";

interface ListProps {
  games: Game[]
}

const List = ({ games }: ListProps) => {
  return (
    <ul>{games.map((game: Game) => {
      return <ListItem game={game}/>
    })}
    </ul>
  );
}

export default List;
import ListItem from "./ListItem";
export interface Game {
  id?: number
  name: string,
  releaseYear: number,
  sales: number
}
import { Game } from './types.ts'
- src/
-- components
--- App.tsx
--- App.test.tsx
--- App.css
--- List.tsx
--- List.test.tsx
--- List.css
...
- src/
-- components
--- App
---- App.tsx
---- App.test.tsx
---- App.css
--- List
---- List.tsx
---- List.test.tsx
---- List.css
...
- src/
-- components
--- App/
--- List/
- src/
-- components
--- App
---- App.tsx
---- App.test.tsx
---- App.css
---- utils.ts
--- List
---- List.tsx
---- List.test.tsx
---- List.css
---- hooks.ts
- src/
-- components
--- App
---- App.tsx
---- App.test.tsx
---- App.css
--- List
---- List.tsx
---- List.test.tsx
---- List.css
---- ListItem.tsx
import InputView from '../InputView/InputView';
import List from '../List/List';
import { Game } from '../../types';
"baseUrl": "src"

Componenten

Maak een React web applicatie aan. Geef jouw web applicatie de naam "basic-componenten". Voeg vervolgens enkele “basic” componenten toe, die beschreven staan in volgende opdrachten:

Opdracht 1:

Voeg een component toe die een title op het scherm brengt “Welkom in het dierenparadijs”

Opdracht 2:

Voeg componenten toe die het 3de element toont uit de array “hond”, “kat”, “olifant”, “slang”

Opdracht 3:

Voeg een component toe die een random getal tussen 1 en 100 op het scherm brengt

Opdracht 4:

Voeg een component toe die een button toont met als label “maak geluid”. Voorlopig doet deze button nog niets. Hij wordt enkel op het scherm getoond.

The Animal Farm

In de opdrachtbeschrijving “Opdracht Componenten” werd gevraagd een aantal componenten in je web applicatie op te nemen, waaronder een array van dieren.

In deze opdracht wordt er gevraagd een uitbreidere web applicatie te maken waarbij je gebruik maakt van een array van dieren.

Opdracht 1:

Maak een nieuwe react web applicatie aan genaamd “animal-farm”

Opdracht 2:

Maak een uitbreiding van de array van dieren met het bijpassende geluid dat een dier maakt.

Zoek zelf naar geluidscomponenten van dierengeluiden.

Voeg een link naar die geluiden toe in de array van de dieren.

Opdracht 3:

Zorg ervoor dat de gebruiker een dier kan selecteren en dat vervolgens bij het aanklikken van “maak geluid” het juiste geluid wordt afgespeeld.

Routes en Context

Maak een React applicatie die bestaat uit minstens 4 componenten. Geef de applicatie de naam "routes-en-context".

Routing

Voorzie een hoofdmenu met navigatiebalk met als inhoud “Home”, “About”, “Games”, “Contact”. Bouw react routing in zodat bij het navigeren naar één van deze opties de inhoud van de gekozen pagina verschijnt en de juiste url gebruikt wordt.

Op de inhoud van de verschillende pagina’s toon je een tijdelijke inhoud (lorem ipsum). Maak wel duidelijk welke pagina wordt getoond.

Op de games pagina geef je de keuze uit 4 games (kies zelf welke). Kies zelf op welke manier je de keuze laat maken (drop down, button per game, ...).

Bij het kiezen van een game, moet je ervoor zorgen dat de gebruiker surft naar een pagina met de naam van de game in de url.

Context

Voor het gebruik van de app, willen we een onderscheid maken tussen het soort gebruikers: beginners en experts. Heel veel toekomstige instellingen willen we in alle pagina’s van de applicatie laten afhangen van deze instelling. Maak daarom gebruik van de Context api om deze instelling te kunnen instellen. Overal in de applicatie moet de gebruiker kunnen switchen tussen beginner en expert.

Voorzie 2 verschillende backgrounds, die door alle pagina’s gebruikt wordt en die wordt aangepast bij de keuze beginner en expert. Rechts bovenaan moet een aanduiding staan of de applicatie in beginners of in experts mode actief is.

https://code.visualstudio.com/code.visualstudio.comchevron-right
interface ListPageProps {
  games: Game[],
  handleAdd: (game: Game) => void
}
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;
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;
npm install --save react-router-dom@5
npm install --save-dev @types/react-router-dom@5
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;
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;
<Link to="/">Home</Link>
// <a href="/">Home</a>
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;
}
import styles from './DetailPage.module.css';

interface DetailPageProps {
  games: Game[]
}

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

export default DetailPage;
<Switch>
  <Route path="/list">
    <ListPage games={games} handleAdd={handleAdd} />
  </Route>
  <Route path="/" exact>
    <HomePage />
  </Route>
  <Route path="/detail/:id">
    <DetailPage games={games}/>
  </Route>
</Switch>
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;
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>
  )
}
installeren als dev dependency zodat we de types kunnen gebruiken voor
node
en
express
API gezorgd. Dit is een manier om data te gaan delen doorheen componenten zonder door heel de structuur te moeten doorgegeven worden.

Het eerste wat we moeten doen is een Context aanmaken aan de hand van de createContext

Je moet altijd een default waarde meegeven aan de context. Deze default waarde wordt enkel gebruikt als er geen provider aanwezig is bovenaan in de component structuur. We geven hier een object met een mode property. Dus in ons geval zal onze layout dus standaard in light mode staan.

Nu moeten we een Provider maken van onze ThemeContext zodat we de values kunnen meegeven die we in de componenten willen gebruiken. De provider moet rond de bovenste component staan die onze context willen gebruiken.

Nu kan je de values van de ThemeContext provider opvragen aan de hand van de useContext hook. Als de mode dark is dan voegen we een extra className toe zodat we in ons css bestand een gameCardDark class kunnen maken.

Je ziet nu dat elk component toegang heeft tot de mode variabele die we in de context hebben aangeboden zonder dat we deze moeten doorgeven doorheen de component hierarchie.

We kunnen ook handler functies toevoegen in onze ThemeContext zodat we ook van elk component de mode kunnen aanpassen. We voegen nu aan de createContext functie een setMode functie toe. We moeten hier een default value voor aanbieden, op dit moment hebben we nog geen handler dus we geven een lege functie mee.

We maken nu in onze App component een state aan die de mode bijhoudt zodat we deze kunnen aanpassen.

Nu geven we de setMode functie mee aan de provider

Vanaf nu is de setMode functie overal beschikbaar in elk component die deze context gebruikt. We kunnen nu de Header component uitbreiden met een ModeButton component die gebruik kan maken van de mode en setMode van de context. We maken hier gebruik van een FontAwesome icon. Dit is een grote verzameling van icons die worden aangeboden om in projecten te gebruiken. Deze kan je installeren door

uit te voeren. We maken ook nog een nieuwe component ModeButton in een nieuwe component directory components/ModeButton

We tonen aan de hand van de huidige mode nu een button die een zon icon toont als het dark mode is en een maan icon als het light mode is. We zetten hier een onClick listener op en roepen in deze listener de setMode functie op. Dit is de setMode functie die we hebben meegegeven met de ThemeContext provider.

We kunnen de button stylen met een css module te maken voor de ModeButton component.

Nu kunnen we deze in de Header component plaatsen en dan hebben we een manier om de dark mode en de light mode te triggeren.

Er zijn nog veel andere voorbeelden waar Context handig is om je applicatie structuur te verbeteren. Maar zorg er wel voor dat je deze niet te veel begint te gebruiken. Zolang je props niet op veel verschillende plaatsen moeten gebruikt worden of niet door verschillende componenten heen moeten doorgegeven worden is het soms beter om toch props te blijven gebruiken. Context is geen alternatief voor props, dus gebruik het zeker niet om alles globaal beschikbaar te maken.

hashtag
CSS Modules

CSS Modules is de aanpak die het hardste aanleunt bij traditionele css bestanden zoals deze gebruikt worden in andere html pagina's. Alle elementen krijgen een class toegewezen en krijgen een style toegewezen via een CSS bestand. In React gebruiken we niet class maar className.

In React willen we alle componenten zo zelfstandig mogelijk kunnen werken zonder te veel afhankelijkheden van elkaar. Daarom proberen we zoveel mogelijk per component een aparte CSS bestand aan te maken. Het is perfect mogelijk om alles in de algemene index.css te plaatsen, maar dat gaan we zoveel mogelijk vermijden.

We kunnen CSS bestanden toevoegen aan een react component aan de hand van de import mechanisme van javascript.

Dit CSS bestand kan dan de volgende inhoud hebben:

We kunnen dan bijvoorbeeld onze <div> tags stylen als volgt

Het nadeel van deze manier is dat we moeten opletten dat al deze CSS bestanden niet dezelfde class names gebruiken. Als dit wel gebeurt dan zullen deze CSS classes met elkaar botsen en zal de ene de andere overschrijven.

Om dit probleem op te lossen biedt React een betere manier aan van importeren van CSS bestanden. We gaan gebruik maken van CSS modules. Het gebruik is zeer gelijkaardig aan gewone CSS bestanden maar deze gaan volledig onafhankelijk van elkaar werken. Alle classes krijgen automatisch een willekeurig stuk tekst toegewezen zodat de namen van de classes allemaal uniek zijn. Om gebruik te maken van de CSS Modules moet het CSS bestand de naam krijgen van de Component gevolgd door .module.css

Als we nu gebruik willen maken van CSS modules in ons App component moeten we dit hernoemen naar App.module.css en moeten we de manier van importeren iets aanpassen

We gebruiken hier dan ook niet meer een string als we de className meegeven maar geven hier direct een variabele uit het styles object.

Je kan eventueel de vscode plugin installeren die autocomplete geeft voor de CSS modules.

We kunnen nu de rest van onze componenten beginnen stylen met CSS Modules.

Dit gaat onze header een grotere font size geven en in uppercase letters zetten. Er zal ook een lijn onder de titel gezet worden.

Dit maakt van de div een flexboxarrow-up-right layout met als direction 'column'. Dit zorgt ervoor dat de elementen die in de div geplaatst worden verticaal zullen worden geplaatst worden in plaats van horizontaal. De breedte van de input-velden worden allemaal vastgezet op 500 pixels en krijgen allemaal een zwarte rand.

We zorgen met de gamesList class dat de div elementen van de games worden in een rij gepositioneerd. Met de flex-wrap property zorgen we dat de elementen op een nieuwe lijn worden geplaatst als deze niet op 1 lijn passen. We willen alle games laten zien aan de gebruiker als kaartjes met een schaduw en nog verschillende eigenschappen.

Je layout zal er ongeveer als volgt uitzien:

hashtag
CSS-in-JS

De manier van styling die we hierboven hebben toegepast is zeer gelijkaardig met hoe je in andere HTML paginas gebruik hebt gemaakt van CSS styles. Je hebt een apart CSS-bestand waarin je alle stijlen beschrijft aan de hand van classes en gebruikt classNames voor deze te gebruiken. In react heb je een alternatieve manier om dit te doen, je kan de styles rechtstreeks ook in javascript definiëren. Het voordeel van deze manier is dat we niet afhankelijk zijn van aparte CSS-bestanden en alles wat nodig is om een component te renderen in javascript beschikbaar is.

De eerste manier om dit te doen is via inline styles. Je geeft rechtstreeks de styles mee aan de hand van de style property.

Je ziet dat we hier ook niet gebruik maken van de font-size, border-bottom en text-transform die we in het gewone CSS-bestand gebruikte. De regel is dat we hier de camelCase varianten gebruiken van onze CSS properties. Zo wordt bvfont-size dus fontSize.

In plaats van het object rechtstreeks mee te geven aan de property kan je deze ook in een aparte constante plaatsen.

Niet alle dingen die je kan gebruiken in CSS kan je in CSS-in-JS gebruiken. Dingen zoals animaties zijn hierdoor niet eenvoudig te implementeren. Daarom wordt er vaak voor CSS-in-JS gebruik gemaakt van de library styled-components

Je kan deze eenvoudigweg installeren door

uit te voeren in je terminal en dan te importeren op de volgende manier

zo kunnen we onze header component ook als volgt stylen

Je merkt op dat we hier wel de standaard CSS properties kunnen gebruiken als in een gewoon CSS-bestand.

We gaven al aan dat we zelfs animaties kunnen definiëren in CSS. Dit kunnen we op de volgende manier doen:

hashtag
UI Frameworks

Er zijn een tal van frameworks die het gebruik van zelf CSS schrijven minimaliseren. Je maakt daar gebruik van herbruikbare componenten die allemaal al voor jou geschreven zijn. De meest bekende zijn:

  • https://material-ui.com/arrow-up-right

  • https://react-bootstrap.github.io/arrow-up-right

  • https://tailwindcss.com/arrow-up-right

Deze vereenvoudigen het ontwikkelproces drastisch en zorgen ervoor dat er een bepaald design systeem wordt gebruikt. Dit zorgt ervoor dat het ontwikkelen van web applicaties drastisch kan versneld worden.

Dreamstays

hashtag
Beschrijving

Dreamstays is een applicatie die – zoals de naam het zegt – exclusieve verblijven wil gaan aanbieden in v luxueuze verblijven die onze dromen overstijgen. Het gaat om verblijven in villa’s die wereldwijd verspreid zijn.

Maak hiervoor een applicatie "dreamstays" aan die de gebruiker toelaat in een lijst van beschikbare villa’s een villa te zoeken.

Verfijn jouw applicatie op basis van volgende opdrachten:

Opdracht 1:

Zet bovenaan de pagina het logo en de slogan “ Beyond your dreams”

Opdracht 2:

Voorzie een dropdown keuzelijst waarbij de gebruiker kan kiezen in welk land hij een luxeverblijf wil zoeken.

Opdracht 3:

Toon de lijst van beschikbare villa’s voor het land dat de gebruiker selecteert.

Opdracht 4:

Laat de details van het pand op het scherm verschijnen nadat de gebruiker 1 pand uit de lijst heeft aangeklikt. Zorg ervoor dat op deze detailpagina een foto van de gekozen villa wordt getoond, de prijs en de beschrijving.

Opdracht 5:

Het doel van deze opdracht is om jouw DreamStay webapplicatie te stylen en te personaliseren. Je zal hiervoor zelf opzoekingswerk moeten doen.

Je kan gebruik maken van een UI framework zoals Bootstrap om de DreamStay applicatie een professionele look te geven.

Enkele requirements:

  • Toon op de home page een hero image.

  • Kies mooie lettertypes en font sizes.

  • Voeg gepaste grafische elementen toe.

Doe een voorstel van de layout die je wil bouwen en laat dit goedkeuren door jouw docent voor dit vak alvorens je met de realisatie begint!

Maak voor het uitvoeren van bovenstaande opdrachten gebruik van de bijgevoegde bronbestanden:

Logo Dreamstay Villas

Json bestand met beschikbare data van de villa’s

Images van de aanwezige villa’s

ButtonHooks

Opdracht:

Deze opdracht bestaat uit een aantal deelopdrachten waarbij de complexiteit van de applicatie steeds groter wordt.

De uiteindelijke bedoeling is om een dashboard te maken waarop een aantal tellers worden getoond, waarvan voor elke teller de waarde door de gebruiker kan worden gewijzigd door voor elke teller specifiek een verhogingswaarde in te stellen. Door op een knop te duwen, kan de gebruiker bepalen wanneer de tellers moeten worden verhoogd.

Voorbeeld:

Teller1 heeft als initiële waarde 1 en als verhogingswaarde ook 1.

Als de gebruiker op de knop “verhoog” drukt, dan zal de nieuwe waarde van teller1 = 2 te worden.

Veronderstel dat de gebruiker de verhogingswaarde van teller1 daarna op 4 zet, dan zal de volgende keer dat de gebuiker op “verhoog” duwt, de waarde van teller1 = 6 worden (2 oude waarde + 4 verhoging).

Detailbeschrijving:

Elke teller is een object. Elke teller heeft een key, een naam, een calcul (=de verhogingswaarde) en een value (=de huidige waarde van de teller).

Als de gebruiker op een knop “verhoog” drukt, dan moeten alle tellers verhoogd worden met de voor hen specifieke verhogingswaarde.

De initïële waarde van elke teller alsook de verhogingswaarde is steeds = de index = de key van de teller.

Nog niet alle kennis zal onmiddellijk na de eerste lesweek aanwezig zijn om deze volledig applicatie te bouwen zoals hierboven beschreven is.

Deze opdracht bestaat daarom uit onderstaande deelopdrachten. Tijdens de labo’s zal aangegeven worden welke opdracht wanneer kan worden gemaakt.

Opdracht 1:

Voorzie een knop op het scherm. Elke keer als de gebruiker op de knop drukt, moet de waarde van de knop verhoogd worden met 5. De initiële waarde van de knop zet je op 25.

Opdracht 2:

Maak de knop die je in deel1 creëerde generieker waarbij je door een parameter mee te geven bepaalt met welke waarde de knop verhoogd moet worden.

Opdracht 3:

Laat de gebruiker bepalen met welke waarde de knop verhoogd moet worden door hem dit in een invoerveld te laten invoeren.

Opdracht 4:

Zet 5 tellers op het scherm waarbij je voor elke teller afzonderlijk de waarde laat verhogen door een door de gebruiker gekozen waarde. Als de gebruiker op 1 knop “verhoog” drukt, dan moeten de waarde van alle tellers worden verhoogd.

Opdracht 5:

Laat de gebruiker kiezen hoeveel tellers hij op het scherm wil zien. Voorzie daarvoor een invoerveld. Initieel moet dus dat aantal tellers worden getoond met hun initiële waarde. Bij het drukken op een “add” knop moet dit aantal knoppen worden toegevoegd. De werking van elke teller moet hetzelfde blijven als in deel4. De gebruiker kan dus nog steeds per teller de verhogingswaarde instellen.

Voorbeeldscenario met bijhorende schermen:

Opstarten van de applicatie: default staat het aantal tellers bij de opstart van de applicatie op 2. Er worden dus 2 tellers getoond met hun initiële waarde en hun verhoogwaarde.

Nadat de gebruiker op de knop “verhoog” gedrukt heeft, zal het volgende worden getoond:

Veronderstel dat de gebruiker nu zegt dat er 3 tellers moeten worden toegevoegd. De gebruiker zet aantal bijkomende tellers op 3.

Veronderstel dat de gebruiker op de “add” knop drukt: 3 bijkomende tellers worden geïnitialiseerd

Na het drukken op de knop “verhoog”:

Veronderstel dat de gebruiker de verhoogwaarde van teller 3 en teller 6 als volgt heeft aangepast:

Als vervolgens de gebruiker op de knop “verhoog” drukt:

Opmerking: bij het wijzigen van het aantal bijkomende tellers, moet de applicatie dus al onmiddellijk reageren zonder dat nog eens extra op de knop add moet worden gedrukt. Bij het wijzigen van de verhoogwaarde, gebeurt er nog niets. Er moet dan wel op de knop “verhoog” worden geduwd om de verhoging met de nieuw ingestelde verhoogwaardes door te voeren.

hashtag
Demo:

tsc --init
{
  "compilerOptions": {
    "target": "es5",                                
    "module": "commonjs",                           
    "allowJs": true,                             
    "strict": true,                                 
    "esModuleInterop": true,                        
    "skipLibCheck": true,                           
    "forceConsistentCasingInFileNames": true        
  }
}
npm install --save express cors
npm install --save-dev @types/node @types/express
import { Request, Response } from 'express';
import express from 'express';

const app = express();                           // A

var cors = require('cors')                       // B

app.use(cors())                                  // B
app.use(express.json());                         // C
app.use(express.urlencoded({ extended: true })); // C

app.listen(3001, async() => {                    // D        
    console.log('The application is listening on port 3001!');
})

export {};
var cors = require('cors');

app.use(cors());
[
  {
    "id": 0,
    "name": "World of Warcraft",
    "releaseYear": 2004,
    "sales": 0
  },
  {
    "id": 1,
    "name": "Valheim",
    "releaseYear": 2021,
    "sales": 0
  },
  {
    "id": 2,
    "name": "Minecraft",
    "releaseYear": 2011,
    "sales": 0
  }
]
import { promises as fs } from 'fs';
import { Game } from '../types';

...

app.listen(3001, async() => {
    games = JSON.parse(await fs.readFile('games.json', 'utf8')) as Game[];
    console.log('The application is listening on port 3001!');
});
let games: Game[] = [];
app.get('/games', (req:Request, res:Response) => {
    res.json(games);
});
app.get('/games/:id', (req:Request, res:Response) => {
    let id = parseInt(req.params.id);
    let game = games.find((g: Game) => g.id === id);
    if (game !== undefined) {
        res.json(game);
    } else {
        res.status(404).json();
    }
});
app.post('/games', (req:Request, res:Response) => {
    let newId : number = Math.max(...games.map((g: Game) => g.id!)) + 1;
    let game : Game = req.body as Game;
    game.id = newId;
    games.push(game);
    res.json(games);
});
app.put('/games/:id', (req: Request, res: Response) => {
    let id = parseInt(req.params.id);
    let game : Game = req.body as Game;

    games = games.map((g: Game) => (g.id === id) ? { ...g, ...game } : g);

    res.json(games);
});
for (let i=0;i<games.length;i++) {
    if (games[i].id === id) {
        games[i] = {...games[i], ...game}
    }
}
app.post('/games/:id/sell', (req: Request, res: Response) => {
    let id = parseInt(req.params.id);
    games = games.map((g: Game) => (g.id === id) ? { ...g, sales: g.sales+1 } : g);
    res.json(games);
});
ts-node index.ts
const App = () => {
    ...
    <ListPage games={games} handleAdd={handleAdd} theme={'light'}/>
    ....
}
interface ListPageProps {
  games: Game[],
  handleAdd: (game: Game) => void,
  theme: string
}

const ListPage = ({games,handleAdd, theme} : ListPageProps) => {
  return (
    <div>
      <List games={games} theme={theme}/>
      <InputView onAdd={handleAdd} theme={theme} />
    </div>
  );
}
interface ListProps {
  games: Game[],
  theme: string
}

const List = ({ games, theme }: ListProps) => {
  return (
    <div className={styles.gamesList}>{games.map((game: Game) => {
      return <ListItem game={game} theme={theme}/>
    })}
    </div>
  );
}
interface ListItemProps {
  game: Game,
  theme: string,
}

const ListItem = ({ game, theme }: ListItemProps) => {
  ...
}
export const ThemeContext = React.createContext({mode: 'light'});
return (
  <ThemeContext.Provider value={{mode: 'light'}}>
    <Router>
      ...
    </Router>
  </ThemeContext.Provider>
import { ThemeContext } from 'components/App/App';
import { useContext } from 'react';
import { Link } from 'react-router-dom';
import { Game } from 'types';
import styles from './List.module.css'

interface ListItemProps {
  game: Game
}

const ListItem = ({ game }: ListItemProps) => {
  const {mode} = useContext(ThemeContext);

  return (
    <Link to={`/detail/${game.id}`}>
      <div key={game.id} className={`${styles.gameCard} ${mode === 'dark' ? styles.gameCardDark : ''}`}>
        <div>{game.name}</div>
        <div>{game.releaseYear}</div>
        <div>{game.sales}</div>
      </div>
    </Link>
  )
}

export default ListItem;
export const ThemeContext = React.createContext({mode: 'light', setMode: (mode: string) => {}});
const [mode, setMode] = useState('light');
<ThemeContext.Provider value={{mode: mode, setMode: setMode}}>
npm install --save @fortawesome/fontawesome-svg-core
npm install --save @fortawesome/free-solid-svg-icons
npm install --save @fortawesome/react-fontawesome
import React, { useContext } from 'react';

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
import { ThemeContext } from 'components/App/App';
import styles from './ModeButton.module.css'

const ModeButton = () => {
  const {mode, setMode} = useContext(ThemeContext);
  return (
    <div className={styles.modeToggle}>
      
      {mode === 'dark' ? 
        <button onClick={() => {setMode('light')}}><FontAwesomeIcon icon={faSun} className={styles.modeIcon}/></button> :
        <button onClick={() => {setMode('dark')}}><FontAwesomeIcon icon={faMoon} className={styles.modeIcon}/></button>
      }
    </div>
  )
}

export default ModeButton;

.modeToggle {
  display: flex;
  align-items: center;
  margin-left: auto;
  padding-right: 20px;
}

.modeToggle button {
  background-color: transparent;
  border: none;
}

.modeIcon {
  color: white
}

.modeIcon:hover {
  color: lightblue
}
import './App.css';
.container {
    padding: 10px
}
<div className="container">
      <Header/>
      <List games={games} />
      <InputView onAdd={handleAdd} />
</div>
import styles from './App.module.css';
  <div className={styles.container}>
    <Header />
    <List games={games} />
    <InputView onAdd={handleAdd} />
  </div>
// Header.tsx
import styles from "./Header.module.css"

const Header = () => {
  return (
    <h1 className={styles.header}>Welcome to the H2O Game shop</h1>
  );
}

export default Header;
/* Header.module.css */
.header { 
  font-size: 22pt;
  border-bottom: 2px solid black;
  text-transform: uppercase;
}
// Inputview.tsx
...
import styles from "./InputView.module.css";
...

const InputView = ({onAdd} : InputViewProps) => {
  ...
  return (
    <div className={styles.inputView}>
      ...
    </div>
  )
}

export default InputView;
/* Inputview.module.css */
.inputView {
  margin: 10px;
  display: flex;
  flex-direction: column;
}

input {
  width: 500px;
  margin: 5px;
  box-sizing: border-box;
}
// List.tsx
...
import styles from './List.module.css'
...

const List = ({ games }: ListProps) => {
  return (
    <div className={styles.gamesList}>{games.map((game: Game) => {
      return <ListItem game={game}/>
    })}
    </div>
  );
}

export default List;
// ListItem.tsx
...
import styles from './List.module.css'
...

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

export default ListItem;
/* List.module.css */
.gamesList {
  display: flex;
  flex: row;
  flex-wrap: wrap;
}

.gameCard {
  border: 1px;
  padding: 10px;
  width: 200px;
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  transition: 0.3s;
  background-color: white;
  border-radius: 5px; /* 5px rounded corners */
  margin: 10px
}
const Header = () => {
  return (
    <h1 style={{fontSize: '22pt', borderBottom: '2px solid black', textTransform: 'uppercase'}}>Welcome to the H2O Game shop</h1>
  );
}
import { CSSProperties } from 'react';

const headerStyle : CSSProperties = {
  fontSize: '22pt', borderBottom: '2px solid black', textTransform: 'uppercase'
}

const Header = () => {
  return (
    <h1 style={headerStyle}>Welcome to the H2O Game shop</h1>
  );
}

export default Header;
npm install styled-components
npm install @types/styled-components
import styled from 'styled-components'
import styled from 'styled-components'

const TitleHeader = styled.div`
  font-size: 22pt;
  border-bottom: 2px solid black;
  text-transform: uppercase;
`;

const Header = () => {
  return (
    <TitleHeader>Welcome to the H2O Game shop</TitleHeader>
  );
}

export default Header;
import styled, { keyframes } from 'styled-components'

const fadeIn = keyframes`
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
`;

const TitleHeader = styled.div`
  font-size: 22pt;
  border-bottom: 2px solid black;
  text-transform: uppercase;
  animation: 1s ${fadeIn} ease-out;
`;

const Header = () => {
  return (
    <TitleHeader>Welcome to the H2O Game shop</TitleHeader>
  );
}

export default Header;
Laat je inspireren door het web... bijvoorbeeld:
  • of doe zelf opzoeking op internet om een hedendaagse, trendy layout te bouwen.

https://en.99designs.be/blog/trends/web-design-trends/arrow-up-right
https://www.lambdatest.com/blog/web-design-trends-2020/arrow-up-right
https://marketplace.visualstudio.com/items?itemName=clinyong.vscode-css-modulesmarketplace.visualstudio.comchevron-right

WebService aanroepen

Tot nu toe hebben we de data rechtstreeks van de applicatie code zelf gebruikt. In de meeste dynamische webapplicaties gebruik je een externe web service met een of andere dataopslag (SQL-database, files, ...). In dit deel gaan we onze webapplicatie aanpassen zodat die gebruik maakt van de Movie Web Service.

Bij het oproepen van een web service krijg je nooit direct het antwoord terug. Er zal altijd een kleine vertraging opzitten omdat deze via het internet zal verlopen, daarom moet deze asynchroon gebeuren. Soms wordt zo'n web service call ook een AJAX call genoemd.

Er zijn veel verschillende libraries om AXJAX calls te maken in React applicaties. Hieronder vindt je een lijst van veel gebruikte libraries:

  • Axios

  • Fetch (ingebouwd in elke browser)

  • Superagent

  • React-axios

  • Use-http

  • React-request

We kunnen hier in principe eender welke library gebruiken maar we gaan Fetch gebruiken omdat deze geen externe library nodig heeft en ingebouwd is in elke moderne browser.

Omdat onze games niet meer in het begin beschikbaar zijn in onze applicatie zorgen we er nu voor dat de state op een lege array wordt gezet bij het initializeren van het App component.

We gaan nu even terug naar wat we gezien hebben in het onderdeel side-effects. We hebben daar de useEffect hook gezien en we hebben daar afgesproken dat alle dingen die bestaat uit communicatie met een externe service via effects moet gebeuren. We hebben daar ook gezien dat we altijd een lege array mee moeten geven als we deze effect maar 1 keer willen laten uitvoeren bij de eerste render van het component.

Je ziet hier dat we voorlopig toch gewoon onze initiële array hier opvullen met onze voorbeeld data. We vullen deze wel pas op na 1 seconde. Dit doen we aan de hand van de javascript setTimeout functie. We doen dit omdat we soms een API call willen nabootsen om dingen eerst te testen of omdat de API op dat moment nog niet klaar is. De reden dat we pas de setGame gebruiken na 1 seconde is omdat we een tragere netwerk request willen naboosten die ongeveer 1 seconde duurt.

Als je nu naar onze webapplicatie naar de /list path zou gaan met je browser zal je zien dat de lijst eerst leeg zal zijn en dan na een seconde gevuld zal worden. Dit kan voor de gebruiker een beetje raar zijn, daarom gaan we nu een extra updating state invoeren zodat we een loading indicator kunnen laten zien aan de gebruiker terwijl de data laadt.

Bij aanvang van onze nagemaakte API call zetten we updating op true en nadat de data is aangepast dan zetten we deze terug op false.

We kunnen nu nakijken of updating op true staat voordat we de rest van onze layout gaan renderen. Als updating true is dan tonen we een div met de text 'Loading data' en anders tonen we gewoon onze Switch van onze routing.

We kunnen deze loading text ook nog vervangen met een loading spinner aan de hand van wat css magie:

en in onze App.module.css maken we dan deze classes aan zodat we deze loader kunnen laten draaien aan de hand van keyframes

We gaan nu de eigenlijke web service aanspreken om de games op te halen. We maken een aparte functie voor het ophalen van games van de API. De getGames functie duiden we aan als async functie omdat we er gebruik gaan maken van async/await omdat er asynchrone code in deze functie staat (het ophalen van de games via de API).

Net als bij de nagebootste call van hiervoor zetten we eerst de updating state eerst op true. Vervolgens doen we de fetch call naar http://localhost:3001/games met de GET methode. Deze fetch methode geeft de response terug als stream, dus deze moet nog helemaal omgezet worden naar json aan de hand van de response.json() methode. Beide fetch en json zijn functies die een promise terug geven dus hiervoor kan het await keyword geplaatst worden zodat er gewacht wordt tot de promise afgehandeld is. Als we het resultaat volledig hebben gebruiken we de setGames methode om de state van de games aan te passen.

Nu kunnen we deze methode aanroepen in onze effect

Met Fetch kunnen we uiteraard ook POST requests doen naast GET requests. Zo kunnen we heel eenvoudig onze handleAdd functie aanpassen zodat die nu een Game toevoegt aan de hand van de POST op /games. Net als bij de getGames zetten we eerst setUpdating op true zodat de loader getoond wordt. We geven nu een method: POST mee aan de fetch functie en plaatsen we het game object in de body. We moeten het wel eerst naar string omzetten. De POST op /games geeft een json terug met alle games in zodat we deze response kunnen gebruiken om onze games state te updaten.

const [games, setGames] = useState<Game[]>([]);
useEffect(() => {
  setTimeout(() => {
    setGames([
    {
      id: 0,
      name: "World of Warcraft",
      releaseYear: 2004,
      sales: 0
    },
    {
      id: 1,
      name: "Valheim",
      releaseYear: 2021,
      sales: 0
    },
    {
      id: 2,
      name: "Minecraft",
      releaseYear: 2011,
      sales: 0
    }
    ]);
  },1000);
},[])
const [updating, setUpdating] = useState(true);

useEffect(() => {
  setUpdating(true);
  setTimeout(() => {
    setGames([
      {
        id: 0,
        name: "World of Warcraft",
        releaseYear: 2004,
        sales: 0
      },
      {
        id: 1,
        name: "Valheim",
        releaseYear: 2021,
        sales: 0
      },
      {
        id: 2,
        name: "Minecraft",
        releaseYear: 2011,
        sales: 0
      }
    ]);
    setUpdating(false);
  },1000);
},[])
return (
  <Router>
    <div>
      <Header />
      {updating ? <div className={styles.container}>Loading data</div> :
      <div className={styles.container}>
        <Switch>
          <Route path="/list">
            <ListPage games={games} handleAdd={handleAdd} />
          </Route>
          <Route path="/detail/:id">
            <DetailPage games={games} />
          </Route>
          <Route path="/" exact>
            <HomePage />
          </Route>
        </Switch>

      </div>}
    </div>
  </Router>
);
{updating ? <div className={styles.loaderContainer}><div className={styles.loader}></div></div> :
.loaderContainer {
  display: flex;
  align-items: center;
  height: 100vh;
  justify-content: center;
}

.loader {
  border: 4px solid #f3f3f3; /* Light grey */
  border-top: 4px solid #3498db; /* Blue */
  border-radius: 50%;
  width: 64px;
  height: 64px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
const getGames = async () => {
  setUpdating(true);

  let response = await fetch('http://localhost:3001/games',{
    method: 'GET', // *GET, POST, PUT, DELETE, etc.
  });
  let json = await response.json();

  setGames(json as Game[]);

  setUpdating(false);
}
useEffect(() => {
  getGames();
},[]);
const handleAdd = async (game: Game) => {
  setUpdating(true);
  let response = await fetch('http://localhost:3001/games',{
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(game)
  });
  let json = await response.json();
  setGames(json as Game[]);
  setUpdating(false);
}