In deze tutorial focussen we ons op het gebruik en begrip van React Hooks. Hiervoor maken we als voorbeeld een simpele productpagina met een shopping cart (zie afbeelding 1). De shopping cart representeert het geheugen (oftewel de ‘state’) van de productpagina. State verwijst in het algemeen naar applicatie data die moet worden bijgehouden.
Auteur: Peter Eijgermans
Om React hooks beter te doorgronden starten we met het implementeren van de ‘useState’ hook om de state (concreet: de shopping cart) bij te werken in de applicatie.
Tenslotte vervangen we de useState hook door de ‘useReducer’ hook. De useReducer hook heeft meestal de voorkeur als je complexe state logica hebt.
Als uitgangspunt voor de tutorial, clone je de volgende Github repository:
git clone https://github.com/petereijgermans11/react-hooks
Installeer de dependencies middels: npm i && npm start
en open de webapp op: http://localhost:3000
Wat is een component in React?
Voordat we met de implementatie van de productpagina aan de slag gaan, zoom ik even in op wat een component is in React.
Componenten zijn onafhankelijke en herbruikbare stukjes code. Ze hebben hetzelfde doel als JavaScript-functies, maar retourneren HTML. In het geval van React wordt er JSX code geretourneerd. JSX stelt ons in staat om HTML-elementen in JavaScript te schrijven en deze in de DOM te plaatsen zonder de methoden createElement() en/of appendChild() te gebruiken. Zie listing 1 voor een voorbeeld van een functie component die properties (als functie arguments) ontvangt en JSX retourneert. Zie ook paragraaf: Klasse componenten versus functie componenten voor meer context.
function Product(props) {
return <div> {props.message} </div> }
export default Product; |
Listing 1
Let op dat om een JavaScript-expressie in JSX in te sluiten, de JavaScript moet worden gewrapped met accolades, zoals {props.message}.
Tevens moet de naam van een React component altijd beginnen met een hoofdletter.
Om dit component beschikbaar te maken in de applicatie dien je deze te exporteren (listing 1):
Components in Files
Bij React draait alles om het hergebruiken van code, en het is aan te raden om je componenten op te splitsen in aparte bestanden.
Om dat te doen, maak je een nieuw bestand met een .js-bestandsextensie en plaats de code voor het component erin. In ons voorbeeld plaatsen we de code in de file Product.js. Deze staat in de folder: src/Components/Product. Let op dat de bestandsnaam moet beginnen met een hoofdletter.
Component in een component
Nu heb je een component genaamd Product, die een message retourneert.
Om dit component in je applicatie te gebruiken, gebruik je een vergelijkbare syntaxis als normale HTML: <Product />. Zie Listing 2 voor een voorbeeld hoe je dit component aanroept met een message-property in een App component:
import Product from ‘../Product’;
function App() { return <Product message=”My shopping cart” /> }
export default App; |
Listing 2
Klasse componenten versus functie componenten
Er zijn twee soorten componenten in React: klasse componenten en functie componenten. In deze tutorial zullen we ons concentreren op functie componenten, omdat React hooks alleen werken in functie componenten.
Stap 1 – Aanmaken Product component
Na deze korte introductie starten we met het maken van een simpele Product ‘functie component’ met React (zie Listing 3). Het component bestaat uit twee delen:
- de shopping cart, dat het aantal gekozen artikelen en de totale prijs bevat
- en het product, die twee knoppen heeft om het artikel toe te voegen of te verwijderen van de shopping cart.
Voeg de volgende code toe aan de Product.js voor een Product functie component (zie voorbeeld Product_1.js):
export default function Product() {
return( <div> <div>Shopping Cart: 0 total items</div> <div>Total: 0</div> <div><span role=”img” aria-label=”gitar”></span></div> <button>+</button> <button>-</button> </div> ) } |
Listing 3
In deze code heb je JSX gebruikt om de HTML-elementen voor de Product component te maken en te retourneren, met een emoji om het product weer te geven.
Step 2 Implementeer de useState Hook
In dit Product component gaan we twee gegevens bijhouden in de ‘state’: de ‘shopping cart’ en de ‘totale kosten’. Beiden kunnen in de ‘state’ worden opgeslagen met behulp van de useState Hook (zie listing 4).
const [cart, setCart] = useState([]);
const [total, setTotal] = useState(0); |
Listing 4
Wat doet useState?
useState declareert een “state variabele”. Onze state variabelen heten cart en total. Dit is een manier om waarden tussen de functie aanroepen te “bewaren”. Normaal gesproken “verdwijnen” variabelen wanneer een functie wordt afgesloten, maar state variabelen worden bewaard door React.
Wat levert useState op?
Het retourneert een array met de volgende twee waarden:
- de huidige state (de variabele ‘cart’ of ‘total’ bevatten de huidige state)
- en een functie waarmee je deze state variable kunt bijwerken zoals: ‘setCart’.
Wat kan je als argument meegeven aan useState?
Het enige argument dat we door kunnen geven aan useState() is de initiële state. In ons voorbeeld geven we een lege array mee als initiële state voor onze variabele cart. En we geven de waarde 0 mee als initiële state voor onze variabele total.
Wat gebeurt er als de functies setCart of setTotal aangeroepen worden?
Naast het bijwerken van de state variable, zorgen deze functies ervoor dat er een re-render plaatsvindt van dit component, zodra deze aangeroepen worden.
import React, { useState } from ‘react’;
const products = [ { emoji: ‘\uD83C\uDFB8’, name: ‘gitar’, price: 500 }];
export default function Product() { const [cart, setCart] = useState([]); const [total, setTotal] = useState(0);
function add(product) { setCart(current => […current, product.name]); setTotal(current => current + product.price); } return( <div> <div>Shopping Cart: {cart.length} total items</div> <div>Total price: {total}</div> <div> {products.map(product => ( <div key={product.name}> <span role=”img” aria-label={product.name}>{product.emoji}</span> <button onClick={() => add(product)}>+</button> <button>-</button> </div> ))} </div> </div> ) } |
Listing 5
In Listing 5 breiden we onze Product component uit met meerdere producten door een product array te definiëren. In de JSX gebruiken we de .map methode om over deze producten array te itereren en te tonen (zie Product_2.js).
Tevens is er een add-functie gedefinieerd om de shopping cart en de totale kosten te kunnen updaten via de Add-button.
In deze add-functie maken we gebruik van de functies setCart en setTotal die gedefinieerd zijn in de useState hook.
In plaats van het direct doorgeven van het nieuwe product aan de setCart en setTotal functies, wordt er een anonieme functie doorgegeven die de huidige state aanneemt en een nieuwe bijgewerkte waarde retourneert.
Zorg er echter voor dat je de cart-state niet direct muteert. In plaats daarvan kun je het nieuwe product aan de cart-array toevoegen door de huidige cart-array te spreaden (…current) en het nieuwe product aan het einde van deze array toe te voegen. Opmerking: door de spread-operator maak je een nieuwe instantie/clone aan van de cart-array en kan je zonder side effects de mutatie doorvoeren.
Step 3 Implementeer de useReducer Hook
Er is nog een Hook genaamd ’useReducer’ die speciaal is ontworpen om de state bij te werken op een manier die vergelijkbaar is met de .reduce array-methode. De useReducer Hook is vergelijkbaar met useState, maar wanneer je deze Hook initialiseert, geef je een functie door die de Hook uitvoert wanneer je de state samen met de initiële gegevens wijzigt. De functie, ook wel de ’reducer‘ genoemd, heeft twee argumenten: de state en het product.
Refactor de shoppingcart om de useReducer Hook te gebruiken (listing 6). Maak een functie genaamd cartReducer die de state en het product als argumenten gebruikt. Vervang useState door useReducer en geef vervolgens de functie cartReducer door als het eerste argument en een lege array als het tweede argument, wat de initiële state is.
import React, { useReducer, useState } from ‘react’;
function cartReducer(state, product) { return […state, product.name] }
export default function Product() { const [cart, setCart] = useReducer(cartReducer, []); const [total, setTotal] = useState(0);
function add(product) { setCart(product); setTotal(current => current + product.price); }
return(…) } |
listing 6
Voer deze wijziging ook door voor setTotal.
Het eindresultaat van deze eerste refactoring kan je vinden in component Product_3.js.
Nu is het tijd om de ‘remove’ functie toe te voegen. Om dit te implementeren maken we gebruik van een veel voorkomend patroon in reducer-functies om een action-object door te geven als tweede argument. Dit action-object bestaat uit het product en het action-type (listing 7).
function cartReducer(state, action) {
switch(action.type) { case ‘add’: return […state, action.product]; case ‘remove’: const productIndex = state.findIndex( item => item.name === action.product.name); if(productIndex < 0) { return state; } const update = […state]; update.splice(productIndex, 1) return update |
listing 7
Binnen de cartReducer wordt het totaal bijgewerkt op basis van het action-type. In dit geval voeg je producten toe aan de shoppingcart bij action-type: add. En verwijdert je ze bij action-type: remove. De remove actie werkt de state bij door ‘splicing out’ de eerste instantie van het gevonden product uit de cart-array. Middels de spread-operator maken we een copy van de bestaande state/cart-array, zodat we geen last zullen hebben van side effects tijdens het updaten.
Vergeet niet om de final state te retourneren aan het eind van iedere actie.
Na het bijwerken van de cartReducer, maken we een remove-functie aan die de setCart aanroept met een action-object (listing 8). Dit action-object bevat het product en het action-type: remove.
Pas tevens de add functie aan die de setCart aanroept met een action-type: add
En verwijder de bestaande aanroep naar setTotal uit de add functie.
Maak tenslotte een getTotal functie aan die de totaalprijs berekend op basis van de totale cart-state. Hier kan je gebruik maken van de ‘cart.reduce() functie’ (listing 8 en Product_5.js).
const products = [
{ emoji: ‘\uD83C\uDFB8’, name: ‘gitar’, price: 500 }];
function getTotal(cart) { return cart.reduce((totalCost, item) => totalCost + item.price, 0); }
function cartReducer(state, action) { switch(action.type) { case ‘add’: return […state, action.product]; case ‘remove’: const productIndex = state.findIndex( item => item.name === action.product.name); if(productIndex < 0) { return state; } const update = […state]; update.splice(productIndex, 1) return update default: return state; } }
export default function Product() { const [cart, setCart] = useReducer(cartReducer, []);
function add(product) { setCart({ product, type: ‘add’ }); }
function remove(product) { setCart({ product, type: ‘remove’ }); }
return(…) } |
Listing 8
Tenslotte
Er zijn zeker andere manieren om useState en useReducer toe te passen. useState en useReducer worden niet aanbevolen voor het beheren van de state in zeer grote, complexe projecten, houd daar rekening mee.
Naast de hier behandelde hooks biedt React vele andere hooks, evenals de functionaliteit om je eigen custom Hooks te maken. Voorbeelden van andere hooks zijn:
- useEffect, maakt het mogelijk om side effects af te handelen, zoals fetching data. In component js is een useEffect geïmplementeerd om de productgegevens op te halen.
- en useContext , is een manier om de state globaal te onderhouden
BIO:
Peter Eijgermans is Frontend Developer en Codesmith bij Ordina JsRoots. Hij deelt graag zijn kennis met anderen middels workshops en presentaties.