Les 2: Class Components

Vorige les hebben we eenvoudige functie componenten gebouwd, deze componenten hadden enkele belangrijke limitaties. Het was niet mogelijk om state (data) te bewaren in deze componenten en om te reageren op gebruikersacties (events).

Class components zijn één van de mogelijke oplossingen voor dit probleem. Origineel waren class components de enige oplossing, deze zijn dan ook terug te vinden in een oudere React applicaties. Vandaag de dag wordt er regelmatig gekozen voor alternatieven zoals hooks (zie les 8).

Deze les maken we gebruik van de developer tools om de state te inspecteren, via de tutorial kan je meer info vinden over het gebruik van de developer tools.

Startbestanden

State

Het concept state vorm een integraal deel van elke React applicatie. Een gebruikersnaam, wachtwoord, e-mailadres, adres, creditcardnummer, ... Al deze zaken komen zeer veel voor, 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. Ook deze zaken zijn niet te implementeren zonder state.

Concept: State

De state van een React component is een JavaScript object dat de huidige situatie van de component bevat. Elk formulier element, welke menu's uitgeklapt zijn, is het dark theme geactiveerd, data die opgehaald wordt van een API of database, ... zijn slechts enkele voorbeelden van data die bewaard moet worden in de state van een component.

Controlled Components

Een class component is geen functie meer, maar een JavaScript ES6 klasse die de klasse React.Component uitbreid. Omdat een class component overerft van React.Component moet elke class component de methode render implementeren, deze methode geeft verplicht JSX-code terug. Er zijn nog andere methodes uit React.Component die overschreven kunnen worden, deze worden verder in de cursus besproken.

/src/voorbeeld1.js
// React moet geïmporteerd worden anders is het niet mogelijk 
// de klasse Component uit te breiden.
import React from 'react';

export default class Voorbeeld1 extends React.Component {
    style = {
        background: "#2B2B2B",
        borderRadius: 10,
        fontFamily: "Oblique, Verdana, serif, sans-serif",
        color: "#F2F2F2",
        padding: "1em",
        margin: "1em 0"
    }

    /**
     * Dit is de enige verplichte methode in een classe component,
     * geeft verplicht JSX code terug.
     *
     * @return {JSX.Element} De UI die getoond moet worden.
     */
    render = () => {
        return (
            <div style={this.style}>
                <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>
                <div>
                    <input type="text"/>
                </div>
                <div>
                </div>
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Copied!

Bovenstaande code exporteert de component Voorbeeld1, deze kan dan gebruikt worden in index.js.

/src/index.js
import ReactDOM from "react-dom";
import Voorbeeld1 from "./voorbeeld1";

const rootElement = document.getElementById("root");
ReactDOM.render(
    <Voorbeeld1/>,
    rootElement
);
1
2
3
4
5
6
7
8
Copied!

CodeSandboxopen in new window

Deze code produceert een eenvoudig formulier, 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.

Zoals eerder gezegd worden formulierelementen in de state bewaard, via de constructor kunnen we de state een initiële waarde geven. Elke React function component heeft properties, ook class components hebben properties. In de constructor moeten deze properties meegegeven worden als argument, vervolgens moet de constructor van de superklasse (React.Component) opgeroepen worden met de properties als argument.

/src/voorbeeld1.js
constructor(props)
{
    super(props);
}
1
2
3
4
Copied!

De state van een component wordt bewaard als instantie variabele onder de naam state. Omdat de state een variabele is die in de volledige klasse gebruikt moet kunnen worden, en niet enkel in constructor, moeten we this gebruiken om naar de klasse te verwijzen (hetzelfde geldt voor de instantie variabele style). De state wordt bewaard als een JavaScript object.

/src/voorbeeld1.js
constructor(props)
{
    super(props);
    this.state = {
        textValue: "Initiële waarde"
    }
}


 
 
 


1
2
3
4
5
6
7
Copied!

Deze waarde kan tenslotte getoond worden in het formulierveld en in de tweede paragraaf.

/src/voorbeeld1.js
render = () => {
    return (
        <div style={this.style}>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: 
                {this.state.textValue}
            </p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <div>
                <input type="text" 
                       value={this.state.textValue}
                />
            </div>
            <div>
            </div>
        </div>
    )
}





 




 







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Copied!

CodeSandboxopen in new window

Bovenstaande code is een stap in de goede richting, de waarde in het formulierelement kan nu eenvoudig uitgelezen worden, want de waarde is bewaard in this.state.textValue. Er is echter één groot probleem, de waarde kan niet meer aangepast worden. In onderstaande voorbeeld zie je dat React dit ook aangeeft als een fout. We hebben een read-only field gemaakt en we dit inputveld ofwel de property readonly moeten geven of een change handler definiëren.

We voegen een changeHandler toe aan de component door een nieuwe methode te definiëren in de klasse Voorbeeld1. Let op de state mag NOOIT rechtstreeks aangepast worden, behalve in de constructor. Op alle andere locaties in de klasse component moet gebruik gemaakt worden van de setState methode.

/src/voorbeeld1.js
handleChange = (e) => {
    // e bevat het change event
    // e.target is het input element dat het event getriggerd heeft.
    // e.target.attributeName kan gebruikt worden om elk attribuut van het input element op te vragen:
    //      - e.target.value --> De nieuwe waarde in het tekst veld
    //      - e.target.type  --> text
    this.setState({textValue: e.target.value});
}






 

1
2
3
4
5
6
7
8
Copied!

Deze handler kan vervolgens gekoppeld worden aan het change event van het formulierelement.

/src/voorbeeld1.js
render = () => {
    return (
        <div style={this.style}>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: {this.state.textValue}</p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <div>
                <input type="text" value={this.state.textValue}
                       onChange={this.handleChange}
                />
            </div>
            <div>
            </div>
        </div>
    )
}








 







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Copied!

CodeSandboxopen in new window

De component is nu een controlled component, dit betekent dat de formulierelementen in de state bewaard worden en via handler aangepast worden. Het is nu mogelijk om de waarde in het formulierveld aan te passen.

De wijzigingen zijn onmiddellijk zichtbaar in de tweede paragraaf. Telkens de setState methode gebruikt wordt, zal de component en elk van de onderliggende componenten opnieuw gerenderd worden. Omdat de render methode opnieuw uitgevoerd wordt ook de JSX code

/src/voorbeeld1.js
<p>De huidige waarde is nu: {this.state.textValue}</p>
1
Copied!

opnieuw gerenderd en wordt de nieuwe waarde van de state dus uitgelezen.

Properties in een class component

Net zoals voor function components kunnen class components ook properties krijgen. Dit gebeurt op exact dezelfde manier. Stel, de achtergrondkleur moet aanpasbaar zijn. We kunnen dan eenvoudig een property backgroundColor toevoegen aan de component Voorbeeld1 in index.js.

/src/index.js
const rootElement = document.getElementById("root");
ReactDOM.render(
    <Voorbeeld1 backgroundColor="#2B2B2B"/>,
    rootElement
);


 


1
2
3
4
5
Copied!

Vervolgens kunnen we deze property gebruiken in de class component Voorbeeld1. Merk op dat ook hier het keyword this vereist is.

/src/voorbeeld1.js
export default class Voorbeeld1 extends React.Component {
    style = {
        background: this.props.backgroundColor,
        borderRadius: 10,
        fontFamily: "Oblique, Verdana, serif, sans-serif",
        color: "#F2F2F2",
        padding: "1em",
        margin: "1em 0"
    }

    constructor(props) {
        super(props);
        this.state = {
            textValue: "Initiële waarde"
        }
    }

    // Niet relevante code weggelaten.
}


 







 
 







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Copied!

CodeSandboxopen in new window

Events op een lager niveau

Momenteel is het niet moeilijk om een de changeHandler te gebruiken. Maar de formulierelementen bevinden zich niet altijd in de class component. Class components worden zo goed als altijd opgedeeld in kleinere componenten. Deze kleinere componenten kunnen op hun beurt weer opgedeeld zijn in nog kleinere componenten en zo voort tot we enkele niveaus diep aan het formulierelement komen. Dit is nodig omdat de state één niveau hoger moet gedefinieerd worden dan alle andere component die hiervan gebruik maken. Dit omdat de setState functie een re-render moet triggeren van alle componenten die gebruik maken van de state.

Een gelijkaardige situatie kan gesimuleerd worden door een extra inputveld te maken en dit af te zonderen in een nieuwe component. De nieuwe component is, in dit vereenvoudig voorbeeld, niets anders dan een input veld. Natuurlijk moet deze component de waarde van het formulierelement via properties meekrijgen.

/src/voorbeeld1.js
const InputContainer = (props) => {
    return <input type={"text"} value={props.textValue}/>;
}
1
2
3
Copied!

Vervolgens kunnen we deze component gebruiken in de render functie van Voorbeeld1.

/src/voorbeeld1.js
render = () => {
    return (
        <div style={this.style}>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: {this.state.textValue}</p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <div>
                <input type="text" value={this.state.textValue}
                       onChange={this.handleChange}
                />
            </div>
            <div>
                <InputContainer textValue={this.state.textValue}
                                onChange={this.handleChange}/>
            </div>
            <div>
            </div>
        </div>
    )
}












 
 






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Copied!

CodeSandboxopen in new window

Deze code produceert, zoals hieronder te zien, wel twee inputvelden, maar deze zijn niet allebei bruikbaar. Het bovenste veld kan zonder problemen aangepast worden, wijzigingen zijn ook meteen zichtbaar in het tweede veld. Dit is logisch want hier wordt dezelfde setState methode gebruikt als in het vorige voorbeeld, dus wordt de render methode opnieuw opgeroepen en wordt de nieuwe waarde via properties doorgegeven aan de InputContainer.

Het tweede veld kan echter niet aangepast worden. De onChange handler is wel gekoppeld, maar aan het onChange event van de InputContainer, niet aan het onChange event van het formulierelement in de InputContainer.

Dit probleem is op te lossen door de onChange handler via een property door te geven aan de InputContainer. Merk op dat de naam moet veranderen, onChange is gereserveerd voor een event en kan dus niet als property gebruikt worden.

/src/voorbeeld1.js
render = () => {
    return (
        <div style={this.style}>
            <p>Tekst aanpassen is heel eenvoudig!</p>
            <p>De huidige waarde is nu: {this.state.textValue}</p>
            <p>In onderstaand input veld kan je deze waarde aanpassen:</p>
            <div>
                <input type="text" value={this.state.textValue}
                       onChange={this.handleChange}
                />
            </div>
            <div>
                <InputContainer textValue={this.state.textValue}
                                changeHandler={this.handleChange}/>
            </div>
            <div>
            </div>
        </div>
    )
}













 






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Copied!

Deze property kan dan gekoppeld worden aan het onChange event van het formulierelement.

/src/voorbeeld1.js
const InputContainer = (props) => {
    return <input type={"text"} value={props.textValue} 
                  onChange={props.changeHandler}/>;
}


 

1
2
3
4
Copied!

CodeSandboxopen in new window

Dankzij deze extra stap zijn beide formulierelementen nu bruikbaar. Wijzigingen zijn ook onmiddellijk zichtbaar in het andere element en in de tweede paragraaf.

State vs. props

propsState
In function & class componentsEnkel in class components
Immutable, kan niet aangepast wordenMutable, aanpassen via setState of this.state in de constructor

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 efficient of propere manier gedaan, we hebben ook geen rekening gehouden met de performantie van de applicatie.

Map functie

Volgende lijst met vakken wordt gedefinieerd in index.js

Lijst met vakken uit fase 2 van het graduaat programmeren
const subjects = [
    {
        name: "Javascript framework React",
        sp: 5,
        semester: 1,
    },
    {
        name: "Agile en testing",
        sp: 3,
        semester: 1,
    },
    {
        name: "Mobiele applicaties",
        sp: 6,
        semester: 1,
    },
    {
        name: "IT Topics 3",
        sp: 3,
        semester: 1,
    },
    {
        name: "Projecten in het werkveld",
        sp: 12,
        semester: 2,
    },
    {
        name: "Startende professional",
        sp: 18,
        semester: 2,
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Copied!

Deze vakken worden via properties doorgegeven aan Voorbeeld2

/src/index.js
ReactDOM.render(
  <div>
      <Voorbeeld1 backgroundColor="#2B2B2B"/>
      <Voorbeeld2 subjects={subjects}></Voorbeeld2>
  </div>,
  rootElement
);



 



1
2
3
4
5
6
7
Copied!

Voorbeeld2 heeft geen state nodig, dus is een functiecomponent de beste keuze. We kunnen net zoals vorige les, een klassieke lus gebruiken om alle vakken uit te printen.

/src/voorbeeld2.js
const Voorbeeld2 = (props) => {
    // Opmaak weggelaten.

    const output = []
    for (const s of props.subjects) {
        output.push(
            <li>{s.name} ({s.sp} studiepunten - Semester {s.semester})</li>
        );
    }

    return (
        <div style={style}>
            <ul>
                {output}
            </ul>
        </div>
    )
}



 
 
 
 
 
 




 




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Copied!

CodeSandboxopen in new window

Deze code werkt, maar in de meeste gevallen is het properder om de map functie te gebruiken. Deze kan, in tegenstelling tot een for lus, wel gebruikt worden in een JSX-expressie.

/src/voorbeeld2.js
const Voorbeeld2 = (props) => {
    // Opmaak weggelaten.

    return (
        <div style={style}>
            <ul>
                {props.subjects
                    .map(s => <li>{s.name} ({s.sp} studiepunten - Semester {s.semester})</li>)}
            </ul>
        </div>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
Copied!

CodeSandboxopen in new window

Key

Bovenstaande code werkt, maar zoals hieronder te zien is, geeft React een foutmelding. Er wordt gevraagd naar een unieke key. Dit is nodig omwille van hoe de React Shadow DOM werkt. React bepaald welke elementen hertekend moeten worden rendert enkel de hoogstnoodzakelijke opnieuw. Zonder een unieke sleutel heeft React de mogelijk niet om te beslissen welk vak opnieuw gerend moet worden.

De meest voor de hand liggende oplossing is de index in de lijst props.subjects 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 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 app gelijk blijft, mag je de index gebruiken.

Een betere optie zijn de ids van de elementen die in de lijst zitten. Als de data uit een relationele database komt, is dit geen enkel probleem, je gebruikt dan de primary key. Data uit een document database heeft GUID dat gebruikt kan worden. Om deze laatste situatie te simuleren kunnen we de uuidopen in new window bibliotheek gebruiken.

npm install uuid --save
1
Copied!

De data wordt dan

Lijst met vakken uit fase 2 van het graduaat programmeren met UUID
const subjects = [
    {
        name: 'Javascript framework React',
        sp: 5,
        semester: 1,
        guid: uuidv4(),
    },
    {
        name: 'Agile en testing',
        sp: 3,
        semester: 1,
        guid: uuidv4(),
    },
    {
        name: 'Mobiele applicaties',
        sp: 6,
        semester: 1,
        guid: uuidv4(),
    },
    {
        name: 'IT Topics 3',
        sp: 3,
        semester: 1,
        guid: uuidv4(),
    },
    {
        name: 'Projecten in het werkveld',
        sp: 12,
        semester: 2,
        guid: uuidv4(),
    },
    {
        name: 'Startende professional',
        sp: 18,
        semester: 2,
        guid: uuidv4(),
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Copied!

Het nieuwe uuid kan vervolgens toegevoegd worden aan de list items via het key attribuut.

/src/voorbeeld2.js
const Voorbeeld2 = (props) => {
    // Opmaak weggelaten.
    
    return (
    <div style={style}>
      <ul>
        {props.subjects
          .map(s => <li key={s.guid}>
              {s.name} ({s.sp} studiepunten - Semester {s.semester})
          </li>)}
      </ul>
    </div>
  )
}







 






1
2
3
4
5
6
7
8
9
10
11
12
13
14
Copied!

CodeSandboxopen in new window

Volledig uitgewerkte lesvoorbeelden met commentaar

Last Updated: 2/2/2022, 11:17:35 AM
Contributors: Sebastiaan Henau