Les 2: State

Sebastiaan HenauOngeveer 11 minuten

Les 2: State

Vorige les hebben we eenvoudige functie componenten gebouwd, deze componenten hadden enkele belangrijke beperkingen. Het was niet mogelijk om state (data) te bewaren in deze componenten en te reageren op gebruikersacties (events). Deze les bekijken we hoe we dit kunnen toevoegen aan een component.

We maken gebruik van de developer tools om de state te inspecteren, we raden je aan de tutorial te doorlopen om vertrouwd te raken met de React developer tools.

Startbestandenopen in new window

Local state

Het concept state vormt een integraal deel van elke React applicatie. Een gebruikersnaam, wachtwoord, e-mailadres, adres, creditcardnummer, ... zijn enkele voorbeelden die frequent voorkomen. Formulieren zijn niet weg te denken uit een moderne webapp. Daarnaast bevat elke webapp ook knoppen, uitklapbare menu's, ... Componenten die reageren op acties van een gebruiker.

Begrip: Local state

De state van een React component is een verzameling van variabelen die de huidige situatie van de component bevatten.

Zo worden formulier elementen, de uit- of dichtgeklapte menu's, de themakeuze, data opgehaald van een API of database, ... bewaard in de state van een component.

Controlled Components

We beginnen met een eenvoudig formulier waar momenteel nog geen nieuwigheden inzitten, de tekst in het input veld kan aangepast worden, maar er gebeurt verder niets mee, we kunnen deze waarde nog op geen enkele manier uitlezen.

const FormContainer = styled.div`
    /* Weggelaten */
`

const Example1: FunctionComponent = () => {
    return (
        <FormContainer>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: </p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <p>De formulierwaarde is keer gewijzigd!</p>
            <div>
                <input type="text"/>
            </div>
            <div>
            </div>
        </FormContainer>
    )
}

Zoals eerder gezegd worden formulierelementen in de state bewaard, om deze state te definiëren moeten we gebruik maken van een hook.

Begrip: Hook

Een hook is een herbruikbare functie die bovenaan in een component opgeroepen wordt en een bepaalde actie uitvoert. Dit kan gaan van het bewaren van UI-state tot het synchroniseren met een externe databron en nog veel meer. We zullen hooks doorheen deze cursus onder anderen gebruiken om state te bewaren, rechtstreeks te communiceren met de dom, data op te halen van een API, te navigeren tussen meerdere pagina's in onze applicatie, en nog heel wat meer.

Een hook moeten verplicht bovenaan een functie component geplaatst worden en mag nooit voorkomen in:

  • Een conditioneel statement
  • Een lus
  • Een geneste functie

Begrip: useState

De useStateopen in new window hook wordt gebruikt om state toe te voegen aan een React component. De useState functie krijgt een initiële waarde als argument en geeft een array met 2 elementen terug. Het eerste element is de huidige waarde van de state, het tweede element is een setter functie die gebruikt kan worden om de state aan te passen.

In de meeste gevallen kan TypeScript het type van de data in de state afleiden (inferred type), maar in sommige gevallen is dit niet mogelijk. In het laatste geval kan je het type uitdrukkelijk meegeven via de generische parameter.

import {useState} from 'react'

const ExampleComponent: FunctionComponent = () => {
    const [stateValue, setStateValue] = useState<string>('Een default waarde')
    
    return (
        <div>
            {stateValue}
            <button onclick={() => setStateValue((new Date()).toISOString())}>
                Verander de state
            </button>
        </div>
    )
}

In onderstaand voorbeeld wordt slechts één useState hook gebruikt, in een realistische applicatie kunnen dit er natuurlijk een pak meer zijn.

import {useState, FunctionComponent} from 'react'

const Example1: FunctionComponent = () => {
    const [text, setText] = useState<string>('Initiële waarde')

    return (
        <FormContainer>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: </p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <p>De formulierwaarde is  keer gewijzigd!</p>
            <div>
                <input type="text"/>
            </div>
            <div>
            </div>
        </FormContainer>
    )
}
 


 















Deconstructing syntax

In bovenstaand voorbeeld maken we gebruik van deconstructing syntaxopen in new window om de elementen die teruggegeven worden door de useState hook een naam te geven. Volgende twee fragmenten zijn identiek.

Het is duidelijk dat de tweede optie veel meer code vereist en minder duidelijk is, maak dus zoveel mogelijk gebruik van deconstructing syntax. Niet enkel voor de useState hook, maar ook in alle andere code.

const Example1: FunctionComponent = () => {
    const [text, setText] = useState('Initiële waarde')

    return (
        <FormContainer>
            ...
        </FormContainer>
    )
}

We gebruiken het eerste element van de state-array twee keer. De eerste keer printen we het uit in een <p> tag. Vervolgens koppelen we de waarde van de state aan het formulierelement, hiervoor wordt het value attribuut gebruikt.

const Example1: FunctionComponent = () => {
    const [text, setText] = useState<string>('Initiële waarde')

    return (
        <FormContainer>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: {text}</p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <p>De formulierwaarde is  keer gewijzigd!</p>
            <div>
                <input type="text" value={text}/>
            </div>
            <div>
            </div>
        </FormContainer>
    )
}






 



 






State bijwerken

Het is duidelijk dat deze applicatie nog niet heel zinvol is. We hebben een variabele aangemaakt waarin we de huidige waarde van het formulier bijhouden, maar we doen hier nog niets mee. Zoals onderstaande video demonstreert, is het onmogelijk om de state aan te passen, we hebben een read-only veld gebouwd. De enige manier om de state aan te passen is via de React dev-tools.

Figuur 1: Read-only form element

React detecteert deze fout ook, als we de development server starten en in de console kijken, zien we volgende foutmelding:

Warning: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly.

Vervangen

Zoals bovenstaande foutmelding zegt, moeten we het onChange attribuut gebruiken om de state aan te passen als de gebruiker het formulier probeert aan te passen.

Merk op dat we de setter gebruiken, de state mag nooit rechtstreeks aangepast worden. Als je dit probeert, zal je merken dat de wijzigingen nog steeds niet zichtbaar worden. Via de setter laat je aan React weten dat de state gewijzigd is en dat de component (en alle kinderen ervan) opnieuw gerenderd moet worden.

const Example1: FunctionComponent = () => {
    const [text, setText] = useState<string>('Initiële waarde')

    return (
        <FormContainer>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: {text}</p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <p>De formulierwaarde is  keer gewijzigd!</p>
            <div>
                <input type="text" value={text}
                       onChange={evt => setText(evt.currentTarget.value)}/>
            </div>
            <div>
            </div>
        </FormContainer>
    )
}











 






evt.currentTarget.value
  • evt bevat het change event
  • evt.currentTarget is het inputelement dat het event getriggerd heeft.
  • evt.target.attributeName kan gebruikt worden om elk attribuut van het inputelement op te vragen:
    • evt.target.value --> De nieuwe waarde in het tekstveld
    • evt.target.type --> text

De component Example1 is nu een controlled component.

Begrip: Controlled component

Een controlled component is een component waarin de formuliergegevens door React beheerd worden. Dit betekent dat er voor elk formulierelement een corresponderende useState hook is.

Controlled components staan tegenover uncontrolled components, i.e. componenten die de formuliergegevens laten beheren door de DOM, door de browser. In dit soort componenten worden de formuliergegevens niet bewaard in de state van een component, maar in de DOM. Dit betekent dat je in zo'n situatie, bij het inzenden van een formulier alle formuliergegevens moet uitlezen via het React equivalent van een document.getElementById.

React heeft dan geen controle meer, dit maakt het bijvoorbeeld moeilijker om tijdens het ingeven van data aan validatie te doen. Verder is het idee achter React dat alles een functie is van de state, alles zou uit de state berekend moeten worden, dit betekent dat een uncontrolled component dus tegen de principes van React ingaat.

Alhoewel we hierboven enkel over formulierelementen gesproken hebben, kan het concept eenvoudig uitgebreid worden naar ander onderdelen van het UI zoals tabs, carousels, accordions, ...

Op basis van de vorige state

Stel, we willen bijhouden hoeveel keer het formulier gewijzigd is, hiervoor hebben we een nieuwe useState hook nodig. We passen ook de onChange listener op het formulier aan naar een aparte methode omdat de logica te veel wordt om op een duidelijke manier in de JSX-code te schrijven.

Begrip: TypeScript voor event handlers

React exporteert een aantal event die gebruikt kunnen worden om de signatuur van een event handler te definiëren. Elk van deze events is generisch wat betekent dat we er een hier aan moeten meegeven welk soort element het event getriggerd heeft. Daarnaast exporteert React ook types die gebruikt kunnen worden om een event handler in één keer te definiëren in de plaats van via de parameters, ook deze functies zijn generisch.

In de meeste gevallen is een event handler de beste keuze, soms heb je naast het event echter ook een extra parameter nodig, in dat geval gebruik je de individuele event interfaces.

In deze cursus gebruiken we bijna uitsluitend de onderstaande elementen als type parameter voor de events of event handlers die door React geëxporteerd worden.

  • HTMLElement (superklasse voor alle tag)
  • HTMLFormElement (<form>)
  • HTMLInputElement (<input/>)
  • HTMLSelectElement (<select>)
  • HTMLTextAreaElement (<textarea>)

Hieronder volgt een lijst van de belangrijkste events die door React geëxporteerd worden en enkele frequent gebruikte signaturen voor event handlers. Voor meer informatie verwijzen we naar React TypeScript Cheatsheetopen in new window.

import {
    ChangeEvent,        // Event voor wijzigingen in een formulier element.
    FocusEvent,         // Element krijgt of verliest focus.
    FormEvent,          // Formulier wordt ingestuurd.
    MouseEvent,         // De gebruiker klikt ergens op.
} from 'react'

const handleChangeEvent = (evt: ChangeEvent<HTMLInputElement>): void => {
    
}

const handleFocusEvent = (evt: FocusEvent<HTMLElement>): void => {
    
}

const handleFormEvent = (evt: FormEvent<HTMLFormElement>): void => {
    
}

const handleMouseEvent = (evt: MouseEvent<HTMLElement>): void => {
    
}

Om het probleem te illustreren verhogen we hier de state 2 keer met 0.5. Op het eerste zicht kan dit artificieel lijken, maar dit illustreert het probleem duidelijk. Het probleem kan in een echte applicatie voorkomen omwille van optimalisaties die React op de achtergrond uitvoert.

const Example1: FunctionComponent = () => {
    const [text, setText] = useState<string>('Initiële waarde')
    const [changeCount, setChangeCount] = useState<number>(0)

    const changeHandler: ChangeEventHandler<HTMLInputElement> = (evt) => {
        setText(evt.currentTarget.value)
        setChangeCount(changeCount + 0.5)
        setChangeCount(changeCount + 0.5)
    }

    return (
        <FormContainer>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: {text}</p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <p>De formulierwaarde is {changeCount} keer gewijzigd!</p>
            <div>
                <input type="text" value={text}
                       onChange={changeHandler}/>
            </div>
            <div>
            </div>
        </FormContainer>
    )
}


 

 
 
 
 
 






 

 







Zoals in onderstaande video gedemonstreerd wordt, wordt de teller na elke wijziging met .5 verhoogt.

Figuur 2: Batched state updates

De reden voor dit onverwacht gedrag is dat als de eventHandler opgeroepen wordt, de waarde van changeCount vast ligt. De twee useState calls worden gebundeld door React (om het aantal re-renders te beperken). Op het moment dat er twee calls gebeuren naar setChangeCount methode, kan de bewerking in de parameter uitgerekend worden, we krijgen dan (in het geval dat de component in zijn eerste render zit), de parameter 0+0.5=0.50 + 0.5 = 0.5 voor beide calls van setChangeCount. Vervolgens worden de methodes op de execution queue geplaatst als:

setChangeCount(0.5)
setChangeCount(0.5)

Dit is natuurlijk geen slechte zaak, als React dit niet deed, zou de component 3 keer gerenderd worden voor één change event.

React voert op de achtergrond soms gelijkaardige optimalisaties uit waarin verschillende state-updates op hetzelfde moment uitgevoerd worden, ook als niet onmiddellijk duidelijk is dat dit kan gebeuren. Gebruik daarom steeds de functionele setter als je de state aanpast op basis van de huidige waarde.

const Example1: FunctionComponent = () => {
    const [text, setText] = useState<string>('Initiële waarde')
    const [changeCount, setChangeCount] = useState<number>(0)

    const changeHandler: ChangeEventHandler<HTMLInputElement> = (evt) => {
        setText(evt.currentTarget.value)
        setChangeCount(oldCount => oldCount + 0.5)
        setChangeCount(oldCount => oldCount + 0.5)
    }

    return (
        <FormContainer>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: {text}</p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <p>De formulierwaarde is {changeCount} keer gewijzigd!</p>
            <div>
                <input type="text" value={text}
                       onChange={changeHandler}/>
            </div>
            <div>
            </div>
        </FormContainer>
    )
}


 

 
 
 
 
 






 

 







Begrip: State updaten

De useState hook geeft, als tweede argument een setter terug, deze kan gebruikt worden om de state aan te passen en heeft 2 vormen.

In het eerste geval heeft de setState methode onderstaande signatuur en kan er dus een nieuwe waarde meegegeven worden die de huidige waarde overschrijft.

setState<T> = (newValue: T): void

In het tweede geval neemt de setState methode een functie als argument, deze functie krijgt de huidige waarde van de state als argument en berekend hieruit de nieuwe waarde. Gebruik deze vorm altijd als je de state aanpast op basis van de huidige waarde. De signatuur van de setter is in dit geval:

setState<T> = ((oldValue: T) => T): void
import {useState} from 'react'

const ExampleComponent: FunctionComponent = () => {
    const [stateValue, setStateValue] = useState<string>('Een default waarde')

    return (
        <div>
            {stateValue}
            <button onclick={() => setStateValue((new Date()).toISOString())}>
                Verander de state
            </button>
        </div>
    )
}

Arrays in JSX

Vorige les hebben we al lussen gebruikt om arrays om te vormen naar een reeks componenten. We hebben dit echter niet op de meest propere of efficiënte manier gedaan, we hebben ook geen rekening gehouden met de performantie van de applicatie.

Volgende lijst met vakken wordt gedefinieerd in /src/data/subjects.ts

Lijst met vakken uit fase 2 van het graduaat programmeren (dagopleiding 2022-2023)
import {ISubject} from '../models/ISubject.ts'

const subjects: ISubject[] = [
    {
        name: 'Javascript framework React',
        sp: 5,
        semester: 1,
        id: '6203412b-14f1-466a-a561-a33e4f4311d6',
        goals: [
            {
                goal: 'De student ontwikkelt een Single Page Application in React',
                id: 'db7173cd-c23a-4cd3-b731-bce96b069d5b',
            },
            {
                goal: "De student kan state management toepassen om data uit te wisselen tussen verschillende pagina's in een Single Page Application",
                id: 'de38eeaf-889c-48e2-b6d9-4d6dba7889fa',
            },
            {
                goal: 'De student kan een SPA opbouwen aan de hand van componenten',
                id: '7cb62148-d404-421e-96eb-db60ca90db7b',
            },
            {
                goal: 'De student kan APIs aanspreken vanuit React en deze gebruiken in een Single Page Application',
                id: 'fa3a0f7f-ab35-4741-9a9b-9e9fabd51177',
            },
        ],
    },
    {
        name: 'Agile en testing',
        sp: 3,
        semester: 1,
        id: 'a2af7e33-f198-44fb-8cc0-e34d27301834',
        goals: [
            {
                goal: 'De student identificeert wat Agile en Lean is',
                id: 'cb0d8804-eb8c-472a-92b9-eccd561e2560',
            },
            {
                goal: 'Kent de de Agile methode en kan dit toepassen in een projectwerk',
                id: 'eaa981e4-277f-42fe-a69f-1cef2206d016',
            },
            {
                goal: 'Kent de meeste voorkomende testvormen binnen softwareontwikkeling en weet wanneer en hoe deze toegepast worden',
                id: '179be1ee-384e-46e1-a69b-ae8c49e6d263',
            },
            {
                goal: 'Kan testscenario’s schrijven en toepassen op een projectwerk',
                id: '9d86cf79-8805-4dee-9d5f-802736f77392',
            },
        ],
    },
    {
        name: 'Mobiele applicaties',
        sp: 6,
        semester: 1,
        id: '476e4ee2-feea-482d-a840-2a1bf2380a0a',
        goals: [
            {
                goal: 'De student kan een mobiele hybrid-webview applicatie schrijven',
                id: 'adc72c96-aa32-4e90-9fb8-c0cd1d55ac28',
            },
            {
                goal: 'De student kan een hybrid-webview applicatie publiceren als Android applicatie',
                id: 'ff9a35f4-6884-4fb8-9f06-2adbda3c2319',
            },
            {
                goal: 'De student kan een mobiele applicatie aanbieden als progressive web app (PWA)',
                id: '9dbca9e8-2784-490a-9226-1d7aafabd557',
            },
            {
                goal: 'De student kan gebruik maken van een back-end-as-a-service',
                id: '5ba6f1d4-1905-472d-9eb5-3b81fb245602',
            },
            {
                goal: 'De student kan communiceren met een API',
                id: '38ac59ed-1c47-4fc8-935e-66013e97763b',
            },
            {
                goal: 'De student kan gebruikmaken van functies voorzien door het mobiele besturingssysteem (iOS of Android)',
                id: 'd8cb8e4e-37c9-4d2a-9100-84493bdd7395',
            },
        ],
    },
    {
        name: 'IT Topics',
        sp: 3,
        semester: 1,
        id: 'acd9dd00-e8a1-4346-ab31-9a53b5b4a3c3',
        goals: [
            {
                goal: 'De student volgt nieuwe ontwikkelingen in IT',
                id: 'b232fa84-bb9b-454c-a09d-8c01cbedc2da',
            },
            {
                goal: 'Kan een project planning in een projecttool interpreteren.',
                id: '9def09ce-e363-47f0-95ee-cc6c746094c9',
            },
        ],
    },
]

export default ISubjects

Deze vakken worden geïmporteerd en via properties doorgegeven aan Example2. We kunnen dan, net zoals in de vorige les, een klassieke lus gebruiken om alle vakken uit te printen. De component Subject is voorzien in de startbestanden.

import ISubjects from './data/subjects.ts'

root.render(
  <StrictMode>
    <Example1/>
    <Example2 ISubjects={ISubjects}/>
  </StrictMode>
)





 


Key

Bovenstaande code werkt, maar als je de developer console opent, zie je dat React een waarschuwing geeft.

Warning: Each child in a list should have a unique "key" prop.

Er wordt gevraagd naar een unieke key. Dit is nodig omwille van hoe React bepaald welke elementen opnieuw gerenderd moeten worden. Zonder een unieke sleutel heeft React de mogelijkheid niet om te beslissen welk vak opnieuw gerenderd moet worden, i.e. welk vak gewijzigd is ten opzichte van de laatste render, dus zal alles opnieuw gerenderd moeten worden.

De meest voor de hand liggende oplossing is de index in de subjects array te gebruiken, dit is echter geen goed idee. De volgorde van elementen in een lijst kan veranderen, bijvoorbeeld omdat de lijst anders gesorteerd wordt of omdat er een element verwijderd wordt. De index gebruiken kan rare bugs als gevolg hebben en moet zoveel mogelijk vermeden worden. Enkel als je absoluut zeker bent dat de lijst voor de volledige levensduur van je component gelijk blijft, kan je de index gebruiken.

Een betere optie zijn de ids van de elementen die in de lijst zitten. Als de data uit een database komt, is dit geen enkel probleem, je gebruikt dan de primary key (relationeel) of het object-id (document). Om dit te simuleren is een ID toegevoegd in de subjects array.

Het ID wordt meegegeven via de key property die op elke component beschikbaar is.

const Example2: FunctionComponent<Example2Props> = ({subjects}) => {

  const output = []
  for (const subject of subjects) {
    output.push(<Subject {...subject} key={subject.id}/>)
  }

  return (
    <>
      {output}
    </>
  )
}




 








Map functie

In de voorbeelden hierboven is een klassieke for-each lus gebruikt. We kunnen echter ook gebruik maken van een functionele aanpak via de Array.map functie. In tegenstelling tot bovenstaande lus kan deze functie wel in JSX-code gebruikt worden.

Omdat deze functie rechtstreeks in JSX-code gebruikt kan worden, leidt deze dikwijls tot kortere en overzichtelijkere code. Als de hoeveelheid JSX-code die gegenereerd wordt in de lus groot begint te worden, is het daarentegen dikwijls properder om deze in een klassieke lus te zetten of om de map functie buiten de return expressie te plaatsen. Zo blijft de code beter leesbaar.

const Subject: FunctionComponent<ISubject> = ({name, sp, semester, goals}) => {
  return (
      <div className="subject">
          <div className="subtitle">
              <div>{name}</div>
              {sp} studiepunten - semester {semester}
          </div>
          <div className="content">
              <ul>
                  {goals.map(g => <li key={g.id}>{g.goal}</li>)}
              </ul>
          </div>
      </div>
  )
}









 





Composition vs. inheritance

We bouwen een Accordion en AccordionItem component, elk item kan open of dichtgeklapt worden. Vervolgens plaatsen we de individuele Subject componenten in een AccordionItem component zodat we de informatie van een vak kunnen dicht- of openklappen. Aangezien de UI aangepast moet worden op basis van user-interactie (open- en dichtklappen), moet er state toegevoegd worden aan de component waarin de user-interactie plaatsvindt.

Begrip: Inheritance vs Composition

Er zijn twee mogelijke manieren om componenten te bouwen, inheritance en composition.

De eerste optie, inheritance is de eenvoudigste, componenten worden in elkaar genest en properties worden van boven naar onder doorgegeven. Stel je wil een accordion bouwen die informatie bevat over verschillende programmeertalen, dan kan je de functionaliteit om open of dicht te klappen toevoegen aan de Language component die informatie over één programmeertaal weergeeft.

Bovenstaande structuur werkt, maar is niet ideaal. Omdat de logica voor het open- en dichtklappen in de Language component bewaard wordt, is de accordion niet herbruikbaar. Als we echter een aparte Accordion en AccordionItem component voorzien, kunnen we de logica verplaatsen naar de Accordion of AccordionItem component, afhankelijk van wat we precies willen bereiken. Deze techniek wordt composition genoemd.

We kunnen de Language component meegeven als kind aan de AccordionItem component, op deze manier staan de Accordion en AccordionItem componenten volledig los van de inhoud van de accordion.

De structuur en de bijhorende (pseudo)code voor beide opties, ziet er als volgt uit. De ingekleurde componenten worden via de children property doorgegeven.

interface AccordionItemProps {
    children: ReactNode
    title: string
}

const AccordionItem: FunctionComponent<AccordionItemProps> = ({children, title}) => {
    const [isOpen, setIsOpen] = useState<boolean>(false)

    return (
        <div className={'accordion-item'}>
            <div className={'title'}>{title}</div>
            <div className={'chevron'} onClick={() => setIsOpen(x => !x)}>
                {isOpen ? <button>&and;</button> : <button>&or;</button>}
            </div>
            <div className={'content'}>
                {isOpen && children}
            </div>
        </div>
    )
}






 




 
 


 




Lifting State

Momenteel zijn de Accordion en AccordionItem componenten niet heel complex, maar wat als we slechts één item tegelijkertijd willen openen? Als we op een ander item klikken, moet dit item opengaan en moeten alle andere items gesloten worden. Hieronder wordt de huidige componentenboom gevisualiseerd.

Als we slechts één van de items tegelijkertijd kunnen openen, moet er gecommuniceerd worden tussen de verschillende instanties van de AccordionItem componenten. Item 11 moet weten wanneer item NN open is en omgekeerd. Deze communicatie is echter niet mogelijk in React, hiervoor moeten we steeds via een bovenliggende component (Accordion) gaan, zo kunnen properties doorgegeven worden aan de gemeenschappelijke kinderen. Deze properties bevatten dan zowel de huidige waarde van de state als functies waarmee deze aangepast kan worden. De bovenliggende component dient als gemeenschappelijke data-bron voor alle kinderen.

Begrip: Lifting state

Als twee of meer componenten dezelfde data nodig hebben, moet deze data bewaard worden in de dichtstbijzijnde gemeenschappelijke ouder. Dit fenomeen wordt lifting state genoemd.

Context

We gebruiken we de Accordion component om informatie te bewaren, maar om een echte herbruikbare accordion component te bouwen, hebben we context nodig. Dit is een manier om data te delen in een (stuk) van de componentenboom zonder dat deze data via properties doorgegeven moet worden. In dit geval kunnen we geen properties gebruiken omdat de Accordion en AccordionItem componenten met compositie in het achterhoofd gebouwd zijn, wat betekent dat de Accordion component properties niet rechtstreeks kan doorgeven aan de AccordionItem component.

Begrip: Context

Contextopen in new window is een manier om prop-drilling te voorkomen, een property kan diep in de componentenboom gebruikt worden, zonder dat de properties door de volledige boom doorgegeven moeten worden.

Hierdoor is context ideaal om dingen te bewaren die nauwelijks of niet aangepast worden, zoals bijvoorbeeld de taalkeuze, themakeuze, munteenheid, ...

Om context te definiëren gebruik je de createContext functie, deze functie heeft één argument, de standaardwaarde. Deze standaardwaarde wordt gebruikt als de context geconsumeerd wordt in een component die geen parent heeft waarin een specifiekere waarde gedefinieerd is.

import {createContext} from 'react'

interface ISomeContext {
    // De properties van de context.
}

const SomeContext = createContext<ISomeContext>(defaultValue)

In dit geval moet de context twee dingen bevatten, een sleutel of ID waarmee het actieve AccordionItem aangeduid wordt en een functie waarmee deze sleutel aangepast kan worden.

import {createContext} from 'react'

interface IAccordionContext {
    currentOpenKey: string | undefined
    toggleOpenKey: (newOpenKey: string | undefined) => void
}

export const AccordionContext = createContext<IAccordionContext>({
    currentOpenKey: undefined,
    toggleOpenKey: (): void => {
        console.warn('An implementation for this method has not been provided.')
    },
})

Provider

De standaardwaarde, die we hierboven gedefinieerd hebben, is niet echt zinvol om rechtstreeks te gebruiken. Het actieve item is vastgezet op undefined en de functie om dit te wijzigen is nog niet geïmplementeerd. Via een provider kunnen we de standaard waardes overschrijven met zinvolle waardes.

Begrip: Provider

Voor elke context moet er minstens één provider voorzien worden. De provider wordt rond alle componenten geplaatst die gebruik maken van de context. Een provider biedt een waarde aan voor een context, aan al de kinderen van deze context. Als, in de bovenliggende componentenboom, meerdere providers beschikbaar zijn voor een bepaalde context, dan wordt de dichtstbijzijnde provider gebruikt.

import {createContext, FunctionComponent} from 'react'

interface ISomeContext {
    // De properties van de context.
}

const SomeContext = createContext<ISomeContext>(defaultValue)

const SomeComponent: FunctionComponent = () => {

    return (
        <SomeContext.Provider value={foo}>
            {/* Children can use the provided value */}
        </SomeContext.Provider>
    )
}

We voegen dus een provider toe aan de Accordion component, omdat de provider een toggleOpenKey property heeft, waarmee de currentOpenKey property aangepast kan worden, moeten we state gebruiken. Zoals eerder vermeld is state nodig voor elke user-interactie die de UI aanpast. Merk op dat de props van de Accordion component aangepast zijn, we verwachten nu niet enkel kinderen, maar eventueel ook een sleutel die het tabblad aanduidt dat standaard open moet zijn. Deze property wordt vervolgens gebruikt om de state te initialiseren.

interface AccordionProps {
    children: ReactNode
    defaultOpenKey?: string
}

const Accordion: FunctionComponent<AccordionProps> = ({children, defaultOpenKey}) => {
    const [currentOpenKey, setCurrentOpenKey] = useState<string | undefined>(defaultOpenKey)

    const toggleOpenKey = (newOpenKey: string | undefined) => {
        if (currentOpenKey === newOpenKey) {
            setCurrentOpenKey(undefined)
        } else {
            setCurrentOpenKey(newOpenKey)
        }
    }

    return (
        <AccordionContext.Provider value={{currentOpenKey, toggleOpenKey}}>
            <div className={'accordion'}>
                {children}
            </div>
        </AccordionContext.Provider>
    )
}


 



 










 

 




useContext

Begrip: useContext

De useContextopen in new window hook kan gebruikt worden om een bestaande context te consumeren. De waarde die teruggegeven wordt door deze hook is die van de dichtstbijzijnde provider, als er geen provider is wordt de default waarde gebruikt.

const SomeComponent: FunctionComponent = () => {
    const contextValue = useContext(SomeContext)

    return <>{/* ... */}</>
}

Via de useContext hook kunnen we de waarde van de context uitlezen in de AccordionItem componenten. Als we dan ook nog een property toevoegen waarmee we een key doorgeven die het item identificeert, is het mogelijk om de accordion volledig herbruikbaar te maken. Natuurlijk moeten we ook de juiste keys meegeven in Example2.

interface AccordionItemProps {
    children: ReactNode
    title: string
    openKey: string
}

const AccordionItem: FunctionComponent<AccordionItemProps> = ({children, title, openKey}) => {
    const {currentOpenKey, toggleOpenKey} = useContext(AccordionContext)
    const isOpen = openKey === currentOpenKey

    return (
        <div className={'accordion-item'}>
            <div className={'title'}>{title}</div>
            <div className={'chevron'} onClick={() => toggleOpenKey(openKey)}>
                {isOpen ? <button>&and;</button> : <button>&or;</button>}
            </div>
            <div className={'content'}>
                {isOpen && children}
            </div>
        </div>
    )
}



 



 
 




 
 


 




Voorbeeldcode

Volledig uitgewerkte lesvoorbeelden met commentaaropen in new window

Laatst geüpdate:
Bijdragers: Sebastiaan Henau