Begrippenlijst
Begrippenlijst
Deze pagina bevat een overzicht van alle begrippen/concepten in de JavaScript vakken. Alles wat op deze pagina te vinden is, is ook terug te vinden in de lesteksten. Deze pagina is bedoeld om snel iets op te zoeken over de belangrijkste zaken terug te overlopen als voorbereiding op een examen en niet als exclusief studeermateriaal. We raden iedereen aan om de lessen bij te wonen en de volledige lesteksten te lezen.
Filters
JavaScript
Frontend
Backend
Mobile
Begrippen
Next.js gebruikt een file-based routing systeem, de App Router. Dit betekent dat de bestandsstructuur van je project bepaalt welke routes er beschikbaar zijn in de applicatie.
Elke subfolder van de /src/app directory wordt een nieuw segment in de URL. Binnen deze mappen moet een bestand met de naam page.tsx aanwezig zijn dat via een default export een React component exporteert.
Op het bovenste niveau van de routing-structuur moet ook een layout route aanwezig zijn.
Elke component die event handlers of een hook bevat moet aangeduid worden als een client component. Ondanks de naam wordt een client component nog steeds op de server gerenderd, het verschil met een server component is dat een client component gehydrateerd wordt op de client en een server component niet.
Een component kan omgevormd worden tot een client component door bovenaan, in het bestand dat de component definieert, het 'use client' directive toe te voegen. Elk kind van een client component wordt automatisch ook een client component.
Als je toch een server component wilt meegeven als kind aan een client component moet er gebruik gemaakt worden van composition.
'use client'
const ClientComponent: FunctionComponent = () => {
const [count, setCount] = useState<number>(0)
return (
<>
<button onClick={() => setCount(x => x + 1)}>{counter}</button>
</>
)
}Een applicatie die gebruikt maakt van Server Side Rendering (SSR) stuurt een React component naar de client als HTML.
Zodra de client (browser) de HTML gerenderd heeft, doorloopt de pagina het hydration process. Dit process zorgt ervoor dat React gekoppeld wordt aan de DOM-elementen die de server gestuurd heeft. React neemt hierbij dus de controle van de DOM-elementen over en koppelt eventuele event handlers.
Het is belangrijk dat de data die door de server gestuurd wordt overeenkomt komt met de data die op de client gerenderd wordt tijdens het hydration process. Als dat niet zo is, kan dit leiden tot hydration errors. Foutieve geneste tags zoals p > button en bepaalde functies zoals Date.toLocaleString() zijn enkele voorbeelden van zaken die zulke errors kunnen veroorzaken.
In de meeste gevallen is het eenvoudig om hydration errors op te lossen, maar in sommige gevallen is het moeilijk of zelfs onmogelijk. De Date.toLocaleString() functie is een voorbeeld van een onvermijdelijke hydration error omdat deze functie afhankelijk is van de taalinstellingen van de server (waar de initiële HTML-code gegenereerd wordt) en de client (waar de functie opnieuw opgeroepen wordt tijdens het hydration process). Omdat de taalinstellingen waarschijnlijk verschillen tussen server en client, is de datum anders geformatteerd en wordt er een foutmelding gegenereerd.
In dit geval kan de booleaanse suppressHydrationWarning property toegevoegd worden aan de directe parent van het HTML-tag waarin de fout zich voordoet. Zo wordt de foutmelding verborgen. Dit is natuurlijk een escape hatch die je best zo weinig mogelijk gebruikt.
Een layout route is een bestand met de naam layout.tsx dat een gedeelde layout bevat voor alle onderliggende pagina's. Dit betekent dat de layout in /src/app/layout.tsx toegepast wordt op alle pagina's in de applicatie. Er kan maximaal layout per subdirectory van de /src/app folder toegevoegd worden.
Layout routes worden niet herladen als de gebruiker navigeert tussen de verschillende geneste pagina's die onder dezelfde layout route vallen. Voor de root-layout betekent dit dus dat deze nooit herladen wordt, tenzij deze pagina expliciet geïnvalideerd wordt.
De layout route op het root niveau moet een geldig HTML5 document bevatten, inclusief de <html>, <head> en <body> tags. Onderstaande code toont een minimaal voorbeeld.
const RootLayout: FunctionComponent<PropsWithChildren> = ({children}) => {
return (
<html>
<head>
<title>My App</title>
</head>
<body>{children}</body>
</html>
)
}
export default RootLayoutIn de Next app router kan voor elke page.tsx een loading.tsx toegevoegd worden. Terwijl page.tsx data aan het laden is, wordt de inhoud van loading.tsx getoond via Suspense.
Net zoals een layout file wordt een loading file toegepast op alle kindroutes.
Logging gebeurd aan de hand van verschillende niveaus. Pino ondersteund standaard onderstaande log-levels:
fatal: Een kritieke fout waardoor de applicatie onmiddellijk stopt of onbruikbaar wordt.error: Ernstige fouten die een functie of proces laten mislukken, maar de applicatie kan meestal blijven draaien.warn: Waarschuwingen voor mogelijke problemen of ongewenst gedrag, maar niet direct fataal.info: Algemene informatie over de normale werking van de applicatie (bijv. een succesvolle start).debug: Gedetailleerde meldingen bedoeld voor ontwikkelaars om problemen te analyseren.trace: Zeer uitgebreide logging die elke stap of actie bijhoudt, vooral nuttig bij diepgaand onderzoek.silent: Er wordt niets gelogd.
Elke pagina of layout-route in Next.js kan metadata definiëren die gebruikt wordt om de titel, description en andere onderdelen van de pagina aan te passen. Sommige van de gegevens die hierin geconfigureerd worden kunnen meegenomen worden door Google en andere zoekmachines.
De metadata wordt geëxporteerd als een named export in een pagina of layout-route. Als dit object geëxporteerd wordt uit de root-layout moet deze layout-route geen <head> tag bevatten omdat dit tag automatisch gegenereerd wordt door Next.js op basis van de metadata.
import {Metadata} from 'next'
export const metadata: Metadata = {
title: 'My search engine optimized website.',
description: 'A Next.js website with metadata.',
}Proxy functies zijn functies die toegang hebben tot het request en response object en dit op een bepaalde manier manipuleren. In Next wordt de proxy uitgevoerd voor elk request, zowel voor pagina's in de applicatie, als voor statische files en API-routes.
Middleware moet gedefinieerd worden in /src/proxy.ts en enkel in deze file. Als je een complexe proxy functies hebt, en deze wilt opsplitsen in verschillende files, moet je de code importeren en oproepen in /src/proxy.ts als onderdeel van de proxy functie.
import {NextResponse} from 'next/server'
import type {NextRequest} from 'next/server'
export function proxy(request: NextRequest): NextResponse {
// Nieuw response object aanmaken.
const response = NextResponse.next()
// Doe verder niet voor statisch files die intern door Next gebruikt worden.
if (request.nextUrl.pathname.startsWith('/_next')) return response
// Algemene proxy code
if (request.nextUrl.pathname.startsWith('/about')) {
// Conditionele proxy code
}
// Geef het response object terug, inclusief eventuele modificaties.
return response
}De redirect functie wordt gebruikt om de gebruiker door te sturen naar een andere pagina en kan gebruikt worden in een server component, route handler of server action.
Let op, er zijn verschillende versies van de redirect functie, gebruik de import uit next/navigation.
De redirect functie werkt intern door een error te gooien, je mag deze functie dus nooit in een try-catch gebruiken.
import { redirect } from 'next/navigation'
export async function action() {
'use server'
// Doe iets op de server
redirect('/login')
}import { redirect } from 'next/navigation'
const ServerComponent: FunctionComponent = async () => {
const profile = await fetchProfile()
if (!profile) {
redirect('/login')
}
return <></>
}Next houdt een cache bij van de pagina's die al bezocht zijn door de gebruiker tijdens de actieve sessie. Na een mutation is het mogelijk dat de data op één of meerdere pagina's verouderd is en opnieuw opgehaald moet worden.
Via de revalidatePath functie kunnen we aangeven dat één of meer paden op de website gerevalideerd moeten worden.
De functie heeft twee parameters. De eerste en enige verplichte parameter is een string die het pad aangeeft dat gerevalideerd moet worden. De tweede en optionele parameter heeft twee mogelijke waarden, 'page' en 'layout', die respectievelijk aangeven of je een pagina of layout wilt revalideren. Als je een layout revalideert worden alle kinderen ook gerevalideerd op het volgende verzoek naar die URL. Als je een pagina revalideert wordt alleen die pagina gerevalideerd en blijft de omringende layout ongewijzigd.
import {revalidatePath} from 'next/cache'
export async function action() {
'use server'
// Doe iets op de server
revalidatePath('/profile')
}
export async function action2() {
'use server'
// Doe iets op de server
// Revalideer alle pagina's in de webapplicatie.
revalidatePath('/', 'layout')
}Server actions zijn server functions die bedoeld zijn om aan een formulier gekoppeld te worden via het action attribuut.
Er zijn twee soorten server actions te onderscheiden. De eerste soort heeft onderstaande structuur en kan rechtstreeks aan een formulier gekoppeld worden via het action attribuut.
(formData: FormData) => Promise<T>We gebruiken de eerste vorm niet in deze cursus. De tweede vorm heeft onderstaande signatuur en is bedoeld om via de useActionState hook aan formulieren te koppelen.
<T,>(prevState: T, formData: FormData) => Promise<T>De prevState parameter moet van hetzelfde type zijn als de return waarde van de actie en wordt gebruikt als we een formulier verschillende keren achter elkaar inzenden.
De formData parameter is een object dat alle waarden van het formulier bevat, dit object wordt automatisch opgebouwd door de browser als een formulier ingezonden wordt (dit is een standaard feature van HTML). Elk formulier element met een name attribuut wordt in dit object opgenomen (onder de naam die meegegeven is aan het name attribuut).
React Server Components (RSC) zijn componenten die enkel op de server uitgevoerd kunnen worden en eventueel asynchroon zijn. Omdat een server component op de server uitgevoerd wordt, kan zo'n component gebruikt worden om een database of bestandssysteem aan te spreken voordat er iets naar de client gestuurd wordt. Hierdoor zijn er minder HTTP request nodig dan bij een SPA, het ophalen van de HTML en de data gebeurt in één keer in de plaats van in twee aparte stappen.
Omdat een RSC enkel op de server uitgevoerd wordt, is het niet mogelijk om hooks zoals useState of useEffect te gebruiken, deze zijn namelijk afhankelijk van de client/de acties van een gebruiker en steunen eventueel op browser API's die natuurlijk niet beschikbaar zijn op de server.
De JSX-code die gegenereerd wordt in een server component moet serialiseerbaar zijn. Dit betekent dat het onmogelijk is om functies (event handlers) te koppelen in een server component. Serialisatiefuncties zoals JSON.stringify verwijderen functies namelijk uit objecten als deze geconverteerd worden naar een JSON-bestand.
Dit betekent niet dat we geen functies kunnen oproepen of gebruiken in een RSC. Zolang deze niet gekoppeld worden aan een JSX-element, maar gebruikt worden om te bepalen welke JSX-code er gerenderd moet worden is er geen probleem.
Binnen Next.js is elke component per default een server component, een component met hooks of event handlers moet expliciet aangeduide worden als een client component via het 'use client' directive.
Server functions zijn asynchrone functies (op de server) die automatisch geconverteerd worden naar HTTP POST endpoints. Deze endpoints worden volledig op de achtergrond aangemaakt en aangesproken, alles wat we moeten doen is de functie oproepen op de client, Next en React doen de rest.
Een functie wordt geconverteerd naar een server function door het 'use server' directive toe te voegen bovenaan de functie. Als een bestand niets anders bevat dan server functions, dan kan je de hele file markeren door 'use server' toe te voegen op de eerste lijn van het bestand.
Een server functions kan een serialiseerbaar JavaScript object teruggeven (inclusief React componenten) of kan gebruikt worden om de gebruiker te redirecten of om bepaalde layouts of pagina's te refreshen.
export async function someServerFunction() {
'use server'
// Doe iets op de server.
}'use server'
export async function someServerFunction() {
// Doe iets op de server.
}
export async function someOtherServerFunction() {
// Doe iets op de server.
}
export async function aThridServerFunction() {
// Doe iets op de server.
}De useActionState hook wordt gebruikt om server actions te koppelen aan formulieren en het resultaat van de action te verwerken.
Deze hook neemt een server action en een initiële state als argument. Het type van de initiële state moet overeenkomen met het type van de return waarde van de form action. De waarde van de initiële state (het tweede argument van de useActionState hook) wordt, via de prevState parameter meegegeven aan de form action. Dit gebeurt natuurlijk enkel de eerste keer dat je de form action oproept want daarna wordt de initiële state overschreven door de returnwaarde van de form action.
De hook geeft een triple terug:
- Het eerste element bevat de huidige state van de form action. Als de action nog nooit ingezonden is, is dit de initiële state. Als de action al wel ingezonden is, is dit de return waarde van de form action.
- Het tweede element is een wrapper rond de form action die doorgegeven kan worden aan de action property van een formulier.
- De laatste waarde is een boolean die aangeeft of de form action momenteel uitgevoerd wordt.
import {FunctionalComponent} from 'react'
import {useActionState} from 'react';
async function increment(previousState: number, formData: FormData): Promise<number> {
'use server'
return previousState + formData.get('incrementCount')
}
const formExample: FunctionComponent = () => {
const [number, formAction, isPending] = useActionState(increment, 0);
return (
<>
<h1>The form has been submitted {number} times</h1>
<form action={formAction}>
<input name="incermentCount" type="number"/>
<button type="submit" disabled={isPending}>Increment</button>
</form>
</>
)
}De useFormStatus hook kan gebruikt worden om informatie op te halen over de laatste keer dat het formulier ingezonden werd.
De hook moet als kind van een formulier opgeroepen worden, je moet dus een nieuwe component gebruiken en kunt de useFormStatus hook niet oproepen in de component waar je het formulier definieert.
import { useFormStatus } from 'react-dom'
const SubmitButton: FunctionComponent = () => {
const { pending, data, method, action } = useFormStatus()
return (
<button type="submit" disabled={pending}>Submit</button>
)
}
const Form: FunctionComponent = () => {
return (
<form action={someAction}>
<SubmitButton />
</form>
)
}De useTransition wordt gebruikt om een server function op te roepen.
const [isPending, startTansition] = useTransition()De isPending variabele geeft aan of de server action nog bezig is, de startTransition functie neemt een server function als argument en roept deze op. Als je verschillende transities start voordat de eerste afgerond is, worden de state updated die het gevolg zijn van de transitie geannuleerd. Pas als de laatste transitie afgerond is, worden de state updates doorgevoerd.
Soms zijn de interfaces die Prisma genereert niet voldoende, deze interfaces beschrijven namelijk één volledige tabel. Als je een query schrijft die slechts een deel van een tabel (DTO) ophaalt, of die data uit meerdere tabellen combineert moet je hiervoor een interface voorzien.
Aangezien het aantal mogelijke combinaties ontzettend groot is, kan Prisma de interfaces niet voorzien. Enerzijds omdat er anders zodanig veel interfaces zouden zijn dat intellisense traag wordt, anderzijds omdat de naamgeving van deze interfaces niet deterministisch is en dus niet geautomatiseerd kan worden. In plaats daarvan voorzien Prisma helpers om deze interfaces te genereren.
Dit process bestaat uit twee stappen:
- Definieer een object dat aan de select, omit of include properties van de Prisma-methodes meegegeven kan
worden en valideer deze via dePrisma.TableInclude,Prisma.TableOmit,Prisma.TableSelectinterfaces, hier wordt table natuurlijk vervangen met de naam van de tabel. - Gebruik het object dat hierboven gedefinieerd is om een nieuw type te generen, hiervoor gebruik je
Prisma.TableGetPayload<{include: typeof objectUitPunt1}>,Prisma.TableGetPayload<{omit: typeof objectUitPunt1}>ofPrisma.TableGetPayload<{select: typeof objectUitPunt1}>.
import type {Prisma} from '@/generated/prisma/clinet'
export const fooWithBarInclude = {
bar: {
select: {
column1: true,
column2: true,
},
},
} satisfies Prisma.FooInclude
export type FooWithBar = Prisma.FooGetPayload<{include: typeof fooWithBarInclude}>Via de create en createMany methodes worden respectievelijk één of meerdere rijen aangemaakt.
// create geeft de ingevoegde rij terug.
const newFooRow = prisma.foo.create({
data: {
// ... Data
},
})// createMany geeft de ingevoegde rijen NIET terug.
const {count: numberOfInsertedRows} = prisma.foo.createMany({
data: [
{
// ... Data
},
{
// ... Data
},
],
})// createManyAndReturn geeft de ingevoegde rijen terug.
const newFooRows = prisma.foo.createManyAndReturn({
data: [
{
// ... Data
},
{
// ... Data
},
],
})Via de create methode kan niet alleen een rij aan een tabel toegevoegd worden, maar kunnen ook relaties tussen verschillende tabellen gelegd worden.
De methode heeft een parameter met dezelfde naam als de relation property in het Prisma schema. Deze parameter is een object dat ofwel een create of connect property bevat. Via de create property kan een nieuwe rij in de gerelateerde tabel aangemaakt worden. Aan de connect property moet een id van een bestaande rij in de gerelateerde tabel meegegeven worden.
const newFooRow = prisma.foo.create({
data: {
// ... Data
relationProperty: {
create: {
// ... Data voor de gerelateerde tabel
}
}
},
})const newFooRow = prisma.foo.create({
data: {
// ... Data
relationProperty: {
connect: {
id: 1
}
}
},
})Via de delete en deleteMany methodes kunnen we respectievelijk één of meerdere rijen verwijderen.
const deletedFooRow = prisma.foo.delete({
where: { someUniqueProperty: 'someValue'},
})// Verwijder specifieke rijen
const {count: numberOfDeletedRows} = prisma.foo.deleteMany({
where: { someUniqueProperty: 'someValue'},
})
// Verwijder alle rijen.
const {count: numberOfDeletedRowsAll} = prisma.foo.deleteMany({})De Prisma API (hier gebruikt met de betekening "verzameling methodes die aangeboden worden door een library"), is grotendeels dynamisch opgebouwd op basis van het schema.
Telkens er een migration uitgevoerd wordt, of het pnpm prisma generate commando gebruikt wordt, wordt de Prisma API opnieuw opgebouwd.
Omdat de API dynamisch opgebouwd is, bevat elke instantie van PrismaClient properties die overeenkomen met de tabellen in de database.
const newFooRow = prisma.foo.someCrudMethod(...)Prisma bevat verschillende methodes om data uit te lezen, er zijn methodes beschikbaar om één of meerdere rijen op te halen en om deze te filteren.
Filteren gebeurd via de where parameter en kan op volgende manieren:
- Zoek op exacte matches voor één of meerdere kolommen, hierbij moet voldaan zijn aan alle voorwaarden:
{ someColumn: 'someValue', anotherColumn: 'anotherValue' } - Filteren via een ingebouwde filtering operatoren zoals equals, contains, gt, lt, in, startsWith, endsWith, notIn, ... Hier moet eveneens voldaan zijn aan alle voorwaarden:
{ someColumn: { notIn: ['x', 'y', 'z'] }, anotherColumn: { gt: 10 } } - Combineren van meerdere filters met AND of OR:
{ OR: [{ someColumn: 'someValue' }, { anotherColumn: {gte: 10 }] }
Het is optioneel mogelijk om aan te geven welke kolommen teruggegeven worden via de select parameter. Zodra de select parameter meegegeven is, worden enkel de opgegeven kolommen opgehaald uit de database.
Via de optionele omit parameter wordt aangeven welke kolommen niet teruggegeven moeten worden. Omdat select en omit elkaar uitsluiten, kan slechts één van beide parameters tegelijkertijd gebruikt worden.
const oneFoo = prisma.foo.findUnique({
where: { someUniqueProperty: 'someValue'},
// Select is optioneel, als je dit meegeeft moet je alle
// properties meegeven die je nodig hebt.
// Als de parameter ontbreekt worden alle kolommen teruggeven.
select: {
// ...
},
// Omit is optioneel, alle kolommen die hierin vermeld worden,
// worden niet teruggegeven door Prisma.
// NIET te combineren met select.
omit: {
// ...
}
})
const oneFoo2 = prisma.foo.findUniqueOrThrow({
where: { someUniqueProperty: 'someValue'},
select: {}, // Optioneel
omit: {}, // Optioneel, niet te combineren met select.
})
const firstMatchingFoo = prisma.findFirst({
where: {}, // Optioneel
select: {}, // Optioneel
omit: {}, // Optioneel, niet te combineren met select.
})
const allMatchingFoos = prisma.findMany({
where: {}, // Optioneel
select: {}, // Optioneel
omit: {}, // Optioneel, niet te combineren met select.
orderBy: { // Optioneel
someColumn: 'asc', // 'desc' is ook geldig.
},
})De find-methodes kunnen gebruikt worden om informatie over een gerelateerde tabel op te halen, dit kan één of meerdere niveaus (relaties) diep gaan.
const oneFoo = prisma.foo.findUnique({
where: { someUniqueProperty: 'someValue'},
include: {
relation: {
relation: true
}
}
})const oneFoo2 = prisma.foo.findUniqueOrThrow({
where: { someUniqueProperty: 'someValue'},
include: {
relation: {
select: {
column1: true,
column2: true,
}
}
}
})const firstMatchingFoo = prisma.findFirst({
where: {},
select: {
column1: true,
column2: true,
relation: {
column3: true,
}
},
})Via de update en updateMany methodes kunnen we respectievelijk één of meerdere rijen aanpassen.
const newFooRow = prisma.foo.update({
where: { someUniqueProperty: 'someValue'},
data: {
// ... Data
},
select: {}, // Optioneel
})const {count: numberOfUpdatedRows} = prisma.foo.updateMany({
where: { someProperty: 'someValue'},
data: {
// ... Data
},
})Via de experimentele typedSql optie ondersteund Prisma de mogelijkheid om zelf SQL queries te schrijven. Alhoewel dit meestal niet nodig is, kan het soms een nuttige escape hatch zijn als je dingen moet doen zoals
- gemiddelde scores bepalen op basis van verschillende reviews
- data moet aanpassen op basis van de waarde van een bestaande kolom
- complexe queries moet schrijven/optimaliseren die via een ORM te traag zouden zijn
Prisma genereert TypeScript definities voor de SQL-query, de parameters en het return-type (dat altijd een array is, ook als er slechts één rij teruggegeven wordt).
CORS (Cross-Origin Resource Sharing) is een beveiligingsmechanisme dat bepaalt hoe webapplicaties in een domein (origin) kunnen communiceren met bronnen die gehost worden op een ander domein. Via HTTP-headers geeft een server expliciet aan welke origins toegang hebben tot specifieke bronnen. Wanneer een HTTP-request afkomstig is van een andere origin dan de server, wordt het door de browser geblokkeerd, tenzij de server expliciet toestemming geeft via de juiste CORS-headers.
CORS is specifiek gericht op verzoeken die vanuit een browser worden geïnitieerd via JavaScript (fetch). Dit betekent dat CORS niet gebruikt wordt bij verzoeken die uitgevoerd worden buiten een browseromgeving, zoals verzoeken vanaf een Node.js server, desktopapplicatie, native mobiele applicatie (geschreven in Kotlin of Swift) of een andere server-side applicatie.
Vanuit beveiligingsoogpunt blokkeert een browser standaard elk HTTP-verzoek dat wordt gedaan naar een andere origin. Dit betekent dat wanneer een Single Page Application (SPA) probeert om gegevens op te halen van een externe API, de browser deze verzoeken zal blokkeren, tenzij de server expliciet toestemming heeft verleend via de juiste CORS-instellingen. Deze aanpak helpt om aanvallen zoals Cross-Site Request Forgery (CSRF) te voorkomen. Kwaadwillenden kunnen dan niet zomaar vanuit een andere website (of via een kwaadaardige extensie of bookmark) JavaScript-code uitvoeren om ongeautoriseerde HTTP-verzoeken te versturen en gevoelige gegevens te stelen.
Stel dat CORS niet bestond en dat een gebruiker in tabblad A is ingelogd op een bankwebsite en in tabblad B een kwaadaardige website bezoekt. De kwaadaardige website kan dan HTTP-requests versturen naar de bankwebsite, alsof de gebruiker deze verzoeken zelf heeft gestuurd. Omdat er geen CORS is, en omdat de browser cookies automatisch meestuurt bij elk request naar de bankwebsite, kan de kwaadwillende dus overschrijvingen uitvoeren op de bankrekening van de gebruiker. CORS helpt om dit soort aanvallen te voorkomen, het is natuurlijk nog steeds mogelijk dat een kwaadwillende JavaScript code injecteert in een website en de requests uitvoert vanuit het banktabblad, waarbij CORS niet van toepassing is. CORS is dus slechts een klein, maar belangrijk, deel van de puzzel.
Natuurlijk zijn er API's die zonder problemen publiek beschikbaar gesteld kunnen worden, met name read-only API's die het weerbericht, nieuws, of andere openbare informatie ophalen. Voor deze API's moeten de Access-Control-Allow-Origin header ingesteld worden. Hier kunnen meerdere origins aan meegegeven worden, de origin * kan gebruikt worden om de route overal beschikbaar te maken.
Als een HTTP Request een side-effect heeft, i.e. als er data aangepast wordt op de server (PUT, DELETE, POST (enkel als het content type niet application/x-www-form-urlencoded, multipart/form-data of text/plain is)), wordt er een preflight request uitgevoerd.
Het preflight request wordt verstuurd voor het echte request. De browser vraagt via de HTTP OPTIONS methode aan de server of het PUT, DELETE of POST request uitgevoerd mag worden. Als de server de juiste headers teruggeeft, wordt het echte request verstuurd, anders wordt het geblokkeerd.
Een preflight request moet minstens twee headers teruggeven, de eerste header Access-Control-Allow-Origin wordt ook gebruikt voor GET en POST requests. De tweede header is Access-Control-Allow-Methods, hiermee wordt aangegeven welke HTTP-methodes toegestaan zijn voor een bepaald endpoint.
Naast deze twee verplichte headers zijn er ook verschillende optionele:
- Access-Control-Allow-Headers: De headers die toegestaan zijn in het PUT, DELETE of POST request.
- Access-Control-Allow-Credentials: Of credentials (cookies of de Authorization header) toegestaan zijn (true/false).
- Access-Control-Expose-Headers: De headers die door de client uitgelezen mogen worden in JavaScript code.
- Access-Control-Max-Age: Geeft aan hoeveel seconden het resultaat van een preflight request bewaard mag worden door de browser.
Route handlers zijn bestanden die niet omgevormd worden naar een pagina in een Next.js applicatie, maar bedoeld zijn om programmatorisch op te roepen.
Een route handler kan eender waar in de app router geplaatst worden. De enige voorwaarde is dat de route handler in een bestand met de naam route.ts gedefinieerd wordt.
In dit bestand moet een functie geëxporteerd worden per HTTP-methode, de methodes krijgen een NextRequest object als parameter en geven een NextResponse object terug.
import type {NextRequest} from 'next/server'
import {NextResponse} from 'next/server'
export async function GET(request: NextRequest): Promise<NextResponse> {}
export async function POST(request: NextRequest): Promise<NextResponse> {}
export async function PUT(request: NextRequest): Promise<NextResponse> {}
export async function PATCH(request: NextRequest): Promise<NextResponse> {}
export async function DELETE(request: NextRequest): Promise<NextResponse> {}
export async function HEAD(request: NextRequest): Promise<NextResponse> {}
export async function OPTIONS(request: NextRequest): Promise<NextResponse> {}Pepper is een willekeurig genereerde string die met het wachtwoord en salt gecombineerd wordt, maar die gelijk is voor elke gebruiker.
De pepper wordt niet bewaard in de database omdat deze, in tegenstelling tot salt, geheim is. Pepper wordt best bewaard in een hardware security module (HSM).
Een salt is een willekeurige string die uniek is voor elk wachtwoord in de database. Door deze string te concateneren met het wachtwoord voordat dit gehasht wordt, wordt er voor twee gebruikers die hetzelfde wachtwoord gekozen hebben een andere hash bewaard in de database.
Naast deze obfuscatie heeft salt nog een ander bijzonder groot voordeel. Omdat elk wachtwoord gehasht is met een andere salt, wordt het onmogelijk om een rainbow table op te stellen. Dit is een tabel waar de hashes van veel voorkomende wachtwoorden teruggevonden kunnen worden, zo kunnen kwaadwilligen zeer snel inloggen op verschillende accounts als ze toegang gekregen hebben tot User tabel.
Omdat de salt uniek is per gebruiker, mag deze in de database bewaard worden. Zelfs als een hacker hier toegang tot krijgt, is dit niet erg, want er zijn zodanig veel mogelijke wachtwoorden dat het computationeel onmogelijk is om het wachtwoord te raden. Om het wachtwoord te bepalen zou de hacker alle mogelijke wachtwoorden moeten hashen met de salt van de gebruiker en dit voor elke gebruiker opnieuw doen.
De protectedApiRoute en publicApiRoute functies, die aangeboden wordt door de backend-docenten, zijn functies die alle gemeenschappelijke logica voor een API route afzonderen.
Deze functies controleren de ingestuurde data met een Zod schema, geven eventuele foutmeldingen terug, voeren een opgegeven functie met backend code uit en logt dit proces. Voor de protected variant wordt ook gecontroleerd of de gebruiker ingelogd is en eventueel of deze de juiste rol heeft. Deze controle kan zowel stateful als stateless uitgevoerd worden, via een secure HTTP-only session cookie of via een bearer token in de authentication header.
De apiRoute functies hebben één parameter object met volgende properties:
schema: Een Zod schema dat de invoer valideert (uit het request body of de query parameters)routeFn: Een functie die een void, NextResponse teruggeeft (al dan niet in een Promise) en backend-code uitvoert. routeFn heeft twee parameter objecten met volgende properties:- Parameter 1: Context
data: De gevalideerde data.profile: Het profiel van de huidige gebruiker. Enkel beschikbaar in de protectedApiRoute variant.logger: Een instantie van de Pino logger met requestId, userId, ...
- Parameter 2: Route parameters
- Een object met de parameters die gedefinieerd worden als onderdeel van het pad in de URL.
- Parameter 1: Context
type: Een optionele parameter die de databron aanduidt, er zijn drie mogelijke opties:body: Data wordt ingestuurd als een JSON-object in de body van een POST, PUT of PATCH request. Dit is de defaultwaarde.form: Data wordt ingestuurd als een form encoded data in de body van een POST, PUT of PATCH request.searchParams: Data wordt ingestuurd als searchparameters in de URL van het request.
authenticationType: Een optionele parameter die aangeeft hoe de gebruiker geauthenticeerd moet worden, de mogelijke opties zijn:jwt: Gebruik een JWT token die doorgegeven wordt als een bearer token in de authentication header. Dit is de defaultwaarde.cookie: Gebruik een secure HTTP-only session cookie met een JWT token als value.
requiredRoles: Een optionele array van de gebruikers rollen die toegang hebben tot de functie. Als de array niet meegegeven wordt, heeft elke gebruiker toegang. Enkel beschikbaar in de protectedFormAction variant.
// Een API route waarvoor de gebruiker INGELOGD moet zijn (via een JWT in de headers).
export const GET = protectedApiRoute({
routeFn: async ({data, profile, logger}) => {
// Database operaties en andere backend logica.
},
}
// Een API route waarvoor de gebruiker INGELOGD moet zijn (via een session cookie).
export const GET = protectedApiRoute({
routeFn: async ({data, profile, logger}) => {
// Database operaties en andere backend logica.
},
authenticationType: 'cookie',
}
// Een action waarvoor de gebruiker INGELOGD moet zijn (via een JWT in de headers) en de Admin rol moet hebben.
// Daarnaast moet er data ingestuurd worden via een JSON object in de body van het request en moet er een route parameter
// meegegeven worden.
export const PUT = protectedApiRoute({
schema: zodSchema,
routeFn: async ({data, profile, logger}, {fooId}: {fooId: string}) => {
// Database operaties en andere backend logica.
},
requiredRoles: [Role.Admin],
}
// Een action waarvoor de gebruiker NIET ingelogd moet zijn.
export const GET = = publicApiRoute({
routeFn: async ({data, logger}) => {
// Database operaties en andere backend logica.
},
}De protectedFormAction en publicFormAction functies, die aangeboden wordt door de backend-docenten, zijn functie die alle gemeenschappelijke logica voor een formAction afzonderen.
Deze functies converteren FormData naar een object (inclusief geneste properties en arrays), valideren de data vervolgens met een Zod schema en voeren tenslotte een opgegeven functie met backend-logica uit. Voor de protected variant wordt ook gecontroleerd of de gebruiker ingelogd is en eventueel of deze de juiste rol heeft. Tussen al deze stappen worden log statements toegevoegd.
Tijdens al deze acties worden eventuele foutmeldingen opgevangen en teruggegeven als {errors: {errors: ['...']}, submittedData: {...}} voor globale fouten of {errors: {field: ['...'],}} voor Zod-validatiefouten.
De functie geeft een nieuwe functie terug die onderstaande signatuur heeft:
(prevData: ServerFunctionResponse, formData: FormData): Promise<ServerFunctionResponse>Met andere woorden een functie die opgeroepen kan worden via de useActionState hook.
De formAction functies hebben één parameter object met volgende properties:
schema: Een Zod schema dat de invoer valideert.serverFn: Een functie die een ServerFunctionResponse object of void teruggeeft en backend-code uitvoert. Als serverFn een void functie is en geen fouten vertoond, geeft de formAction functie {success: true} terug. Als serverFn geen void functie is, wordt het resultaat van fn teruggegeven. serverFn heeft één parameter object met volgende properties:data: De gevalideerde data.profile: Het profiel van de huidige gebruiker. Enkel beschikbaar in de protectedFormAction variant.logger: Een instantie van de Pino logger met requestId, userId, ...
globalErrorMessage: Een optionele foutboodschap die teruggegeven wordt in result.error.error in geval er zich een interne serverfout voordoet. Wordt standaard op 'Something went wrong, please ensure you are logged in and try again' gezet.functionName: Een optionele naam voor de functie die gebruikt wordt tijdens het loggen. Wordt standaard op 'Server function' gezet.requiredRoles: Een optionele array van de gebruikers rollen die toegang hebben tot de functie. Als de array niet meegegeven wordt, heeft elke gebruiker toegang. Enkel beschikbaar in de protectedFormAction variant.
// Een action waarvoor de gebruiker INGELOGD moet zijn.
export const createFoo = protectedFormAction({
schema: zodSchema,
serverFn: async ({data, profile}) => {
// Database operaties en andere backend logica.
},
functionName: 'Create foo action',
})
// Een action waarvoor de gebruiker INGELOGD moet zijn en de Admin rol moet hebben.
export const createFoo = protectedFormAction({
schema: zodSchema,
serverFn: async ({data, profile}) => {
// Database operaties en andere backend logica.
},
functionName: 'Create foo action',
requiredRoles: [Role.Admin],
})
// Een action waarvoor de gebruiker NIET ingelogd moet zijn.
export const createFoo = publicFormAction({
schema: zodSchema,
serverFn: async ({data}) => {
// Database operaties en andere backend logica.
},
functionName: 'Create foo action',
})De Form component is een component die gebruikt wordt om formulieren te bouwen die react-hook-form en server actions gebruiken. Deze component is niet standaard beschikbaar, maar wordt aangereikt door de backend-docenten.
De Form component heeft twee optionele en twee verplichte properties:
hookForm: Het resultaat van de useForm hook.action: De form action die uitgevoerd moet worden, i.e. het tweede element in de returnwaarde van de useFormState hook.actionResult: Het resultaat van de form action, i.e. de returnwaarde van de form action, het eerste element in de returnwaarde van de useFormState hook. Deze optionele property wordt gebruikt om de form action te resetten na een succesvolle submit, als deze property ontbreekt, werkt het submitten nog steeds, maar wordt het formulier niet automatisch leeg gemaakt als de form action mislukt is.id: Een optionele property die gebruikt kan worden om een id veld toe te voegen aan het formulier. Het veld wordt automatisch geregistreerd bij react-hook-form en kan gebruikt worden voor update formulieren.
import {z} from 'zod'
import {useForm} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
const zodSchema = z.object({
example: z.string()
})
const Component: FunctionComponent = () => {
const [action, actionResult] = useFormState(someFormAction, {success: false});
const hookForm = useForm<z.infer<typeof zodSchema>>({
defaulValues: {example: ''},
resolver: zodResolver(zodSchema)
});
return (
<Form hookForm={hookForm} action={createTag} actionResult={actionResult}>
<input {...register('example')} />
<button type="submit">Submit</button>
</Form>
);
};De protectedServerFunction en publicServerAction functies, die aangeboden wordt door de backend-docenten, zijn functies die alle gemeenschappelijke logica voor een server functie afzonderen.
De parameters van deze functie zijn dezelfde als voor de formAction functie, maar het returntype is anders:
type ServerFunction<Schema extends ZodType> = (data: z.infer<Schema>) => Promise<void>Deze functie controleren de ingestuurde data met een Zod schema, geven eventuele foutmeldingen terug, voeren een opgegeven functie met backend code uit en logt dit proces. Voor de protected variant wordt ook gecontroleerd of de gebruiker ingelogd is en eventueel of deze de juiste rol heeft.
De serverFunction functies hebben één parameter object met volgende properties:
schema: Een Zod schema dat de invoer valideert.serverFn: Een functie die een void teruggeeft en backend-code uitvoert. serverFn heeft één parameter object met volgende properties:data: De gevalideerde data.profile: Het profiel van de huidige gebruiker. Enkel beschikbaar in de protectedFormAction variant.logger: Een instantie van de Pino logger met requestId, userId, ...
globalErrorMessage: Een optionele foutboodschap die teruggegeven wordt in result.error.error in geval er zich een interne serverfout voordoet. Wordt standaard op 'Something went wrong, please ensure you are logged in and try again' gezet.functionName: Een optionele naam voor de functie die gebruikt wordt tijdens het loggen. Wordt standaard op 'Server function' gezet.requiredRoles: Een optionele array van de gebruikers rollen die toegang hebben tot de functie. Als de array niet meegegeven wordt, heeft elke gebruiker toegang. Enkel beschikbaar in de protectedFormAction variant.
// Een server function waarvoor de gebruiker INGELOGD moet zijn.
export const deleteFoo = protectedServerFunction({
schema: zodSchema,
serverFn: async ({data, profile}) => {
// Database operaties en andere backend logica.
},
functionName: 'Delete foo server function',
}
// Een action waarvoor de gebruiker INGELOGD moet zijn en de Admin rol moet hebben.
export const deleteFoo = protectedServerFunction({
schema: zodSchema,
serverFn: async ({data, profile}) => {
// Database operaties en andere backend logica.
},
functionName: 'Delete foo server function',
requiredRoles: [Role.Admin],
}
// Een action waarvoor de gebruiker NIET ingelogd moet zijn.
export const deleteFoo = = publicServerFunction({
schema: zodSchema,
serverFn: async ({data}) => {
// Database operaties en andere backend logica.
},
functionName: 'Delete foo server function',
}De useForm hook is een hook die gebruikt wordt om formulieren te beheren met React Hook Form.
import {z} from 'zod'
import {useForm} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
// Een Zod schema dat de invoer valideert.
const zodSchema = z.object({
example: z.string()
})
const Component: FunctionComponent = () => {
const {
// Een functie die de inputnaam als argument neemt
// en de nodige properties toevoegt aan een input element.
register,
// Een functie die de invoer controleert en als
// alles correct is een functie die de data verwerkt.
handleSubmit,
// Een functie die het formulier terugdraait naar
// de defaultValues.
reset,
// Een object dat gebruikt kan worden om de formulierwaarden
// aan te passen.
values,
// Een object dat aan andere hooks doorgegeven moet worden.
control,
// Informatie over het formulier, zoals de defaultwaarden, de
// validatieerrors, de gevalideerde velden, ...
formState,
} = useForm<z.infer<typeof zodSchema>>({
defaulValues: {example: ''},
resolver: zodResolver(zodSchema)
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('example')} />
<button type="submit">Submit</button>
</form>
);
};De useZodValidatedForm hook is een hook die gebruikt wordt om de code die voor elk formulier geschreven moet worden af te zonderen en te herbruiken. Deze hook is niet standaard beschikbaar, maar wordt aangereikt door de backend-docenten.
De hook heeft drie parameters:
schema: Een Zod validatieschema waarmee in inhoud van het formulier gevalideerd wordt.action: Een server (form) action die gebruikt wordt om de inhoud van het formulier te verwerken.options: De optionele configuratie die doorgegeven wordt aan de useForm hook.
De hook geeft een array met vier elementen terug.
hookForm: De data die teruggegeven wordt door useFormaction: De functie die aan de action property van Form of <form> gekoppeld moet worden.actionState: De state van de useActionState hook.isPending: Een boolean die aangeeft of het formulier bezig is met een inzending te verwerken.
const Foo: FunctionComponent = () => {
const [hookForm, createFoo, actionState, isPending] = useZodValidatedForm(createFooZodSchema, createFooAction)
// Dit is de Form component die door docenten aangereikt wordt, niet de component uit react-hook-form of Next.
return <Form hookForm={hookForm} action={createFoo}>
...
</Form>
}Een component is een onderdeel van een React applicatie. Een applicatie bestaat uit tal van componenten. Een component staat op zich, kan herbruikt worden en gebruikt eventueel andere componenten.
Er is geen vast regel over hoe klein een component mag/moet zijn. Alles hangt af van wat je bent aan het bouwen. In een UI-componenten library heeft het zin om een aparte component te bouwen voor een titel of voor een paragraaf omdat hier bepaalde styling aan verbonden is die moet integreren met de rest van library. Als je een eigen CRUD-applicatie bent aan het ontwikkelen, ga je geen aparte component maken voor een titel of paragraaf, maar gewoon een header of paragraph tag gebruiken (of een component uit een library) in een grotere component die de pagina of een onderdeel daarvan weergeeft.

Bron: https://react.dev/learn/thinking-in-react
In deze cursus gebruiken we voor componenten steeds een default export, dit wil zeggen dat de componenten geïmporteerd kunnen worden zonder accolades te moeten schrijven in het import-statement.
Als we de default modifier weglaten, wordt de component nog steeds geëxporteerd, maar maakt deze deel uit van een object. We spreken in dit geval van een named export.
Alle named exports worden verzameld in een object, via accolades in het import-statement kunnen we aangeven dat we slechts één element uit dat object willen importeren.
Een bestand kan maximaal één default export hebben, maar kan meerdere named exports bevatten. De twee kunnen ook gecombineerd worden binnen één bestand.
import {FunctionComponent} from 'react';
export const HelloWorld: FunctionComponent = () => {
const greeting = {
greeting1: 'World',
greeting2: 'Universe'
}
return (
<>
<h1>Hello {greeting.greeting1 + '!'}</h1>
<h1>Hello {greeting.greeting2}!</h1>
</>
)
}
const HelloWorlDefault = HelloWorld;
export default HelloWorlDefault;import {HelloWorld} from './helloWorld.tsx';
import HelloWorlDefault from './helloWorld.tsx';Een fragment is een container element dat enkel in de React code bestaat en geen effect heeft op de uiteindelijke HTML-code in het afgewerkte product. Een fragment wordt niet gerenderd.
const element = (
<div>
<h1>Hello {greeting.greeting1 + "!"}</h1>
<h1>Hello {greeting.greeting2}!</h1>
</div>
)const element = (
<>
<h1>Hello {greeting.greeting1 + "!"}</h1>
<h1>Hello {greeting.greeting2}!</h1>
</>
)Een functie component is een functie die één herbruikbaar onderdeel van de user interface definieert en JSX-code teruggeeft. De naam van zo'n functie begint steeds met een hoofdletter. Een functie component kan elders in de code gebruikt worden op dezelfde manier als je een HTML-element zou gebruiken.
De twee onderstaande voorbeelden zijn gelijkwaardig, de FunctionComponent en FC interfaces zijn aliassen van elkaar.
import {FunctionComponent} from 'react';
const FunctionComponent: FunctionComponent = () => {
return (
<p>
Dis is een functie component,
de return waarde moet JSX-code zijn.
</p>
)
}
const exampleUsage = <FunctionComponent/>;import {FC} from 'react';
const FunctionComponent: FC = () => {
return (
<p>
Dis is een functie component,
de return waarde moet JSX-code zijn.
</p>
)
}
const exampleUsage = <FunctionComponent/>;JavaScript XML (JSX), laat toe om HTML-code in JavaScript te gebruiken zonder quotes of concatenaties. Het is natuurlijk onmogelijk voor een browser om zo'n code te lezen en uit te voeren. Daarom moet elke lijn JSX-code gecompileerd worden naar klassiek JavaScript code.
Een bestand waar JSX in gebruikt wordt, moet de extensie JSX of tsx hebben, afhankelijk van de taal die je gebruikt.
Via SWC wordt onderstaande TypeScript code gecompileerd naar de bijhorende JavaScript code.
const element = <h1>Hello, world!</h1>var element=React.createElement("h1",null,"Hello, world!");De properties of props zijn beschikbaar op elke component en worden meegegeven via een parameter in de functie die de component definieert. Deze parameter is steeds een object. Via een TypeScript interface wordt gedefinieerd welke properties verwacht worden.
In JSX-code kunnen properties doorgegeven worden als HTML-attributen.
import {FunctionComponent} from 'react';
interface ExampleComponentProps {
example: string;
}
const ExampleComponent: FunctionComponent<ExampleComponentProps> = (props) => {
return <p>{props.example}</p>;
}
const exampleUse = <FunctionComponent
example={'This is an example value for the property example.'}/>De kinderen van een componenten, doorgegeven via de children property, kunnen als volgt aangesproken worden als een array:
import {Children, FunctionComponent, PropsWithChildren} from 'react'
const ExampleComponent: FunctionComponent<PropsWithChildren> = ({children}) => {
const childrenArray = Children.toArray(children)
// Render enkel het eerste kind.
return childrenArray[0]
}Context is een manier om prop-drilling (het doorgeven van properties tussen verschillende niveaus in de componentenboom) te vermijden. Via context kan een property die op het root niveau van de componentenboom gedefinieerd wordt gebruikt worden door bladeren in de boom, zonder dat de properties door de volledige boom doorgegeven moeten worden.
Het gebruik van context blijft best beperkt tot
- het bewaren van dingen die nauwelijks of niet aangepast worden, zoals de taalkeuze, themakeuze, munteenheid, ...;
- het bouwen van herbruikbare componenten voor een componentenbibliotheek.
Context wordt best niet gebruikt voor dingen die vaak wijzigen, of tenminste niet als deze door veel niveaus gescheiden zijn in de componentenboom. Bewaar dus nooit alle data van je applicatie op het root-niveau. Een wijziging aan de waarde die in de context zit betekent dat elke onderliggende component opnieuw gerenderd moet worden en dit kan slechte performantie tot gevolgd hebben.
Om context te definiëren gebruik je de createContext functie die functie één argument heeft, 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)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 en dus niet in de state van een component. Dit betekent dat je in zo'n situatie, bij het inzenden van een formulier, alle formuliergegevens moet uitlezen via het React equivalent van document.getElementById (of via de FormData API).
React heeft dan geen controle meer, dit maakt het bijvoorbeeld moeilijker om tijdens het invullen de data te valideren. 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, ...
React exporteert een aantal events 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 (functie) in één keer te definiëren in de plaats van via de parameters, ook deze types 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.
TypeScript definieert verschillende types die overeenkomen met de verschillende HTML elementen. Hieronder enkele voorbeelden:
- HTMLElement: superklasse voor alle tags
- 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 Cheatsheet.
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 => {}import {
ChangeEventHandler, // Functiedefinitie voor <T>(evt: ChangeEvent<T>) => void
FocusEventHandler, // Functiedefinitie voor <T>(evt: FocusEvent<T>) => void
FormEventHandler, // Functiedefinitie voor <T>(evt: FormEvent<T>) => void
MouseEventHandler, // Functiedefinitie voor <T>(evt: MouseEvent<T>) => void
} from 'react'
const handleChangeEvent: ChangeEventHandler<HTMLInputElement> = evt => {}
const handleFocusEvent: FocusEventHandler<HTMLElement> = evt => {}
const handleFormEvent: FormEventHandler<HTMLFormElement> = evt => {}
const handleMouseEvent: MouseEventHandler<HTMLElement> = evt => {}Een hook is een herbruikbare functie die 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 functiecomponent geplaatst worden en mag nooit voorkomen in:
- Een conditioneel statement
- Een lus
- Een geneste functiedefinitie
Voor meer informatie over de redenen waarom deze regels gelden verwijzen we door naar Let's break React's rules een talk gegeven door Charlotte Isambert op React Conf 2024.
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.
De state van een React component is de verzameling van variabelen die de huidige situatie van de component beschrijven. React verstrekt vanuit het principe UI = f(state), met andere woorden de UI is een functie van de state. De state van de applicatie bepaald dus hoe de UI eruit ziet.
Zo worden formulier elementen, uit- of dichtgeklapte menu's, het gekozen UI thema, data opgehaald van een API of database, ... bewaard in de state van een component.
Voor elke context moet er minstens één provider voorzien worden. De provider wordt rond alle componenten geplaatst die gebruik maken van de context en biedt een waarde aan voor de context. Alle kinderen van de provider kunnen deze waarde dan uitlezen, ongeacht hoe diep ze genest zijn in de componentenboom. Als er, in de bovenliggende componentenboom, meerdere providers beschikbaar zijn voor een bepaalde context, dan wordt de dichtstbijzijnde provider gebruikt. Als er helemaal geen provider beschikbaar is, dan wordt de standaardwaarde van de context gebruikt.
De provider kan op twee manieren gebruikt worden. Origineel moest de provider gebruikt worden als <Context.Provider>, maar sinds React 19 is het ook mogelijk om <Context> te schrijven, de Provider property wordt dan automatisch gebruikt. In de frontend cursus tref je voornamelijk de oudere notatie aan, in de backend en mobile cursussen kan je een combinatie van de oude en nieuwe notatie aantreffen.
import {createContext, FunctionComponent} from 'react'
interface ISomeContext {
// De properties van de context.
}
const SomeContext = createContext<ISomeContext>(defaultValue)
const SomeComponent: FunctionComponent = () => {
return (
<SomeContext value={foo}>
{/* Children can use the provided value */}
</SomeContext>
)
}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>
)
}De useState hook geeft een array terug met twee elementen, het tweede element is een setter die gebruikt kan worden om de state aan te passen. Dit kan op twee manieren.
In het eerste geval heeft de setter methode onderstaande signatuur en kan er dus een nieuwe waarde meegegeven worden die de huidige waarde overschrijft.
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:
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>
)
}import {useState} from 'react'
const ExampleComponent: FunctionComponent = () => {
const [stateValue, setStateValue] = useState<number>(0)
return (
<div>
{stateValue}
<button onclick={() => setStateValue(old => old + 1)}>
Verander de state
</button>
</div>
)
}De useContext hook kan gebruikt worden om een bestaande context uit te lezen. De waarde die teruggegeven wordt door deze hook is die van de dichtstbijzijnde provider, als er geen provider is wordt de default waarde gebruikt.
import {useContext} from 'react'
const SomeComponent: FunctionComponent = () => {
const contextValue = useContext(SomeContext)
return <>{/* ... */}</>
}De useState hook wordt gebruikt om state toe te voegen aan een React component. Deze 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. In deze cursus kiezen we ervoor om het type altijd expliciet mee te geven.
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>
)
}De Outlet component wordt gebruikt om geneste routes weer te geven. Zonder deze component wordt enkel de parent route getoond en worden kinderen genegeerd.
import {FunctionComponent} from 'react'
import {Routes, Route, Outlet} from 'react-router'
const Routing: FunctionComponent = () => {
return (
<Routes>
<Route path={'/foo'} element={<Foo/>}>
<Route path={'/bar'} element={<Baz/>}/>
<Route path={':param1'} element={<Qux/>}/>
</Route>
</Routes>
)
}
const Foo: FunctionComponent = () => {
return (
<>
<h1>Foo</h1>
{/**
* De code hierboven worden altijd gerenderd,
* omdat deze in de Foo (parent) component zit.
* De Baz en Qux componenten worden nooit getoond,
* tenzij er ergens een <Outlet/> gebruikt wordt.
*/}
<Outlet/>
</>
)
}Een absoluut pad begint met een forward-slash (/) en bevat het volledige pad, vanaf de root.
Een relatief pad begint niet met een forward-slash, maar met het volgende deel van het pad. Stel de link is gedefinieerd in een component die zich op het pad /foo/bar bevindt, dan kunnen we in deze component een link definiëren als baz. Omdat dit pad relatief is (het bevat vooraan geen slash), zal het geïnterpreteerd worden ten opzichte van het huidige pad en wordt een gebruiker na het drukken op deze link dus naar /foo/bar/baz gestuurd.
De Routes en Route componenten uit react-router worden gebruikt om, op basis van de huidige URL, de juiste componenten te tonen.
De Routes component bevat één of meerdere Route componenten waarmee aangegeven wordt welke componenten getoond moeten worden voor welke route. Een Route component gebruikt de property path om de route te definiëren en de property element om de bijhorende component vast te leggen.
Als onderstaande code gerenderd wordt, zal slechts één van de drie pagina's (HomeComponent, AboutComponent, PricingComponent) getoond worden, afhankelijk van de huidige URL.
import {Routes, Route} from 'react-router'
const Routing: FunctionComponent = () => {
return (
<Routes>
<Route path={'/'} element={<HomeComponent/>}/>
<Route path={'/about'} element={<AboutComponent/>}/>
<Route path={'/pricing'} element={<PricingComponent/>}/>
</Routes>
)
}SPA links zijn links die gebruikt kunnen worden om te navigeren binnen een SPA zonder de pagina te moeten herladen.
React Router bevat een Link en NavLink component, beide componenten passen de URL in de adresbalk aan, maar doen dit zonder de pagina te herladen. Het verschil tussen de twee componenten is de opmaak. Aan een NavLink component kan via de property style of className een functie meegegeven worden die de opmaak aanpast als de link actief is. Verder is er geen verschil tussen de twee componenten. Elk van deze componenten heeft een to property die gebruikt kan worden om het pad door te geven waarnaar genavigeerd moet worden als op de link geklikt wordt.
import {Link, NavLink} from 'react-router';
const textLinkExample = <Link to="The path to navigate to">Link text on the website</Link>
const navLinkExample1 = (
<NavLink to="The path to navigate to"
className={({isActive}) => isActive ? 'activeClass' : 'standardClass'}>
Link text on the website
</NavLink>
)
const navLinkExample2 = (
<NavLink to="The path to navigate to"
activeStyle={'CSSProperties object with styling'}>
Link naam op website
</NavLink>
)De useLocation uit React Router geeft informatie terug over de huidige locatie, i.e. de url.
De informatie wordt teruggeven is een object dat onder meer de pathname property bevat. Deze property bevat de het huidige pad in de webapplicatie, een url als http://example.com/foo/bar?baz=qux geeft dus /foo/bar terug.
import {useLocation} from 'react-router'
const ExampleComponent: FunctionComponent = () => {
const location = useLocation()
return (
<div>
{location.pathname === '/home' ? <h1>Home</h1> : <h1>Not Home</h1>}
</div>
)
}De useParams hook geeft een object terug waarin elke parameter voor de actieve route beschikbaar is onder dezelfde naam als in de Route component die de parameter definieert. Naast de actieve route zijn ook alle parameters van parent routes beschikbaar.
Navigatieparameters worden via de URL doorgegeven en zijn dus altijd strings, als je een number doorgeeft, moeten de parameter eerste geconverteerd worden.
import {FunctionComponent} from 'react'
import {Routes, Route, useParams} from 'react-router'
const Routing: FunctionComponent = () => {
return (
<Routes>
<Route path={'/users'} element={<Outlet/>}>
<Route index element={<Users/>}/>
<Route path={':userId'} element={<Outlet/>}>
<Route index element={<UserDetails/>}/>
<Route path={':activityId'} element={<UserActivityDetails/>}/>
</Route>
</Route>
</Routes>
)
}
const UserActivityDetails: FunctionComponent = () => {
const {userId, activityId} = useParams()
return (
<>
...
</>
)
}Een custom hook is een hook die je zelf schrijft met als doel herbruikbare code af te zonderen in een aparte functie die later herbruikt kan worden in verschillende componenten. De conventie is dat de naam van deze functie begint met use. Binnen een hook kunnen we alle gekende hooks (en custom hooks) gebruiken, hiervoor gelden dezelfde regels als in een component. Een custom hook kan eender wat teruggeven, net zoals een normale JavaScript functie. Daarnaast kunnen er ook nul, één of meer parameters aanwezig zijn in de definitie van de hook.
const useSomeReusableCode = (...) => {
// Do something reusable
return ...
}Voor projecten aangemaakt via vite kan een .env file gebruikt worden. In deze file worden environment variabelen opgeslagen die beschikbaar moeten zijn in de frontend code, let op, hiervoor moet de prefix VITE_ toegevoegd worden aan de naam van de variabele. De .env file moet in de root van je project bewaard worden.
Via het import.meta.env object kunnen de verschillende environment variabelen opgevraagd worden.
Gevoelige API Keys
Alhoewel wij dit bestand gebruiken om een API key te bewaren is dit niet veilig. Enkel API keys die bedoeld zijn om publiek beschikbaar te zijn (i.e. in een website), kunnen hier bewaard worden. De .env file wordt mee in de production build van de applicatie gezet waardoor elke key gelezen kan worden door al je gebruikers.
Het is meestal beter om je API keys op de server te gebruiken zodat je authenticatie en autorisatie kan toevoegen en kan garanderen dat je keys niet misbruikt worden.
VITE_ENVIRONMENT_VARIABLE=some-string-value-hereconst ENVIRONMENT_VARIABLE = import.meta.env.VITE_ENVIRONMENT_VARIABLEEen error boundary is een component die, net als suspense, rond een stuk code gezet wordt en foutmeldingen opvangt. Net als bij suspense kan je een fallback component tonen wanneer er een fout optreedt.
Er bestaat echter geen kant-en-klare ErrorBoundary component, zoals in de documentatie te lezen is moet je zelf een error boundary bouwen en dit gaat enkel via class components, een outdated manier om componenten te schrijven in React. Aangezien we in deze cursus enkel functionele componenten gebruiken kiezen we ervoor om een ErrorBoundary component te installeren via npm
pnpm add react-error-boundaryimport {ErrorBoundary} from 'react-error-boundary'
const SomeComponent: FunctionComponent = () => {
return (
<ErrorBoundary fallback={<SomeFallbackComponent/>}>
<Suspense fallback={<SomeFallbackComponent/>}>
<SomeComponentThatLoadsDataThroughALibraryThatSupportsSuspense/>
</Suspense>
</ErrorBoundary>
)
}De Suspense component kan gebruikt worden om een fallback element, zoals een spinner of loading animatie, te tonen terwijl data aan het laden is. Zodra de data geladen is, wordt het fallback verborgen en wordt de data getoond.
Deze component is enkel beschikbaar als je een library gebruikt die hier ondersteuning voor biedt of als je de use operator uit React 19 gebruikt.
import {FunctionComponent, Suspense} from 'react'
const SomeComponent: FunctionComponent = () => {
return (
<Suspense fallback={<SomeFallbackComponent/>}>
<SomeComponentThatLoadsDataThroughALibraryThatSupportsSuspense/>
</Suspense>
)
}Het stale while revalidate (SWR) principe betekent dat zodra het antwoord op een request binnen komt, er een timer begint te lopen die aangeeft hoelang de data nog als juist/correct/recent beschouwd mag worden. De default waarde (in TanStack Query) is 0 seconden, data wordt dus als stale (verouderd) beschouwd vanaf dat deze opgehaald is.
Zodra een gebruiker terug focust op de pagina, of op een andere manier naar nieuwe data vraagt, wordt de data uit de cache gebruikt om de pagina zo snel mogelijk te tonen, maar er wordt ook onmiddellijk een nieuw verzoek gestuurd om de data te refreshen. Standaard blijft data (in TanStack Query) 5 minuten in de cache zitten.
De useSuspenseQuery hook wordt gebruikt om data op te halen van een externe bron. Wat deze bron is, speelt geen rol. Dit kan, afhankelijk van het platform (web, desktop, mobile), een API, een filesystem operatie, een sqlite query of nog iets anders zijn.
De hook is zeer uitgebreid en complex, we beperken ons in deze tekst tot de basisfunctionaliteiten. Voor meer details verwijzen we door naar de documentatie.
Deze useSuspenseQuery hook heeft 2 vaste parameters en één optionele:
Een array waarvan de elementen samen de key van de query vormen. Deze key moet uniek zijn, als 2 queries dezelfde key krijgen, worden ze als gelijk beschouwd en wordt eventueel gecachete data gebruikt.
Een functie die data ophaalt.
Optionele configuratie, de lijst van mogelijke opties is te groot om hier te bespreken, we verwijzen door naar de documentatie
De useSuspenseQuery hook geeft een groot object terug, we verwijzen opnieuw naar de documentatie voor de volledige lijst. We gebruiken in deze les enkel de data property.
import {FunctionComponent} from 'react'
import {useSuspenseQuery} from '@tanstack/react-query'
const SomeComponent: FunctionComponent = () => {
const {data} = useSuspenseQuery({
queryKey: ['some', 'unique', 'query', 'key'],
queryFn: someAsynchronousFetchingFunction,
// Optional extra configuration
})
return <>{/* Some JSX that uses the fetched data. */}</>
}Via de useRef hook kan je een variabele persistent maken doorheen renders, i.e. tussen verschillende keren dat een functiecomponent opgeroepen wordt of de state aangepast wordt.
Net zoals de useState hook heeft de useRef hook een generische parameter waarmee het type van de inhoud van de inhoud van de hook bewaard kan worden.
De waarde van de _useRef* hook zit steeds in de current property. Dus de signatuur van de hook ziet er (ongeveer) als volgt uit.
import {FunctionComponent, useRef} from 'react'
const SomeComponent: FunctionComponent = () => {
const persistentVariable = useRef<string>(defaultValue)
const foo = () => {
if (persistentVariable.current === bar) {
// Do something
// Update the persistent variable
persistentVariable.current = 'baz'
}
}
return <p onClick={foo}>Some JSX code</p>
}Om data die opgehaald is door de useSuspenseQuery hook te verversen nadat deze aangepast is, moet de huidige data eerst geïnvalideerd worden.
Dit kan door een combinatie van de invalidateQueries uit de QueryClient, de onSuccess property van de useMutation hook en de useQueryClient hook waarmee de queryClient uit de dichtstbijzijnde QueryClientProvider opgehaald kan worden.
De invalidateQueries methode wordt gebruikt om één of meerdere queries te invalideren, vervolgens wordt de data opnieuw opgehaald van de server. Voor een volledige lijst van mogelijke parameters voor invalidateQueries verwijzen we door naar de documentatie.
import {useQueryClient, useSuspenseQuery, useMutation} from '@tanstack/react-query'
const useGetFoo = () => {
return useSuspenseQuery({
queryKey: ['foo'],
queryFn: getFoo
})
}
const useUpdateFoo = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateFoo,
onSucces: () => queryClient.invalidateQueries({queryKey: ['foo']})
})
}Een optimistische update, is een update die ervan uit gaat dat een update, create of delete operatie succesvol weggeschreven zal worden naar de server en dat de wijzigingen dus in de UI getoond mogen worden. Hier kan dan eventueel nog een animatie of progressbar aan toegevoegd worden om aan te geven dat de data nog weggeschreven wordt.
Een optimistische update is voor een gebruiker aangenamer omdat er sneller iets te zien is op het scherm en de gebruiker dus verder kan werken. Het is echter ook mogelijk dat de mutatie mislukt en dat de UI teruggedraaid moet worden. In een goed gebouwde en geteste applicatie zou dit echter zo goed als nooit mogen voorvallen, maar je moet er natuurlijk wel rekening mee houden in je code.
Om een optimistische update te implementeren moeten volgende stappen doorlopen worden
In de onMutate functie van de useMutation hook
- Eventuele actieve queries moeten geannuleerd worden omdat het mogelijk is dat de optimistische update anders overschreven wordt door het resultaat van de active query. Hiervoor kunnen de onMutate property van de useMutation hook en de cancelQueries methode van de queryClient gebruikt worden.
- We maken vervolgens een kopie van de huidige data in de query cache via de getQueryData methode van de queryClient. We hebben deze nodig om de optimistische update terug te draaien als er iets misgaat.
- We overschrijven de huidige data in de query cache met de aangepast (optimistische) data via de setQueryData methode. Als het om een create operatie gaat, zal in de optimistische data natuurlijk geen ID aanwezig zijn. Hiermee moet dan rekening gehouden worden in de code.
In de onError functie van de useMutation hook
- De oude data gebruiken om de query cache aan te passen via de setQueryData methode.
- De query cache invalideren in het geval dat de error ontstaan is door oude (stale) data op de client die niet meer overeenkwam met data op de server.
In de onSuccess functies van de useMutation hook
- De aangepaste data, teruggegeven door de mutationFn gebruiken om de optimistische data te overschrijven, zo zijn zaken als het ID en andere dingen die door de server gegenereerd moeten worden in orde. Dit is natuurlijk enkel nodig indien de server bepaalde velden moet genereren (zoals een ID). Voor een delete-operatie is deze optie zo goed als altijd overbodig.
import {useQueryClient, useQuery, useMutation, UseMutationResult, UseQueryResult} from '@tanstack/react-query'
const useGetFoo = (): UseQueryResult<TData, Error> => {
return useQuery({
queryKey: ['foo'],
queryFn: getFoo
})
}
const useUpdateFoo = (): UseMutationResult<TData, Error, TVariables, TContext> => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateFoo,
onMutate: async (newData) => {
const queryKey = ['foo']
await queryClient.cancelQueries({queryKey})
// De generische parameter kan ook een gewoon object
// zijn in de plaats van een array.
const oldData = queryClient.getQueryData<TData[]>() ?? []
// In het geval dat de cache een enkel object bevat vervang je
// de data in de plaats van een functionele setter (analoog aan useState).
// Voor een update pas je de data in de old array aan.
queryClient.setQueriesData<TVariables>({queryKey}, old => [...old, newData])
return {oldData, queryKey}
},
onError: async (error, variables, context) => {
if (context) {
queryClient.setQueryData<TData[]>(context.queryKey, context.oldData)
await queryClient.invalidateQueries({queryKey: context.queryKey})
}
},
onSucces: (data, variables, context) => {
if (context) {
// Voor een update vervang je het item in de old array met
// de data parameter.
queryClient.setQueryData<TData>({queryKey: context.queryKey}, [...context.oldData, data])
}
}
})
}Via de setQueryData van de QueryClient kan data die geacht is door TanStack Query aangepast worden.
De functie heeft twee parameters, de query key en de nieuwe data. Net als bij de useState hook kan de tweede parameter een functie zijn die de oude data als argument heeft en de nieuwe data teruggeeft of kan de nieuwe waarde rechtstreeks meegegeven worden.
Alhoewel hieronder het type string gebruikt wordt als generische parameter, kan dit natuurlijk elk type zijn.
const useSetQueryExample = () => {
const queryClient = useQueryClient()
// Via vervanging
queryClient.setQueryData<string>(['foo'], 'bar')
// Via update functie
queryClient.setQueryData<string[]>(['foo'], (oldData) => oldData.map(x => x + 'bar'))
}De useEffect hook kan gebruikt worden om een bepaalde actie uit te voeren nadat de component gerenderd is. De actie die uitgevoerd wordt na het renderen is in eerste instantie bedoeld om te synchroniseren met een extern systeem. De term extern systeem bevat hier alles wat niet rechtstreeks door React beheerd wordt. Enkele voorbeelden zijn real-time data connecties, loggen van bezochte pagina's in een externe database, het aanspreken van browser API's (keydown, resize, ...), een third-party library die geen React componenten aanbiedt, ...
De useEffect hook heeft 2 parameters, de eerste parameter is een functie die een bepaalde actie uitvoert, de tweede optionele parameter is dependenciesArray. Via de dependencies wordt het mogelijk om te controleren wanneer de eerste parameter uitgevoerd wordt.
Als de dependencies ontbreken, zal de functie na elke render uitgevoerd worden.
const SomeComponent: FunctionComponent = () => {
useEffect(() => {
// Functie wordt uitgevoerd na elke render.
})
return <></>
}Als de dependencies array aanwezig is, maar leeg is, dan wordt de functie één keer, na de eerste render uitgevoerd.
const SomeComponent: FunctionComponent = () => {
useEffect(() => {
// Functie wordt één keer uitgevoerd na de eerste render.
}, [])
return <></>
}Als de dependencies array één of meer elementen bevat, dan wordt de functie uitgevoerd na de eerste render en als minstens één van deze elementen wijzigt in waarde.
const SomeComponent: FunctionComponent = () => {
useEffect(() => {
// Functie wordt uitgevoerd na de eerste render
// en als dependencyVar1, dependencyVar2
// of een andere dependency van waarde wijzigt.
}, [dependencyVar1, dependencyVar2, ...])
return <></>
}Tenslotte kan de eerste parameter een returnwaarde hebben, deze returnwaarde is een functie die real-time connecties stop zet, event listeners, timeouts of intervals annuleert of andere clean-up code uitvoert. Dit soort clean-up is cruciaal om memory leaks en bugs te vermijden, daarom wordt elke functie in de useEffect hook ook twee keer uitgevoerd in development. Dit vertraagt de development omgeving, maar zorgt ervoor dat bugs gedetecteerd worden voordat de applicatie in productie gezet wordt.
const SomeComponent: FunctionComponent = () => {
useEffect(() => {
// Een functie die een bepaalde actie uitvoert.
// Geef een functie terug die clean-up uitvoert voor
// de bovenstaande code.
return () => {
// Clean-up code
}
}, [dependencyVar1, dependencyVar2, ...])
return <></>
}De useMutation hook (uit TanStack Query) wordt gebruikt om data aan te passen op de server en bevat verschillende mogelijkheden om functies uit te voeren als de aanpassingen succesvol afgerond zijn, mislukken of juist beginnen.
De hook neemt een object als parameter dat één verplichte property (mutationFn) heeft en verschillende optionele properties (onMutate, onSuccess, onError, onSettled, ...). We bespreken in deze cursus enkel de meest courante properties.
In volgend codefragment wordt gebruik gemaakt van de data, error, variables en context parameters die volgende betekenis hebben:
- data: Hetgeen dat teruggegeven wordt door de mutationFn, i.e. de aangepaste data, het resultaat van een POST, PUT, PATCH of DELETE request. Het type, TData moet dus overeenkomen met de returnwaarde van de mutationFn.
- error: Een eventuele error die opgegooid wordt door de mutationFn in het geval dat deze niet succesvol afgerond kon worden. _ variables: Een object met parameters die aan de mutationFn doorgegeven worden, TVariables moet dus overeenkomen met de eerste (en enige) parameter van de mutationFn. Gebruik dus altijd een object als parameter voor de mutationFn en nooit een primitieve variabele zoals een string, number, boolean of array. _ context: Data die van de onMutate functie doorgegeven wordt naar de onSuccess, onError en onSettled functies.
const {
isError,
mutate,
data,
isSuccess,
isIdle
}: UseMutationResult<TData, TError, TVariables, TContext> = useMutation({
mutationFn: (variables: TVariables) => {
// Wordt uitgevoerd als de mutate property in de returnvalue opgeroepen wordt.
// Via deze functie wordt de data server-side aangepast.
// POST, PUT, DELETE of PATCH geen GET!
},
onMutate: (variables: TVariables): TContext => {
// Wordt opgeroepen vlak voordat de mutation functie opgeroepen is.
// Wordt meestal gebruik om optimistische updates uit te voeren.
},
onSuccess: (data: TData, variables: TVariables, context: TContext) => {
// Wordt uitgevoerd als de mutationFn zonder problemen afgerond is.
},
onError: (error: TError, variables: TVariables, context: TContext) => {
// Wordt uitgevoerd als de mutationFn met errors afgerond is.
},
onSettled: (data: TData | undefined, error: TError | undefined, variables: TVariables, context: TContext) => {
// Wordt uitgevoerd als de onSuccess en onError functies afgehandeld zijn, of
// nadat de mutation afgerond is in het geval dat onSucess en onError niet gedefinieerd zijn.
}
})Het kan af en toe nuttig zijn om een lus vroegtijdig te beëindigen of een iteratie over te slaan.
Stel dat je in een grote dataset moet zoeken of bepaalde elementen aanwezig zijn of juist niet aanwezig zijn. Dan kan je natuurlijk door elke element in de dataset gaan, maar zodra je het element gevonden hebt, doet de rest van de dataset er niet meer toe. In dit geval kan je best het break statement gebruiken om de lus te beëindigen, zo is je programma sneller.
Continue is vooral nuttig om je code beter leesbaar te maken. Soms moet heel wat code enkele uitgevoerd worden in een bepaald geval. Je kan dan natuurlijk een conditioneel statement gebruiken en de conditionele code daar in steken. Dit betekent echter dat je code dieper genest is en slechter leesbaar, zeker als deze conditionele code nog andere conditionele code bevat. In de plaats daarvan kan je de code onder het conditioneel statement zetten en het continue statement gebruiken in de if.

In JavaScript kunnen variabelen aangemaakt worden via de const en let keywords. Het verschil tussen beide is dat een variabele die aangemaakt is met const niet meer gewijzigd kan worden nadat deze een waarde heeft gekregen. Een variabele die aangemaakt is met let kan wel gewijzigd worden.
Maak zoveel mogelijk gebruik van const om variabelen aan te maken, dit heeft enkele belangrijke voordelen:
- De code is beter leesbaar, je weet dat een variabele nooit een nieuwe waarde zal krijgen.
- De code is kwalitatiever, het is zelden nodig om een variabele aan te passen nadat deze gedeclareerd is.
constletconstconst foo = "An exmple string variable";
// Onderstaande lijn zorgt voor een error.
// foo = "A new value";letlet foo = "An exmple string variable";
// Onderstaande lijn is geldige JavaScript code.
foo = "A new value";Een functie is een manier om code af te zonderen. Er zijn verschillende redenen om dit te doen. Enerzijds zijn functies heel handig om code die verschillende keren uitgevoerd moet worden slechts één keer neer te schrijven. Anderzijds bevorderen functies de leesbaarheid van je code, ook als de code slechts één keer uitgevoerd wordt.
Een functie wordt gedefinieerd via het function keyword en kan eventueel parameters bevatten. In verschillende situaties is het handig om parameters optioneel te maken en een defaultwaarde mee te geven. Zo moet je, als je de functie oproept, enkel een waarde meegeven voor de parameter als je afwijkt van de standaard situatie.
Functies kunnen behandeld worden als variabelen, je kan ze dus doorgeven aan andere functies, in een array bewaren, ... Om een functie als een variable te behandelen moet je de naam van de functie gebruiken zonder ronde haakjes.
Een functie heeft regelmatig een andere functie nodig als parameter. We kunnen, zoals hierboven besproken, een functie als variabele doorgeven door de ronde haakjes weg te laten en de naam van de functie te gebruiken. Maar regelmatig is de functie die je als parameter doorgeeft uniek en ga je deze nooit opnieuw gebruiken. In dit geval kan je een anonieme functie gebruiken, dit is een functie zonder naam.
function generateGreeting(name, age) {
return `Hello ${name}, you are ${age} years old .`;
}
console.log(generateGreeting('John Doe', 30));
console.log(generateGreeting('Jane Doe', 25, 'Master in Computer Science'));function generateGreeting(name, age, education = 'Gegradueerde in het programmeren') {
return `Hello ${name}, you are ${age} years old and you are working towards the '${program}' degree.`;
}
console.log(generateGreeting('John Doe', 30));
console.log(generateGreeting('Jane Doe', 25, 'Master in Computer Science'));function execute10Times(func) {
for (let i = 0; i < 10; i++) {
// Door hier de ronde haakjes te gebruiken wordt de functie uitgevoerd.
func();
}
}
function printGreeting() {
console.log('Hello world!');
}
// De functie printGreeting wordt hier als variabele doorgegeven aan execute10Times.
execute10Times(printGreeting);function execute10Times(func) {
for (let i = 0; i < 10; i++) {
// Door hier de ronde haakjes te gebruiken wordt de functie uitgevoerd.
func();
}
}
// Het argument van execute10Times is een anonieme functie.
// De functie bestaat enkel in de scope van execute10Times en heeft geen naam.
execute10Times(function () {
console.log('Hello world!');
});Conditionele statements zijn statements die een bepaald blok code uitvoeren als een bepaalde voorwaarde waar is.
if (voorwaarde) {
// voer instructie(s) uit indien voorwaarde waar is
}if (voorwaarde) {
// voer instructie(s) uit indien voorwaarde waar is
} else {
// anders voer instructie2 uit
}if (voorwaarde1) {
// voer instructie(s) uit indien voorwaarde1 waar is.
} else if (voorwaarde2) {
// voer instructie(s) uit indien voorwaarde2 waar is.
} else {
// anders voer deze instructie(s) uit
}(voorwaarde) ? instructie indien waar : instructie indien niet waar ;We onderscheiden drie soorten iteraties.
Een begrensde herhaling (for) kan gebruikt als je exact weet hoeveel keer je een lust wilt uitvoeren.
Een onbegrensde herhaling (while) kan gebruikt worden als je niet weet hoeveel keer de lus uitgevoerd moet worden. De lus neemt een conditie als argument, zolang de conditie waar is, wordt de lus uitgevoerd.
De (do-while) lus is een onbegrensde herhaling die minstens één keer wordt uitgevoerd. Ook al is de conditie onwaar, wordt de lus toch minstens één keer uitgevoerd.
// Algemene syntax
for ( startwaarde ; eindwaarde ; verhoging ) {
// herhaalt n-maal dezelfde instructie.
}
// Praktisch voorbeeld
for (let i = 0; i < 3; i++) {
console.log(`Lus ${i + 1}`)
}while(voorwaarde){
// Herhaal tot de voorwaarde false wordt.
}
let count = 1;
let i = 1;
while (i !== 0) {
console.log(`Lus ${count}`);
i = Math.round(Math.random())
count++;
}do {
// Herhaal tot de voorwaarde false wordt.
// Gebeurd minstens één keer omdat de voorwaarde pas op het einde gecontroleerd wordt.
} while(voorwaarde);| Operator | Omschrijving |
|---|---|
| * | Vermenigvuldiging |
| / | Deling |
| + | Optelling |
| - | Aftrekking |
| % | Modulus (rest na gehele deling) |
| ++ | Increment (verhoog met 1) |
| -- | Decrement (verminder met 1) |
| ** | Machtsverheffing |
| += | Optelling en toekenning |
| -= | Aftrekking en toekenning |
| *= | Vermenigvuldiging en toekenning |
| Operator | Omschrijving |
|---|---|
| == | Gelijk aan, datatype is niet belangrijk, volgens deze operator is "1" == 1 waar. |
| === | Gelijk aan, datatype is belangrijk, volgens deze operator is "1" == 1 niet waar. Gebruik bij voorkeur altijd deze operator. |
| < | Kleiner dan. |
| > | Groter dan. |
| <= | Kleiner of gelijk aan. |
| >= | Groter of gelijk aan. |
| != | Niet gelijk aan, datatype is niet belangrijk. |
| !== | Niet gelijk aan, datatype is belangrijk. |
| Operator | Omschrijving |
|---|---|
| && | En |
| || | Of |
| ! | Niet |
| Operator | Omschrijving | Voorbeeld |
|---|---|---|
| ?? | Nullish coalescing operator, geeft de rechtenwaarde als de linkerwaarde null of undefined is anders de linkerwaarde. | null ?? "default" geeft "default" terug.'a' ?? 'default' geeft 'a'terug |
| ?. | Optionele chaining operator, roept een methode of eigenschap aan als de linkerwaarde bestaat. | foo?.bar?.baz roept baz aan als foo en bar bestaan. |
| ? : | Ternary operator, een verkort if-else statement. | condition ? value1 : value2 geeft value1 terug als condition waar is, anders value2. |
Een switch is een alternatief voor een if-else-elseif structuur. De switch neemt een variabele als argument en controleert vervolgens verschillende cases tot een case gelijk is aan de variabele.
Elke case moet een break statement bevatten, anders worden de andere cases ook doorlopen. De laatste case hoeft geen break statement te bevatten, omdat er geen andere cases meer zijn om door te lopen. Meestal is deze laatste case een default case die uitgevoerd wordt als geen van de andere cases true zijn.
Als je condities wilt gebruiken in de plaats van vooraf gedefinieerde variabelen, moet de switch de parameter true krijgen. Vervolgens kan je voor elke case een conditie schrijven. Deze tweede optie is slechter leesbaar en kan beter vervangen worden door een if-else-elseif structuur.
switch(variabele){
case optie1:
programmacode;
break;
case optie2:
programmacode;
break;
default:
programmacode;
}switch(true){
case conditie1:
programmacode;
break;
case conditie2:
programmacode;
break;
default:
programmacode;
}Een template literal is een string die variabelen bevat en gedefinieerd wordt tussen backticks (``). Via ${} kan je variabelen invoegen in de string.
const foo = "An exmple string variable";
const bar = `A template literal containing other variables like ${foo}`;Via de for...of lus kan er geïtereerd worden over alle elementen in een array.
Merk op dat je het of keyword gebruikt moet worden in plaats van in zoals in de meeste andere programmeertalen.
const vakken = ['Programming Essentials', 'Object Oriented Programming', 'JavaScript'];
for (const vak of vakken) {
console.log(vak);
}In bovenstaande lus is het mogelijk om gebruik te maken van het break of continue keyword om respectievelijk de lus vroegtijdig te beëindigen of een iteratie over te slaan.
Via de forEach methode van de array kan er eveneens geïtereerd worden over alle elementen in een array, maar hierbij is het niet mogelijk om break of continue te gebruiken.
const vakken = ['Programming Essentials', 'Object Oriented Programming', 'JavaScript'];
// Elk vak word automatisch doorgegeven aan console.log
vakken.forEach(console.log);
// Een arrow functie om elk vak te printen.
vakken.forEach(vak => console.log(vak));
// Een arrow functie met meerdere lijnen om elk vak te printen.
vakken.forEach(vak => {
console.log(vak)
});
// Een forEach lus die itereert over zowel het vak als de bijhorende index.
vakken.forEach((vak, index) => console.log(vak, index))Arrays hebben een groot aantal ingebouwde methoden die je kunt gebruiken om bewerkingen op arrays uit te voeren. De volledige lijst is te vinden in de MDN documentatie. Hieronder bespreken we de belangrijkste:
push(element1, ..., elementN): Voegt één of meerdere elementen toe aan het einde van de array.const cijfers = [1, 2, 3]; cijfers.push(4); // cijfers is nu [1, 2, 3, 4] cijfers.push(5, 6); // cijfers is nu [1, 2, 3, 4, 5, 6]pop(): Verwijdert het laatste element van de array en retourneert dit element.const cijfers = [1, 2, 3]; const laatste = cijfers.pop(); // laatste is 3, cijfers is nu [1, 2]shift(): Verwijdert het eerste element van de array en retourneert dit element.const cijfers = [1, 2, 3]; const eerste = cijfers.shift(); // eerste is 1, cijfers is nu [2, 3]sort(): Sorteert de elementen van de array in plaats (standaard in lexicografische volgorde, volgens de ASCII tabel).const cijfers = [3, 1, 2]; cijfers.sort(); // cijfers is nu [1, 2, 3]reverse(): Keert de volgorde van de elementen in de array om.const cijfers = [1, 2, 3]; cijfers.reverse(); // cijfers is nu [3, 2, 1]join(separator): Combineert alle elementen van de array tot een enkele string, gescheiden door de opgegevenseparator.const woorden = ['Hello', 'World']; const zin = woorden.join(' '); // zin is "Hello World"at(index): Retourneert het element op de opgegeven index. Ondersteunt negatieve indices om vanaf het einde te tellen.const cijfers = [10, 20, 30, 40, 50]; const derde = cijfers.at(2); // derde is 30 const laatste = cijfers.at(-1); // laatste is 50fill(value, start, end): Vult de array met het opgegevenvaluevan destartindex tot deendindex (exclusief). Alsstartenendniet opgegeven zijn, wordt de hele array gevuld.const cijfers = [1, 2, 3, 4, 5]; cijfers.fill(0); // cijfers is nu [0, 0, 0, 0, 0]filter(predicate): Creëert een nieuwe array met alle elementen die voldoen aan de voorwaarde gedefinieerd in depredicatefunctie.const cijfers = [1, 2, 3, 4, 5]; const evenCijfers = cijfers.filter(num => num % 2 === 0); // evenCijfers is [2, 4]map(callback): Creëert een nieuwe array door decallbackfunctie toe te passen op elk element van de originele array.const cijfers = [1, 2, 3]; const verdubbeld = cijfers.map(num => num * 2); // verdubbeld is [2, 4, 6]reduce(callback, initialValue): Reduceert de array tot een enkele waarde door decallbackfunctie toe te passen op elk element, met een optioneleinitialValue.const cijfers = [1, 2, 3, 4]; const som = cijfers.reduce((accumulator, current) => accumulator + current, 0); // som is 10slice(start, end): Retourneert een nieuw array dat een deel van de originele array bevat, beginnend bijstartindex totendindex (exclusief). Alsendniet opgegeven is, worden alle elementen vanafstarttot het einde van de array genomen.const cijfers = [1, 2, 3, 4, 5]; const deel = cijfers.slice(1, 4); // deel is [2, 3, 4]
Arrays worden gebruikt om een collectie van data te bewaren in één variabele. Arrays zijn geordend, wat betekent dat de volgorde van de elementen behouden blijft, het eerste element wordt aangeduid met index , het tweede met index , enzovoort.

const leeg = []; // Een lege array.
const leeg2 = new Array(5); // Een lege array met 5 lege plaatsen.
const tientallen = [10, 20, 30, 40, 50]; // Een array met 5 numerieke waarden.
const hello = ['Hello', 'World']; // Een array met 2 tekstwaarden.
const reeks = [1, , 3] // Een array met een lege plaats tussen 1 en 3.Om elementen uit de array uit te lezen of te wijzigen, gebruikt je de index tussen vierkante haken [].
const tientallen = [10, 20, 30, 40, 50];
console.log(tientallen.length) // Print 5 naar de console (aantal elementen in de array).
console.log(tientallen[1]) // Print 20 naar de console.
tientallen[5] = 70; // Voeg een nieuw element toe aan het einde van de array (op index 5).
tientallen[5] = 60; // Wijzig het element op index 5 naar 60.
console.log(tientallen.length) // Print 6 naar de console (aantal elementen in de array).Een arrow function of (lambda function) is een compacte alternatief voor traditionele functies. Ze worden vaak gebruikt voor korte functies of als argumenten voor hogere-orde functies zoals map, filter en reduce.
Arrow functies hebben geen eigen this context, wat betekent dat ze de this waarde van de omliggende scope overnemen. Zie hoofdstuk 3 voor meer informatie over this.
// Klassieke functie
function foo() {
return 'Hello Foo'
}
// Arrow functie op één lijn.
// Geen return nodig, een one-line array functie
// geeft het resultaat automatisch terug.
const bar = () => 'Hello Bar'
// Arrow functie met meerdere statements.
const baz = () => {
const message = 'Hello Baz'
// Een multi-line arrow functie heeft een
// return statement nodig (als je iets wilt teruggeven).
return message
}Arrow functies kunnen nul, geen of meerdere parameters hebben. In het geval er nul of meerdere parameters zijn, moeten deze tussen ronde haakjes staan.
// Geen parameters --> lege haakjes
const functionName = () => expression
// Eén parameter --> geen haakjes nodig
const functionName = () => expression
// Meerdere parameters --> haakjes nodig
const functionName = (param1, paramN) => expressionEen Set is een datastructuur die overeenkomt met een verzameling in de wiskunde, duplicaten zijn dus onmogelijk.
Controleren of een waarde in een set aanwezig is, is bijzonder snel in vergelijking met een array.
// Een lege set
const emptySet = new Set();
// Een set met een aantal elementen
const languages = new Set(['JavaScript', 'Python', 'Java', 'C++', 'JavaScript']);
console.log(languages);
// Voeg een element toe aan de set
languages.add('C#');
// Verwijder een element uit de set
languages.delete('Java');
// Controleer of een element in de set zit
console.log('Python in set: ', languages.has('Python'));
// Itereren over de set
console.log('Talen in de set:')
for (const language of languages) {
console.log(language);
}De volledige lijst van Set methoden is te vinden in de MDN documentatie. Hieronder bespreken we de belangrijkste:
add(value): Voegt een nieuw element toe aan de set. Als het element al bestaat, verandert de set niet.const talen = new Set(); talen.add('JavaScript'); talen.add('Python');delete(value): Verwijdert een element uit de set. Retourneerttrueals het element werd verwijderd, andersfalse.const talen = new Set(['JavaScript', 'Python']); talen.delete('Python'); // retourneert true, talen bevat nu alleen 'JavaScript'has(value): Controleert of een element in de set aanwezig is. Retourneerttrueoffalse.const talen = new Set(['JavaScript', 'Python']); console.log(talen.has('JavaScript')); // retourneert true console.log(talen.has('C++')); // retourneert falseclear(): Verwijdert alle elementen uit de set.const talen = new Set(['JavaScript', 'Python']); talen.clear(); // talen is nu een lege setunion(otherSet): Retourneert een nieuwe set die de unie is van de huidige set enotherSet.const setA = new Set([1, 2, 3]); const setB = new Set([3, 4, 5]); const unionSet = setA.union(setB); // unionSet bevat 1, 2, 3, 4, 5intersection(otherSet): Retourneert een nieuwe set die de doorsnede is van de huidige set enotherSet.const setA = new Set([1, 2, 3]); const setB = new Set([2, 3, 4]); const intersectionSet = setA.intersection(setB); // intersectionSet bevat 2, 3difference(otherSet): Retourneert een nieuwe set die de elementen bevat die in de huidige set zitten maar niet inotherSet.const setA = new Set([1, 2, 3]); const setB = new Set([2, 3, 4]); const differenceSet = setA.difference(setB); // differenceSet bevat 1
Via deconstructing kunnen één of meer elementen van een array eenvoudig toegewezen worden aan variabelen. Hierbij is
- de volgorde wel belangrijk
- de naam van de variabele is niet belangrijk
const accomplishments = ['Turing machine', 'Enigma code breaking', 'Turing test'];
const [firstAccomplishment, , accomplishment3] = accomplishments;
const [theFirstAccomplishment] = accomplishments;Via deconstructing kunnen één of meer properties van een object eenvoudig toegewezen worden aan variabelen. Hierbij is
- de volgorde niet belangrijk
- de naam van de variabele moet overeenkomen met de naam van de property in het object
const alan = {
firstName: 'Alan',
name: 'Turing',
birthYear: 1912,
deathYear: 1954,
age() {
return this.deathYear - this.birthYear;
},
}
// Deconstructing van het object.
// Merk op dat de age en deathYear properties niet gedeconstructed worden.
// Merk op dat name eerste komt in de objectdeinitie, maar alse laatste in de deconstructing.
const { birthYear, firstName, name } = alan;Via de Object.keys(), Object.values() en Object.entries() methodes kan je respectievelijk de keys, values of key-value paren van een object ophalen als een array. Deze arrays kunnen vervolgens geïtereerd worden met behulp van bijvoorbeeld een for...of lus.
const foo = {
a: 1,
b: 2,
c: 3,
}
// Itereren over de keys
for (const key of Object.keys(foo)) {
console.log(key) // 'a', 'b', 'c'
}
// Itereren over de values
for (const value of Object.values(foo)) {
console.log(value) // 1, 2, 3
}
// Itereren over de key-value paren
for (const pair of Object.entries(foo)) {
console.log(pair) // ['a', 1], ['b', 2], ['c', 3]
}Een object literal is een object dat rechtstreeks in de code gedefiniëerd wordt met behulp van accolades {}.
// Een object literal met waarden
const obj = {prop1: val1, prop2: val2, ...}
// Een object literal zonder waarden
const emptyObj = {}
// Een object met vijf properties
const person = {
firstName: "Alan",
lastName: "Turing",
birthYear: 1912,
deathYear: 1954,
}Een property van een object kan een methode zijn, dit is een functie die als eigenschap van het object gedefinieerd is.
Om gebruik te maken van andere properties binnen het object, kan de speciale variabele this gebruikt worden. Deze variabele verwijst naar de context waarbinnen de methode aangeroepen wordt, in dit geval het object zelf.
const person = {
firstName: "Alan",
lastName: "Turing",
birthYear: 1912,
deathYear: 1954,
age: function () {
return this.deathYear - this.birthYear
},
}Eigenschappen en methodes van een object kunnen op twee manieren uitgelezen worden, via puntnotatie en via vierkante haken notatie.
De puntnotatie is de handigste manier om properties uit te lezen, maar werkt als je
- De naam van de property kent en deze niet dynamisch is.
- De naam van de property een geldige identifier is (geen spaties, geen speciale tekens, ...), i.e. een geldige variabele naam.
objectnaam.eigenschap;
objectnaam.methode();De vierkante haken notatie is iets omslachtiger, maar laat toe om properties dynamisch uit te lezen of om eigenschappen te gebruiken die een spatie bevatten of andere speciale tekens.
objectnaam['eigenschap met een spatie'];
const methodName = 'foo'
objectnaam[methodName]();Elke HTMLElement heeft verschillende attributen waarmee extra informatie over het element meegegeven kan worden.
Bijna elk attribuut is beschikbaar als property of het HTMLElement. Dit betekent dat het mogelijk is om de waarde van een bestaand attribuut uit te lezen of een nieuwe waarde toe te kennen zoals we dat zouden doen voor elk ander object.
CSS-eigenschappen kunnen op dezelfde manier aangepast worden, let hier wel op, de CSS-eigenschappen staan nu in camelCase in plaats van kebab-case.
Sommige attributen heeft een bepaalde betekenis binnen JavaScript en kunnen daarom niet als propertynaam gebruikt worden. Zo wordt class gebruikt om een klasse aan te maken, maar ook om een CSS-klasse toe te voegen aan een HTMLElement, de property wordt daarom vervangen met className.
// Maak een nieuw hidden element aan.
const errorMessage = document.createElement('p')
errorMessage.hidden = true
// Pas de style van een element aan via een geneste property.
errorMessage.style.color = 'red'
errorMessage.style.border = '1px solid red'
errorMessage.style.borderRadius = '5px'
// Pas de style aan door een CSS-klasse toe te voegen.
errorMessage.className = 'error-message'
// Lees de waarde van een formulierelement uit.
const nameInput = document.getElementById('name-input')
const name = nameInput.value- element.hasAttribute(attributeName): Controleert of een element een opgegeven attribuut heeft.
- element.setAttribute(attributeName, attributeValue): Wijzigt de waarde van een attribuut.
- element.getAttribute(attributeName): Geeft de waarde van een attribuut terug.
- element.removeAttribute(attributeName): Verwijdert een attribuut van een element.
- element.toggleAttrivute(attributeName): Verandert de waarde van een boolean attribuut van true naar false of omgekeerd.
createElement: Maak een nieuw element aan van het opgegeven type. Via de eerste parameter wordt type gespecifieerd, dit kan eender welk HTML-tag zijn.
const title = document.createElement('h1')appendChild: Voeg een HTML-element toe als een kind aan een ander HTML-element.
const title = document.createElement('h1') document.body.appendChild(title)removeChild: Verwijder een HTMLElement uit een ander element. Het element moet steeds verwijderd worden van de directe parent.
// 1. Een element moet steeds uit de directe parent verwijderd worden. const parent = document.getElementById("myList"); // 2. Haal het kind op dat verwijderd moet worden. const child = document.querySelector("#myList > #item1"); // 3. Verwijder het kind. parent.removeChild(child);replaceChild: Vervang een element met een ander.
// Een element kan enkel vervangen worden in de directe parent. const parent = document.getElementById("myList"); const old = document.getElementById("someItem"); const newItem = document.createElement('div') parent.replace(newItem, old)remove: Verwijder een element uit de DOM.
document.getElementById('someId').remove()
Alle events bubbelen per default naar boven, hierdoor is het mogelijk om een event te koppelen aan een parent element dat toch reageert op dingen in de kinderen. Een click event dat gekoppeld wordt aan het body tag, zou dus reageren op elke klik op de volledige pagina.
Dit gedrag kan geannuleerd wordt via de stopPropagation methode die beschikbaar is op elke instantie van de Event klasse.
De inhoud van een element kan via drie properties uitgelezen of aangepast worden:
innerHTML: Geeft de HTML-code terug die als kind meegegeven is aan het element.
Waarschuwing
Gebruik deze property nooit om willekeurige HTML-code toe te voegen aan een website, hier zouden script tags of andere dingen in kunnen zitten die gebruikt kunnen worden voor een Cross-site scripting (XSS) aanval.
<html lang="en"> <body> <div id="inhtml"> <span>Hello <span style="display: none;">World</span></span> </div> <script> const inHtml = document.getElementById('inhtml') // Print <span>Hello <span style="display: none;">World</span></span> console.log(inHtml.innerHTML) // Past de inhoud aan naar een titel met de H1 styling. inHtml.innerHTML = '<h1>Inner HTML</h1>' </script> </body> </html>innerText: Geeft de gerenderde text terug. Inhoud die via CSS of accessibility attributes verborgen is, wordt hier niet weergegeven.
<html lang="en"> <body> <div id="inTxt"> <span>Hello <span style="display: none;">World</span></span> </div> <script> const inTxt = document.getElementById('inTxt') // Print Hello console.log(inTxt.innerHTML) // Past de inhoud aan naar de string <h1>Inner HTML</h1>, tags worden genegeerd. inTxt.innerText = '<h1>Inner HTML</h1>' </script> </body> </html>textContent: Geeft alle textinhoud van een element, inclusief spaties en newlines. Ook dingen die door CSS of accessibility attributes verborgen zijn, worden weergegeven.
<html lang="en"> <body> <div id="txtContent"> <span>Hello <span style="display: none;">World</span></span> </div> <script> const txtContent = document.getElementById('txtContent') // Print "\n Hello World\n" console.log(txtContent.textContent) // Past de inhoud aan naar de string <h1>Inner HTML</h1>, tags worden genegeerd. txtContent.innerText = '<h1>Inner HTML</h1>' </script> </body> </html>
Events worden afgevuurd om de code te verwittigen dat er iets interessant gebeurd is. De bron van deze events is meestal een actie van de gebruiker, e.g. het klikken op een knop, het herschalen van het venster, het typen in een formulier, ... In sommige gevallen kan een event ook veroorzaakt worden door gebeurtenis op het systeem van de gebruiker, zo kan een bijna lege batterij of een wijziging in de netwerkverbinding ook JavaScript code triggeren. De volledige lijst van events is beschikbaar op mdn.
Events kunnen op twee manieren gekoppeld worden aan JavaScript code. Aan HTML-attributen zoals onclick, onchange, ... kan JavaScript code meegegeven worden, anderszijds kan een event gekoppeld worden aan HTMLElement via de addEventListener methode.
De tweede aanpak geniet in alle gevallen de voorkeur, via de addEventListener methode wordt altijd een parameter meegegeven aan de handler functie die informatie over het event bevat. Wat deze informatie juist is, verschilt van event tot event. Een MouseEvent bevat informatie over hoe de mouse bewogen is (boven-onder, links-rechts, snelheid), een ChangeEvent bevat informatie over de waarde die in het formulierelement ingegeven is, een KeydownEvent bevat de knop die ingedrukt is, ...
<html lang="en">
<head>
<title>Voorbeeld</title>
</head>
<body>
<button onclick="showAlert1()">Click Me!</button>
<button id="second-button">Click Me V2!</button>
<script>
function showAlert1() {
alert('Hello World!')
}
document.getElementById('second-button')
.addEventListener('click', showAlert2)
function showAlert2(eventInfo) {
console.log(eventInfo)
alert('Hello World!')
}
</script>
</body>
</html>JavaScript voorziet verschillende methodes om elementen op te halen uit de DOM. Bijna al van deze methodes geven een live referentie naar één of meerdere HTML-elementen terug, dit betekent dat de returnwaarde altijd gesynchroniseerd is met de DOM. De enigste uitzondering hierop is de querySelectorAll methode. Als iets gewijzigd wordt in de browser (door een actie van de gebruiker) wordt de returnwaarde ook bijgewerkt, en omgekeerd wordt elke wijziging die via JavaScript gedaan wordt, ook onmiddelijk zichtbaar op de browser.
We onderscheiden binnen deze methodes drie verschillende returntypes.
Methodes die één Element teruggeven
Een Element stelt exact één tag in de DOM voor, één paragraaf, één afbeelding, één div, ...
getElementById: Deze methode krijgt één parameter, het id van het op te halen element.
const someElement = document.getElementById('someId') someElement.style.color = '#FFF'querySelector: Deze methode krijg een CSS-selector als argument en geeft het eerste overeenkomstige element terug.
const someElement = document.querySelector('#someId') someElement.style.color = '#FFF'
Methodes die een HTMLCollection teruggeven
Een HTMLCollection is een verzameling van één of meerdere Element items. Op deze verzameling zijn de array-methodes zoals forEach, map, filter, ... niet beschikbaar. Om over de collectie te itereren moet de collectie omgevormd worden naar een array, of moet een klassieke lus gebruikt worden.
getElementByClassName: Deze methode krijgt één parameter, de (CSS) klasse van de elementen die opgehaald moeten worden. Aangezien een klasse per definitie aan meerdere elementen toegekend kan worden, geeft deze methode altijd een collectie terug.
const elementsWithWhiteText = document.getElementsByClassName('white-text') // Optie één: converteer naar een array. Array.from(elementsWithWhiteText).forEach(elem => elem.style.color = '#FFF') // Optie twee: klassiek lus for (const elem of elementsWithWhiteText) { elem.style.color = '#FFF' }getElementsByTagName: Deze methode heeft één parameter, de naam van het HTML-tag dat opgehaald moet worden. Aangezien eenzelfde tag per definitie meerdere keren kan voorkomen in een HTML-document, geeft deze methode altijd een verzameling terug.
const paragraphs = document.getElementsByTagName('p') // Optie één: converteer naar een array. Array.from(paragraphs).forEach(elem => elem.style.borderLeft = '1px solid black') // Optie twee: klassiek lus for (const elem of paragraphs) { elem.style.borderLeft = '1px solid black' }
Methodes die een NodeList teruggeven
Een NodeList is een verzameling van één of meer Element items, maar in tegenstelling tot een HTMLCollection, is de forEach methode hier wel beschikbaar (andere methodes blijven onbruikbaar).
getElementsByName: Deze methode krijgt één parameter, de waarde van het name attribuut van een formulier element. Aangezien meerdere formulier elementen hetzelfde name attribuut kunnen hebben (checkboxes, radio button, ...) geeft deze methode altijd een verzameling van elementen terug.
const selectedNewslettersElements = document.getElementsByName('newsletter') const selectedNewsletters = [] selectedNewslettersElements.forEach(elem => elem.checked && selectedNewsletters.push(elem.value))querySelectorAll: Deze methode heeft één parameter, een CSS-selector en geeft alle elementen terug die overeen komen met de opgegeven selector.
const quotes = document.getElementsByTagName('p.quote') for (const elem of paragraphs) { elem.style.borderLeft = '3px solid black' elem.style.paddingLeft = '1em' elem.style.marginLeft = '1em' }
Het <script> element wordt gebruikt om JavaScript code te linken in een HTML-pagina. Via het src attribuut kan een link naar een externe JavaScript file toegevoegd worden.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Voorbeeld</title>
</head>
<body>
<script src="/index.js"></script>
</body>
</html>prompt('Dit werkt')Indien gewenst kan de JavaScript-code ook in de body van het <script> tag geplaatst worden, in dat geval wordt het src attribuut natuurlijk niet gebruikt.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Voorbeeld</title>
</head>
<body>
<script>
prompt('Dit werkt')
</script>
</body>
</html>In beide gevallen plaats je het <script> element onderaan de pagina, op deze manier garandeer je dat de HTML-pagina geladen is voordat je JavaScript code uitvoert. Het voordeel hiervan is tweeledig, enerzijds zal het script zo geen fouten vertonen omdat de dingen die gemanipuleerd worden nog niet bestaan, maar anderzijds wordt de pagina zo ook sneller geladen. Voor de gebruiker is de HTML/de UI het belangrijkst. Die moet zo snel mogelijk op het scherm komen. Als de JavaScript code eerst verwerkt wordt door de browser, kan dit het tekenen van de UI vertragen.
De fetch methode kan gebruik worden om informatie op te halen of weg te schrijven naar een API.
De methode heeft één verplichte parameter, de URL die aangeroepen moet worden. Verder kan optioneel een HTTP-methode (GET, POST, PUT, PATCH, DELETE), body of header toegevoegd worden.
fetch(
'http://example.com',
{
// OPTIONEEL: De HTTP-methode, GET is de default.
method: 'GET',
// OPTIONEEL: De body van het request.
// Enkel beschikbaar voor POST, PUT en PATCH.
// Meestal een JSON-Object, maar kan ook een Blob of FormData zijn.
body: JSON.stringify({}),
// OPTIONEEL: Extra HTTP-headers.
headers: {
'Content-Type': 'application/json',
'Api-Key': 'amkqjq24lnsfnpiohz1',
},
},
)Het resultaat van een fetch-call is steeds een promise van een Response object. Het result-object heeft onder andere volgende properties:
status: De HTTP Status Code, 200 voor success, 500 voor een internal server error, ...text(): Een methode die het antwoord verwerkt en teruggeeft als een promise van plain-text.json(): Een methode die het antwoord verwerkt en teruggeeft als een promise van een JavaScript object.blob(): Een methode die het antwoord verwerkt en teruggeeft als een promise van een blob (een binary large object). Dit kan bijvoorbeeld gebruikt worden voor afbeeldingen, pdf bestanden, Word bestanden, ...
Een interface is een manier om de vorm van een object te definiëren in TypeScript. Een interface specificeert welke eigenschappen (en soms methodes) een object moet hebben, zonder dat het bepaalt hoe deze geïmplementeerd worden.
In tegenstelling tot objectgeoriënteerde talen zoals C# en Java worden interfaces enkel gebruikt in combinatie met klassen. In TypeScript is dit niet het geval, een interface wordt doorgaans gebruikt om data in een variabele te beschrijven en wordt zelden gebruikt om het contract van een klasse vast te leggen.
Eens een interface gedefinieerd is, kan deze gebruikt worden als elk ander TypeScript type. Het is dus mogelijk om union types, arrays, ... van interfaces te definiëren.
interface Person {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
// Een optionele property.
yearOfDeath?: number
}
// Door hier : Person toe te voegen, garanderen we
// dat de correcte properties in het object zitten.
// In dit geval kan TypeScript het type niet afleiden en
// moet dit expliciet aangegeven worden.
const alanTuring: Person = {
name: 'Turing',
firstName: 'Alan',
accomplishments: [
'Turing machines',
'Turing test',
'Enigma codebreaker',
],
yearOfBirth: 1912,
yearOfDeath: 1954,
}Moderne JavaScript code maakt gebruik van ES Modules (ESM) om code te structureren in JavaScript en TypeScript projecten[1]. Via modules kan code verspreid worden over verschillende bestanden, zo blijft de code overzichtelijk, ook in grote projecten. Variabelen en functies die in het éné bestand geëxporteerd worden, kunnen in een ander bestand geïmporteerd worden.
Om met modules te werken moet het <script> als volgt aangepast worden:
<script type="module" src="/src/main.ts"></script>In een project met ES modules, worden twee soorten exports onderscheiden, named exports en default exports Via named exports leg je de naam van de variabele of functie vast, het is dus onmogelijk om de export te importeren onder een andere naam. Een bestand kan een willekeurig aantal named exports bevatten, deze worden allemaal samengevoegd in een object. Tijdens import moet deconstructing gebruikt worden om de verschillende named exports te importeren.
Default exports zijn exports die onder eender welke naam geïmporteerd kunnen worden. Zoals de naam doet vermoeden is een default export de "standaard" export van een bestand, het volgt dus dat er maar één default export per file kan zijn.
// Een named export, moet geïmporteerd worden onder de naam helloWorld
// en moet binnen accolades staan.
export const helloWorld = () => console.log('Hello World')
// Een default export, kan onder eender welke naam geïmporteerd worden
// en moet niet binnen accolades staan.
const helloWorlDefault = () => console.log('Hello World')
export default helloWorlDefault// Import van een named export, accolades zijn vereist.
import {helloWorld} from './helloWorld.ts'
// Import van een default export, accolades zijn niet nodig.
import helloWorlDefault from './helloWorld.ts'
// Import van een defauklt export, onder aan andere naam.
import aRenamedHalloWorldDefaultExport from './helloWorld.ts'De nullish coalescing operator is een logische operator die het rechterlid teruggeeft als het linkerlid null of undefined is, en anders het linkerlid.
console.log(null ?? 'Dit wordt teruggegeven')
console.log(undefined ?? 'Dit wordt teruggegeven')
console.log('' ?? 'Dit wordt NIET teruggegeven')De optional chaining operator (?.) controleert of het linkerlid gedefinieerd is, voordat het rechterlid verder geëvalueerd wordt. Via deze operator kunnen TypeErrors voorkomen worden zonder dat hiervoor uitgebreide if-else constructies geschreven moeten worden.
interface Profile {
firstName: string
lastName: string
hobbies?: string[]
}
const alan: Profile = {
firstName: 'Alan',
lastName: 'Turing',
}
// De eerste hobby uitloggen zonder optional chaining.
if (alan.hobbies) {
console.log(alan.hobbies.at(0))
}
// De eerste hobby uitloggen met optional chaining
console.log(alan.hobbies?.at(0))Meestal kan een variabele beschreven worden met primitief type of via een interface, soms is het echter nodig om iets te beschrijven dat niet in een interface past.
Denk hierbij aan coördinaten in een twee-dimensionale ruimte, we weten dat dit steeds een paar is, en dat er twee getallen bewaard moeten worden. We zouden hiervoor onderstaande interface kunnen gebruiken.
interface Coordinate {
xCoordinate: number
yCoordinate: number
}Alhoewel dit werkt, is het redelijk omslachtig, zeker als we dit type zouden willen uitbreiden naar meer dimensies. Via een type alias kan dit herschreven worden als
type Coordinate = [number, number]Ook in andere situaties kan een type alias nuttig zijn, bijvoorbeeld als je een string variabele hebt die slechts een beperkt aantal waardes kan aannemen.
type Position = 'left' | 'right' | 'top' | 'bottom'TypeScript ondersteund, onder anderen, volgende types:
number: Een getal, zowel kommagetallen als integers worden gedefinieerd door dit type. TypeScript maakt geen onderscheid tussen een integer, float of double.string: Een tekstwaarde.boolean: True of false.x[]: Een array van type , waar één van de andere types in deze lijst is.x[][]: Een tweedimensionale array van type , kan uitgebreid worden naar meer dimensies.(x | y): Een union type, waarden van type en worden allebei geaccepteerd, kan uitgebreid worden naar meerdere types.[x, y, ..., z]: Een tuple met een vast aantal elementen die elk een vast type hebben, hier zijn , en dus types uit deze lijst.Record<T, K>: Een object met keys van het type die verwijzen naar informatie van het type , een eenvoudig object dus.Record<string, number>is dus een object dat strings als sleutel heeft en een getal als bijhorende waarde.[2]any: Eender welk type, gebruik dit zo min mogelijk.unknown: Een veiligere versie vanany, alles kan toegekend worden aan eenunknownvariabele, maar je kan verder niets doen met deze variabele tenzij je expliciet controleert of de variabele een bepaald type heeft.
Een abstracte klasse is een klasse die niet geïnstantieerd kan worden, maar bedoeld is als basis voor andere klassen. Een abstracte klasse bevat een combinatie van concrete en abstracte methoden, een concrete methode is een methode die een body heeft en niet noodzakelijk overschreven moet worden in een subklasse. Een abstracte methode bevat daarentegen geen body en moet door een subklasse worden geïmplementeerd.
Een abstracte klasse dwingt af dat alle afgeleide klassen bepaalde methoden implementeren. Dit bevordert structuur, hergebruik van code en maakt je programma flexibeler en beter onderhoudbaar.
abstract class AbstractClass {
// Abstract method
abstract abstractMethod(): void;
// Concrete method
concreteMethod() {
console.log('This is a concrete method');
}
}Bekijk de cursus OOP of wikipedia voor meer informatie.
JavaScript ondersteunt een gelimiteerd aantal concepten die gekend zijn uit de cursus object oriented programming. Klassen met public of private properties en methodes, static members en overerving via het extends keyword, kunnen gebruikt worden in elke browser.
TypeScript voeg nog heel wat dingen toe, zoals: interfaces, abstracte klassen, de protected en readonly modifiers, syntactische suiker voor getters en setters en generics.
Statische klassen bestaan niet, noch in JavaScript noch in TypeScript. Statische klassen zijn enkel nodig omdat in objectgeoriënteerde talen historische alle code in een klasse moet staan. Dit is in JavaScript niet het geval, bijgevolg kunnen statische methodes gewoon als functies geëxporteerd worden, of eventueel als onderdeel van een gewoon object.
// Een interface die een methode vastlegt.
import Bar from './bar'
interface Foo {
fooMethod: () => void;
}
// Een abstracte klasse die overerft van de interface Foo
abstract class Bar implements Foo {
// Private properties worden met een # aangeduid.
#somePrivateProperty: string
protected someProtectedProperty: string
somePublicProperty: string
readonly somePublicConstantPropert = 'Value'
// Een abstracte methode die verplicht geïmplementeerd
// moet worden in een conrete subklasse.
abstract someAbstractMethod(): void
fooMethod(): void {
// ... body
}
// Een publieke getter voor een private property.
get somePrivateProperty() {
return this.#somePrivateProperty
}
// Een publieke setter voor de private property.
set somePrivatePropertySetter(value: string) {
this.#somePrivateProperty = value
}
}
class ConcreteBar extends Bar {
#somePrivateProperty: string
someAbstractMethod(): void {
// Body
}
}Een custom element is een zelfgedefinieerd HTML-element waarvan de werking en UI volledig bepaald wordt door de programmeur. Het element kan, na registratie bij de browser, gebruikt worden als eender welk ander HTML-element.
// Een minimaal custom element.
class HelloWorld extends HTMLElement {
static observedAttributes = ["foo"]
constructor() {
// Verplichte call naar de constructor van de superklasse.
super()
}
// Wordt uitgevoerd nadat het element gekoppeld is.
// Moet de UI opbouwen.
connectedCallback() {
const helloWorld = document.createElement('h1')
helloWorld.innerText = 'Hello world!'
// This verwijst hier naar een HTMLElement (want daarvan erven we over)
this.appendChild(helloWorld)
}
// Wordt uitgevoerd als een attribuut van het element verandert.
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
}
// Registreer een nieuw HTML element <hello-world></hello-world> bij de browser.
// De eerste parameter is de naam van het element, de tweede de constructor.
// De naam van het element moet een koppelteken bevatten.
customElements.define('hello-world', HelloWorld);
// Element kan opgeroepen worden als: <hello-world foo="someValue"></hello-world>Generics maken het mogelijk om functies, klassen en interfaces te definiëren die werken met verschillende datatypes zonder dat je deze individueel moest vastleggen. Denk hier bijvoorbeeld aan de Array<T> klasse, deze kan werken met verschillende datatypes zoals string, number of boolean. De generic parameter T kan vervangen worden door eender welk datatype.
// De functie accepteerd een array van een bepaald datatype en
// retourneerd een array van hetzelfde datatype terug.
function doubleArray<T>(arr: T[]): T[] {
return [...arr, ...arr]
}
// Het type kan expliciet meegegeven worden tussen <>,
// dit is duidelijker maar niet altijd noodzakelijk.
doubleArray<number>([1, 2, 3]) // [1, 2, 3, 1, 2, 3]
// Het type meestal impliciet afgeleid worden van de waarde die je meegeeft.
// Aangezien dit een eenvoudige functie is, weet TypeScript dat
// het resultaat number[] (Array<number>) is.
doubleArray([1, 2, 3]) // [1, 2, 3, 1, 2, 3]Observer is een ontwerppatroon in de object-georiënteerde softwareontwikkeling. Het patroon maakt het mogelijk om data centraal te beheren en te synchroniseren met meerdere objecten zonder dat deze objecten constant moeten pollen voor updates.
Het centrale object (het "subject", hieronder de PersistenceProvider) houdt een lijst bij van "observers" (hieronder PageA en PageB). Een observer komt pas in de lijst te staan als deze zich registreert bij het subject (hieronder via de addObserver methode). Via deze methode geeft je een callback functie door die wordt aangeroepen als de data verandert, deze callback functie wordt dan opgeslagen in de lijst van observers in het centrale object (hieronder PersistenceProvider).
Wanneer de data verandert, roept het subject de notifyObservers methode aan, die op zijn beurt door de lijst van observers itereert en de callback functie aanroept met de nieuwe data.
De Web Storage API biedt een manier om gegevens op te slaan in de browser van de gebruiker.
Web Storage is bedoeld om eenvoudige string gegevens op te slaan die je als geheel wilt ophalen en die eenvoudig reproduceerbaar zijn. De hoeveelheid data die via Web Storage kan worden opgeslagen, is beperkt tot ongeveer 5 MB per origin (protocol (http/https) + domain + poort), verder kan de browser deze gegevens automatisch wissen als er een tekort aan opslagruimte is.
Web Storage wordt opgedeeld in twee verschillende opslagmethoden: localStorage en sessionStorage. LocalStorage is persistent doorheen verschillende sessies, terwijl sessionStorage alleen geldig is voor de huidige sessie. Dit betekent dat gegevens die in localStorage zijn opgeslagen, beschikbaar blijven, zelfs als de gebruiker het tabblad of de browser sluit. SessionStorage daarentegen is alleen beschikbaar zolang het tabblad niet afgesloten wordt. Als localStorage gebruikt wordt in incognito-modus, is de opslag ook tijdelijk en wordt deze gewist zodra het venster gesloten wordt, de data is dus langer beschikbaar dan sessionsStorage, maar niet zo lang als localStorage in een normaal venster.
Data die in localStorage bewaard is, blijft gegarandeerd bestaan tot dat (a) de gebruiker de browsergeschiedenis wist of (b) de maximale opslag voor de origin bereikt is of (c) de totale maximale opslag voor de Storage API bereikt is (tussen de 10 en 60% van de totale schrijfruimte, afhankelijk van de browser).
// Analoge API voor sessionStorage
window.localStorage.setItem('key', 'value')
window.localStorage.getItem('key')
window.localStorage.removeItem('key')
window.localStorage.clear()Via de ComponentProps helper is het mogelijk om een interface/type te generen voor een component die de verwachte properties niet exporteert als interface/type.
De helper ook gebruikt worden om de properties van een klassiek HTML-element te weten te komen, maar hiervoor zijn binnen React ook interfaces voor voorzien die je rechtstreeks kunt importeren.
import {ComponentProps} from 'react'
import Foo from 'foo'
type fooProps = ComponentProps<typeof Foo>
type inputProps = ComponentProps<'input'>Omdat React Native de UI die in JavaScript geschreven is converteert naar native UI-elementen, is het aantal componenten die door React Native aangeboden worden eerder beperkt. Klassieke HTML tags kunnen niet gebruikt worden.
Elk van de core components is gelinkt aan een vast UI-element in de Android of iOS SDK. Alhoewel de standaard componenten eenvoudig zijn, kunnen deze wel gebruikt worden om complexere en grotere componenten te bouwen.
Het is belangrijk om op te merken dat verschillende cruciale componenten, zoals een checkbox, radio button, ... niet beschikbaar zijn in React Native. Als je deze wilt gebruiken, moet je de component zelf bouwen, of gebruikmaken van een community library.
MMKV is een zeer snelle (en kleine) key-value storage library ontwikkeld door het Chinese WeChat. Deze library presteert in benchmarks aanzienlijk beter de SharedPreferences en UserDefaults, die respectievelijk voorzien worden door Android en iOS.
MMKV kan gebruikt worden om kleine, niet relationele, data te bewaren of om het resultaat van een HTTP request lokaal te cachen. Via de react-native-mmkv library kan MMKV gebruikt worden in een React Native applicatie.
Via de useMMKV... hooks kunnen we (een stuk van) een object of een primitieve waarde bewaren.
const Foo: FuctionComponent = () => {
const [someString, setSomeString] = useMKKVString('sleutel1')
const [someBoolean, setSomeBoolean] = useMKKVBoolean('sleutel2')
const [someNumber, setSomeNumber] = useMKKVNumber('sleutel3')
const [someObject, setSomeObject] = useMKKVObject<T>('sleutel4')
return <> {/* JSX-Code */} </>
}Het package id van je app is een wereldwijd unieke naam die gebruikt wordt om te bepalen of een app al geïnstalleerd is op een toestel en voor een hele reeks andere doeleinden.
Het package id moet in reverse domain name notation formaat genoteerd worden. Als je een app bouwt met als doel deze te publiceren, in plaats van om te leren, is het natuurlijk belangrijk dat je hiervoor een domeinnaam gebruikt die je effectief bezit. Domeinnamen zijn uniek en twee apps zullen dus nooit als "dezelfde app" beschouwd worden.
Expo bewaart de package name in app.json, je kan de naam altijd aanpassen in deze file.
Notitie
Gebruik tijdens deze cursus een appId van de vorm com.achternaam.voornaam.appnaam. De apps die als voorbeeld aangeboden worden krijgen steeds een id van de vorm be.pitgraduaten.appnaam.
Om de opmaak van een React Native applicatie te implementeren wordt iets gebruikt dat op CSS lijkt, maar niet helemaal overeen komt. Er zijn enkele belangrijke verschillen tussen CSS en React Native styling.
- Id's, klassen en andere CSS-selectors kunnen niet gebruikt worden
- Lay-out gebeurt uitsluitend met flexbox. Er zijn wel enkele verschillen met CSS-flexbox.
- flexDirection heeft column als default
- alignContent heeft flex-start als default
- flexShrink heeft 0 als default
- flex kan slechts een waarde krijgen, een getal.
- Er zijn twee mogelijke eenheden rem, em, px, ... worden niet gebruikt.
- Als de hoogte en/of breedte zonder eenheid (en als integer) genoteerd wordt, verwijst dit naar density-independent pixels.
- Als de hoogte en/of breedte als '%' genoteerd wordt, verwijst dit naar procentuele afmetingen.
- Een View component moet een vaste hoogte of breedte hebben of moet de regel flex: krijgen waarbij een integer groter dan of gelijk aan is. Als dit niet het geval is, zijn de kinderen niet zichtbaar.
- Er is GEEN overerving van View naar Tekst, dit betekent dat dingen als tekst kleur voor elke Text component individueel aangepast moet worden. Behalve als de Text component in een andere Text component genest is.
Opmaak wordt gedefinieerd via de StyleSheet namespace en wordt volledig in JavaScript genoteerd.
import {View, Text, StyleSheet} from 'react-native'
import {FunctionComponent} from 'react'
import {StatusBar} from 'expo-status-bar'
const Foo: FunctionComponent = () => {
return (
<View style={styles.container}>
<Text style={[styles.textStyle, styles.strikeThrough]}>
Je kan één of meer stijlen toekennen aan een component.
De laatste stijl heeft voorrang.
De kleur van deze tekst is dus rood.
</Text>
<StatusBar style="auto" />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#282c34',
alignItems: 'center',
justifyContent: 'center',
},
textStyle: {
color: '#fff',
fontSize: 18,
},
strikeThrough: {
color: 'red',
textDecorationLine: 'line-through',
},
})De useColorScheme hook kan gebruikt worden om het voorkeursthema van de gebruiker uit te lezen.
De hook geeft een string terug die één van onderstaande waarden heeft:
- light: De gebruiker verkiest een licht thema
- dark: De gebruiker verkiest een donker thema
- null: De gebruiker heeft geen voorkeur opgegeven. Normaliter is dit enkel het geval op oudere toestellen waar deze optie nog niet beschikbaar was.
import {useColorScheme} from 'react-native'
const Foo: FunctionComponent = () => {
const colorScheme = useColorScheme()
if (colorScheme === 'light') {
return <>{/* A light styled component */}</>
} else if (colorScheme === 'dark') {
return <>{/* A dark styled component */}</>
} else if (colorsScheme === null) {
return <>{/* A component styled in the app's prefered theme */}</>
}
}Binnen de Expo Router kunnen layout routes aangemaakt worden.
Dit zijn bestanden die:
- Verplicht de naam _layout.tsx krijgen
- Code definiëren die rond alle kind routes geplaatst worden
- Navigator componenten bevatten voor een stack, tabs, drawer, ... layout
- De Slot component gebruiken in de plaats van children om de kind routes te renderen
In de standaard configuratie evalueert TypeScript imports enkel relatief ten opzichte van de huidige directory of de Nodemodules_. Alhoewel dit voldoende is om een volledig pad te schrijven, is het dikwijls interessant om ook uit andere mappen te kunnen importeren zonder dat je hiervoor een lang relatief pad moet vermelden. Op deze manier worden import statements korter en is het duidelijker vanwaar code komt, een pad als '@hooks/someHook' is veel properder en duidelijker dan iets als '../../../lib/hooks/someHooks'.
Zulke path aliases kunnen geconfigureerd worden in tsconfig.ts. Via compilerOptions.paths kan een alias gemapt worden naar één of meer mappen waarnaar deze verwijst.
Onderstaande configuratie voegt de alias '@' toe waarmee elementen uit de root map van het project ingeladen kunnen worden.
{
...
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./*"
],
}
},
...
}De useRef hook kan gebruikt worden om een specifiek HTML-element rechtstreeks aan te spreken en biedt daarmee een alternatief voor de document.getElementById methode die gekend is uit de JavaScript cursus.
Omdat de useRef hook niet steunt op CSS-klassen, ID's of query-selectors is dit een stabielere keuze dan de gekende document.x methodes.
Via de reference kunnen we HTML elementen rechtstreeks manipuleren, bijvoorbeeld om de focus te zetten op een inputveld.
In React Native is er natuurlijk geen klassieke DOM, maar de useRef hook kan nog steeds op dezelfde manier gebruikt worden. Bijvoorbeeld om een TextInput component in focus te brengen.
const SomeComponent: FunctionComponent = () => {
const htmlRef = useRef<HTMLDivElement>(null)
return (
<div>
{/**
* Het element waaraan de ref gelinkt wordt
* moet natuurlijk geen div zijn.
**/}
<div ref={htmlRef}></div>
</div>
)
}Alhoewel ESM de beste manier is om code te structureren, gebruiken verschillende oudere libraries nog steeds het Common JS (CJS) formaat. We raden elke lezer aan om hier meer over te lezen. ↩︎
Maps hebben ongeveer dezelfde functionaliteit, maar hebben enkele voordelen, we verwijzen de geïnteresseerde lezer door naar MDN voor meer informatie; ↩︎