Component Lifecycle in LIT Elements ​
Inleiding ​
De levenscyclus van een component is een fundamenteel concept in webcomponent frameworks, inclusief LIT. Het begrijpen van deze levenscyclus stelt ontwikkelaars in staat om hun componenten correct te initialiseren, bij te werken en op te ruimen. In deze gids verkennen we de verschillende levenscyclusfasen van LIT elements en hoe je deze effectief kunt gebruiken in onze dcr-components.
Wat is de component lifecycle? ​
De component lifecycle verwijst naar de verschillende fasen die een component doorloopt vanaf het moment dat het wordt aangemaakt tot het moment dat het wordt verwijderd uit de DOM. LIT biedt een reeks lifecycle callbacks die worden aangeroepen op specifieke momenten, waardoor je code kunt uitvoeren op precies het juiste moment in de levensduur van een component.
Overzicht van de LIT lifecycle ​
De levenscyclus van een LIT element kan worden onderverdeeld in drie hoofdfasen:
- Initialisatie: Wanneer het element wordt aangemaakt en aan de DOM wordt toegevoegd
- Updates: Wanneer de eigenschappen of state van het element veranderen
- Verwijdering: Wanneer het element uit de DOM wordt verwijderd
Hier is een visuele weergave van de volledige levenscyclus:
constructor() → connectedCallback() → firstUpdated() → updated() → disconnectedCallback()
↑ |
└─────────────────┘
(bij elke update)Initialisatiefase ​
constructor() ​
De constructor() is het eerste wat wordt aangeroepen wanneer een LIT element wordt aangemaakt. Dit is waar je initiële setup kunt doen, zoals het instellen van standaardwaarden voor interne state.
// counter-element.ts
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import styles from './counter-element.scss';
@customElement('counter-element')
class CounterElement extends LitElement {
static styles = styles;
// Interne state
@state()
private count = 0;
constructor() {
super(); // Altijd super() aanroepen eerst!
console.log('Constructor aangeroepen');
// Initiële setup
this.addEventListener('keydown', this.handleKeyDown);
}
private handleKeyDown(e: KeyboardEvent) {
if (e.key === 'ArrowUp') this.increment();
if (e.key === 'ArrowDown') this.decrement();
}
private increment() {
this.count++;
}
private decrement() {
this.count--;
}
render() {
return html`
<div class="counter">
<p>Huidige telling: ${this.count}</p>
<button @click=${this.increment}>Verhoog</button>
<button @click=${this.decrement}>Verlaag</button>
</div>
`;
}
}Best practices voor de constructor:
- Altijd
super()aanroepen als eerste - Vermijd DOM-manipulatie (er is nog geen shadow DOM)
- Initialiseer alleen interne state en bind event handlers
- Voer geen zware berekeningen uit
- Roep geen async operaties aan
connectedCallback() ​
De connectedCallback() wordt aangeroepen wanneer het element aan de DOM wordt toegevoegd. Dit is een goed moment om resources te initialiseren die afhankelijk zijn van de DOM, zoals het opzetten van ResizeObservers of het laden van externe data.
// data-chart.ts
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import styles from './data-chart.scss';
@customElement('data-chart')
class DataChart extends LitElement {
static styles = styles;
@property({ type: String })
dataSource = '';
private resizeObserver?: ResizeObserver;
private chartData: any[] = [];
connectedCallback() {
super.connectedCallback(); // Altijd super.connectedCallback() aanroepen eerst!
console.log('Element toegevoegd aan DOM');
// Setup ResizeObserver
this.resizeObserver = new ResizeObserver(() => {
this.updateChartDimensions();
});
// Laad data
this.loadChartData();
}
private async loadChartData() {
if (this.dataSource) {
try {
const response = await fetch(this.dataSource);
this.chartData = await response.json();
this.requestUpdate(); // Trigger een update
} catch (error) {
console.error('Fout bij laden data:', error);
}
}
}
private updateChartDimensions() {
// Update chart afmetingen gebaseerd op container grootte
console.log('Chart dimensions updated');
}
render() {
return html`
<div class="chart-container">
${this.chartData.length ? html`<div class="chart"><!-- Chart rendering --></div>` : html`<p>Laden...</p>`}
</div>
`;
}
}Best practices voor connectedCallback:
- Altijd
super.connectedCallback()aanroepen als eerste - Initialiseer resources zoals observers en event listeners
- Start data loading of andere async operaties
- Vermijd directe DOM-manipulatie (gebruik LIT's rendering systeem)
- Gebruik
this.requestUpdate()als je een update wilt triggeren
firstUpdated() ​
De firstUpdated() callback wordt aangeroepen na de eerste keer dat de component is gerenderd en de shadow DOM is aangemaakt. Dit is het ideale moment om DOM-gerelateerde initialisatie te doen, zoals het focussen van elementen of het initialiseren van third-party libraries die DOM-toegang nodig hebben.
// search-field.ts
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import styles from './search-field.scss';
@customElement('search-field')
class SearchField extends LitElement {
static styles = styles;
@property({ type: Boolean })
autofocus = false;
// Query decorator voor directe toegang tot DOM elementen
@query('input')
inputElement!: HTMLInputElement;
firstUpdated() {
console.log('Component voor het eerst gerenderd');
// Nu kunnen we veilig DOM elementen benaderen
if (this.autofocus) {
this.inputElement.focus();
}
// Initialiseer third-party libraries die DOM-toegang nodig hebben
this.initializeAutocomplete();
}
private initializeAutocomplete() {
// Bijvoorbeeld: setup van een autocomplete library
console.log('Autocomplete geïnitialiseerd op', this.inputElement);
}
render() {
return html`
<div class="search-container">
<input type="text" placeholder="Zoeken..." />
<button @click=${this.handleSearch}>Zoek</button>
</div>
`;
}
private handleSearch() {
const searchTerm = this.inputElement.value;
this.dispatchEvent(
new CustomEvent('search', {
bubbles: true,
composed: true,
detail: { term: searchTerm },
})
);
}
}Best practices voor firstUpdated:
- Gebruik het voor DOM-gerelateerde initialisatie
- Initialiseer third-party libraries die DOM-toegang nodig hebben
- Focus elementen indien nodig
- Voer metingen uit die afhankelijk zijn van gerenderde elementen
- Vermijd zware berekeningen die de eerste render kunnen vertragen
Updatefase ​
willUpdate() ​
De willUpdate() methode wordt aangeroepen voordat een update wordt verwerkt, maar nadat de eigenschappen zijn bijgewerkt. Dit is een goed moment om berekeningen uit te voeren die nodig zijn voor de rendering, of om properties te synchroniseren.
// data-table.ts
import { LitElement, html, PropertyValues } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import styles from './data-table.scss';
interface TableData {
id: string;
name: string;
value: number;
}
@customElement('data-table')
class DataTable extends LitElement {
static styles = styles;
@property({ type: Array })
data: TableData[] = [];
@property({ type: String })
sortBy = 'name';
@property({ type: Boolean })
ascending = true;
@state()
private sortedData: TableData[] = [];
willUpdate(changedProperties: PropertyValues) {
// Check welke properties zijn gewijzigd
if (changedProperties.has('data') || changedProperties.has('sortBy') || changedProperties.has('ascending')) {
console.log('Hersorteren van data voor rendering');
this.sortData();
}
}
private sortData() {
// Maak een kopie om immutability te behouden
this.sortedData = [...this.data].sort((a, b) => {
const factor = this.ascending ? 1 : -1;
return a[this.sortBy] > b[this.sortBy] ? factor : -factor;
});
}
render() {
return html`
<table>
<thead>
<tr>
<th @click=${() => this.sort('id')}>ID</th>
<th @click=${() => this.sort('name')}>Naam</th>
<th @click=${() => this.sort('value')}>Waarde</th>
</tr>
</thead>
<tbody>
${this.sortedData.map(
(item) => html`
<tr>
<td>${item.id}</td>
<td>${item.name}</td>
<td>${item.value}</td>
</tr>
`
)}
</tbody>
</table>
`;
}
private sort(column: string) {
if (this.sortBy === column) {
this.ascending = !this.ascending;
} else {
this.sortBy = column;
this.ascending = true;
}
}
}Best practices voor willUpdate:
- Gebruik het voor berekeningen die nodig zijn vóór rendering
- Controleer welke properties zijn gewijzigd om onnodige berekeningen te voorkomen
- Synchroniseer properties en state
- Bereid data voor voor rendering
- Vermijd DOM-manipulatie (gebruik LIT's rendering systeem)
render() ​
De render() methode is het hart van een LIT component. Het wordt aangeroepen wanneer de component moet worden gerenderd, en moet een TemplateResult teruggeven die beschrijft hoe de DOM eruit moet zien.
// user-profile.ts
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import styles from './user-profile.scss';
@customElement('user-profile')
class UserProfile extends LitElement {
static styles = styles;
@property({ type: String })
username = '';
@property({ type: String })
avatarUrl = '';
@property({ type: Boolean })
isExpanded = false;
render() {
return html`
<div class="profile ${this.isExpanded ? 'expanded' : ''}">
${this.avatarUrl
? html`<img src=${this.avatarUrl} alt=${this.username} />`
: html`<div class="avatar-placeholder"></div>`}
<div class="info">
<h3>${this.username || 'Anonieme gebruiker'}</h3>
<button @click=${this.toggleExpand}>${this.isExpanded ? 'Minder' : 'Meer'} info</button>
${this.isExpanded ? this.renderDetails() : ''}
</div>
</div>
`;
}
private renderDetails() {
return html`
<div class="details">
<slot></slot>
</div>
`;
}
private toggleExpand() {
this.isExpanded = !this.isExpanded;
this.dispatchEvent(
new CustomEvent('expand-change', {
detail: { expanded: this.isExpanded },
})
);
}
}Best practices voor render:
- Houd het puur en voorspelbaar (geen side effects)
- Gebruik helper render-methoden voor complexe onderdelen
- Gebruik conditionele rendering voor dynamische UI
- Gebruik slots voor flexibele content projectie
- Vermijd directe DOM-manipulatie
updated() ​
De updated() callback wordt aangeroepen nadat een update is verwerkt en de DOM is bijgewerkt. Dit is een goed moment om DOM-gerelateerde acties uit te voeren die afhankelijk zijn van de nieuwe state.
// image-gallery.ts
import { LitElement, html, PropertyValues } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import styles from './image-gallery.scss';
@customElement('image-gallery')
class ImageGallery extends LitElement {
static styles = styles;
@property({ type: Array })
images: string[] = [];
@property({ type: Number })
activeIndex = 0;
@query('.gallery-container')
private galleryContainer!: HTMLElement;
updated(changedProperties: PropertyValues) {
// Check welke properties zijn gewijzigd
if (changedProperties.has('activeIndex')) {
console.log('Active index changed, scrolling to new image');
this.scrollToActiveImage();
}
if (changedProperties.has('images')) {
console.log('Images changed, resetting gallery');
this.resetGallery();
}
}
private scrollToActiveImage() {
const activeImage = this.shadowRoot?.querySelector(`.image-${this.activeIndex}`);
activeImage?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
});
}
private resetGallery() {
this.activeIndex = 0;
// Andere reset acties
}
render() {
return html`
<div class="gallery-container">
${this.images.map(
(src, index) => html`
<img
src=${src}
class="image-${index} ${index === this.activeIndex ? 'active' : ''}"
@click=${() => this.setActiveImage(index)}
/>
`
)}
</div>
<div class="controls">
<button @click=${this.prevImage} ?disabled=${this.activeIndex === 0}>Vorige</button>
<span>${this.activeIndex + 1} / ${this.images.length}</span>
<button @click=${this.nextImage} ?disabled=${this.activeIndex === this.images.length - 1}>Volgende</button>
</div>
`;
}
private setActiveImage(index: number) {
this.activeIndex = index;
}
private prevImage() {
if (this.activeIndex > 0) {
this.activeIndex--;
}
}
private nextImage() {
if (this.activeIndex < this.images.length - 1) {
this.activeIndex++;
}
}
}Best practices voor updated:
- Controleer welke properties zijn gewijzigd om onnodige acties te voorkomen
- Gebruik het voor DOM-manipulaties die afhankelijk zijn van de nieuwe state
- Integreer met third-party libraries die moeten reageren op state wijzigingen
- Vermijd acties die een nieuwe update triggeren zonder guard clauses
- Gebruik het voor side effects die moeten gebeuren na een render
Verwijderingsfase ​
disconnectedCallback() ​
De disconnectedCallback() wordt aangeroepen wanneer het element uit de DOM wordt verwijderd. Dit is het moment om resources op te ruimen, zoals event listeners, timers, en observers.
// video-player.ts
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import styles from './video-player.scss';
@customElement('video-player')
class VideoPlayer extends LitElement {
static styles = styles;
@property({ type: String })
videoSrc = '';
@query('video')
private videoElement!: HTMLVideoElement;
private resizeObserver?: ResizeObserver;
private updateInterval?: number;
connectedCallback() {
super.connectedCallback();
// Setup ResizeObserver
this.resizeObserver = new ResizeObserver(() => {
this.updatePlayerDimensions();
});
// Setup interval voor progress updates
this.updateInterval = window.setInterval(() => {
this.updateProgress();
}, 1000);
// Global event listeners
document.addEventListener('keydown', this.handleKeyDown);
}
disconnectedCallback() {
super.disconnectedCallback();
console.log('Element verwijderd uit DOM, ruim resources op');
// Cleanup ResizeObserver
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
// Cleanup interval
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
// Cleanup global event listeners
document.removeEventListener('keydown', this.handleKeyDown);
}
private handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ' && this.shadowRoot?.activeElement !== this.videoElement) {
this.togglePlay();
}
};
private updatePlayerDimensions() {
// Update player dimensions
}
private updateProgress() {
// Update progress bar
}
private togglePlay() {
if (this.videoElement.paused) {
this.videoElement.play();
} else {
this.videoElement.pause();
}
}
render() {
return html`
<div class="video-container">
<video src=${this.videoSrc} controls></video>
<div class="custom-controls">
<button @click=${this.togglePlay}>Play/Pause</button>
<!-- Andere controls -->
</div>
</div>
`;
}
}Best practices voor disconnectedCallback:
- Altijd
super.disconnectedCallback()aanroepen als eerste - Ruim alle resources op die je hebt aangemaakt in connectedCallback
- Verwijder event listeners, vooral globale
- Stop timers en intervals
- Disconnect observers
- Annuleer lopende async operaties indien mogelijk
Reactieve eigenschappen en de updatecyclus ​
LIT's updatecyclus wordt geactiveerd wanneer reactieve eigenschappen veranderen. Hier is hoe het werkt:
Reactieve eigenschappen definiëren ​
// feedback-form.ts
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import styles from './feedback-form.scss';
@customElement('feedback-form')
class FeedbackForm extends LitElement {
static styles = styles;
// Publieke API - properties
@property({ type: String })
heading = 'Feedback formulier';
@property({ type: String })
submitLabel = 'Verzenden';
@property({ type: Boolean, reflect: true })
disabled = false;
// Interne state - niet zichtbaar als attributen
@state()
private rating = 0;
@state()
private comment = '';
@state()
private submitted = false;
render() {
if (this.submitted) {
return html`
<div class="success">
<h2>Bedankt voor je feedback!</h2>
<button @click=${this.reset}>Nieuw formulier</button>
</div>
`;
}
return html`
<div class="form-container">
<h2>${this.heading}</h2>
<div class="rating">
${[1, 2, 3, 4, 5].map(
(star) => html`
<button
class="star ${star <= this.rating ? 'active' : ''}"
@click=${() => this.setRating(star)}
?disabled=${this.disabled}
>
★
</button>
`
)}
</div>
<textarea
.value=${this.comment}
@input=${this.handleCommentInput}
placeholder="Je feedback..."
?disabled=${this.disabled}
></textarea>
<button class="submit" @click=${this.submit} ?disabled=${this.disabled || !this.isValid}>
${this.submitLabel}
</button>
</div>
`;
}
private get isValid() {
return this.rating > 0;
}
private setRating(value: number) {
this.rating = value;
}
private handleCommentInput(e: Event) {
this.comment = (e.target as HTMLTextAreaElement).value;
}
private submit() {
if (!this.isValid) return;
this.dispatchEvent(
new CustomEvent('feedback-submit', {
bubbles: true,
composed: true,
detail: {
rating: this.rating,
comment: this.comment,
},
})
);
this.submitted = true;
}
private reset() {
this.rating = 0;
this.comment = '';
this.submitted = false;
}
}De updatecyclus ​
Wanneer een reactieve eigenschap verandert, wordt de volgende cyclus geactiveerd:
- Property waarde wordt bijgewerkt
requestUpdate()wordt automatisch aangeroepen- Update wordt gepland (microtask)
willUpdate()wordt aangeroepenrender()wordt aangeroepen- DOM wordt bijgewerkt
updated()wordt aangeroepen
Handmatig updates triggeren ​
Soms moet je handmatig een update triggeren, bijvoorbeeld wanneer een niet-reactieve waarde verandert:
// countdown-timer.ts
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import styles from './countdown-timer.scss';
@customElement('countdown-timer')
class CountdownTimer extends LitElement {
static styles = styles;
@property({ type: Number })
duration = 60; // seconden
private remainingTime = 0;
private timerId?: number;
connectedCallback() {
super.connectedCallback();
this.remainingTime = this.duration;
this.startTimer();
}
disconnectedCallback() {
super.disconnectedCallback();
this.stopTimer();
}
private startTimer() {
this.timerId = window.setInterval(() => {
this.remainingTime--;
// Handmatig een update triggeren
this.requestUpdate();
if (this.remainingTime <= 0) {
this.stopTimer();
this.dispatchEvent(new CustomEvent('countdown-complete'));
}
}, 1000);
}
private stopTimer() {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = undefined;
}
}
private resetTimer() {
this.stopTimer();
this.remainingTime = this.duration;
this.requestUpdate();
this.startTimer();
}
render() {
const minutes = Math.floor(this.remainingTime / 60);
const seconds = this.remainingTime % 60;
return html`
<div class="timer ${this.remainingTime < 10 ? 'warning' : ''}">
<span class="time">${minutes}:${seconds.toString().padStart(2, '0')}</span>
<button @click=${this.resetTimer}>Reset</button>
</div>
`;
}
}Lifecycle hooks en async operaties ​
Async operaties vereisen speciale aandacht in lifecycle hooks:
// user-data.ts
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import styles from './user-data.scss';
interface User {
id: string;
name: string;
email: string;
}
@customElement('user-data')
class UserData extends LitElement {
static styles = styles;
@property({ type: String })
userId = '';
@state()
private user: User | null = null;
@state()
private loading = false;
@state()
private error: string | null = null;
// Gebruik een setter om automatisch data te laden wanneer userId verandert
set userId(value: string) {
const oldValue = this.userId;
if (value !== oldValue) {
this._userId = value;
this.loadUserData();
}
}
get userId() {
return this._userId;
}
private _userId = '';
private abortController: AbortController | null = null;
async loadUserData() {
if (!this.userId) {
this.user = null;
return;
}
// Annuleer eventuele lopende requests
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
const { signal } = this.abortController;
try {
this.loading = true;
this.error = null;
const response = await fetch(`/api/users/${this.userId}`, { signal });
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
this.user = await response.json();
} catch (err) {
if (err.name !== 'AbortError') {
this.error = err.message || 'Er is een fout opgetreden';
this.user = null;
}
} finally {
if (!signal.aborted) {
this.loading = false;
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
// Annuleer lopende requests
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
}
render() {
if (this.loading) {
return html`<div class="loading">Gebruiker laden...</div>`;
}
if (this.error) {
return html`<div class="error">${this.error}</div>`;
}
if (!this.user) {
return html`<div class="empty">Geen gebruiker geselecteerd</div>`;
}
return html`
<div class="user-card">
<h3>${this.user.name}</h3>
<p>Email: ${this.user.email}</p>
<slot></slot>
</div>
`;
}
}Best practices voor async operaties:
- Gebruik AbortController om lopende requests te annuleren
- Houd rekening met race conditions
- Cleanup in disconnectedCallback
- Toon loading states voor betere UX
- Handel errors correct af
Geavanceerde lifecycle patronen ​
Lifecycle voor geneste componenten ​
Wanneer je werkt met geneste componenten, is het belangrijk om rekening te houden met de volgorde van lifecycle events:
// parent-component.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import './child-component';
@customElement('parent-component')
class ParentComponent extends LitElement {
connectedCallback() {
super.connectedCallback();
console.log('Parent connected');
}
firstUpdated() {
console.log('Parent first updated');
}
render() {
return html`
<div>
<h2>Parent Component</h2>
<child-component></child-component>
</div>
`;
}
updated() {
console.log('Parent updated');
}
disconnectedCallback() {
super.disconnectedCallback();
console.log('Parent disconnected');
}
}
// child-component.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('child-component')
class ChildComponent extends LitElement {
connectedCallback() {
super.connectedCallback();
console.log('Child connected');
}
firstUpdated() {
console.log('Child first updated');
}
render() {
return html`<div>Child Component</div>`;
}
updated() {
console.log('Child updated');
}
disconnectedCallback() {
super.disconnectedCallback();
console.log('Child disconnected');
}
}De console output zal zijn:
Parent connected
Child connected
Child first updated
Child updated
Parent first updated
Parent updatedBij verwijdering:
Parent disconnected
Child disconnectedControllers voor herbruikbare lifecycle logica ​
Controllers zijn een krachtig patroon in LIT om lifecycle logica te hergebruiken:
// resize-controller.ts
import { ReactiveController, ReactiveControllerHost } from 'lit';
export class ResizeController implements ReactiveController {
private host: ReactiveControllerHost & HTMLElement;
private resizeObserver?: ResizeObserver;
private size: { width: number; height: number } = { width: 0, height: 0 };
private callback: (size: { width: number; height: number }) => void;
constructor(host: ReactiveControllerHost & HTMLElement, callback: (size: { width: number; height: number }) => void) {
this.host = host;
this.callback = callback;
host.addController(this);
}
hostConnected() {
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === this.host) {
const { width, height } = entry.contentRect;
this.size = { width, height };
this.callback(this.size);
}
}
});
this.resizeObserver.observe(this.host);
}
hostDisconnected() {
this.resizeObserver?.disconnect();
}
}
// Gebruik in component
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { ResizeController } from './resize-controller';
@customElement('responsive-card')
class ResponsiveCard extends LitElement {
@state()
private size: { width: number; height: number } = { width: 0, height: 0 };
private resizeController = new ResizeController(this, (size) => {
this.size = size;
});
render() {
const isSmall = this.size.width < 300;
return html`
<div class="card ${isSmall ? 'small' : 'large'}">
<h3>Card Component</h3>
<p>Huidige breedte: ${this.size.width}px</p>
<p>Huidige hoogte: ${this.size.height}px</p>
<slot></slot>
</div>
`;
}
}Veelvoorkomende valkuilen ​
1. DOM-manipulatie in de verkeerde lifecycle fase ​
// Verkeerd
connectedCallback() {
super.connectedCallback();
// Fout: DOM is nog niet beschikbaar
this.shadowRoot.querySelector('button').focus();
}
// Juist
firstUpdated() {
// Correct: DOM is nu beschikbaar
this.shadowRoot?.querySelector('button')?.focus();
}2. Vergeten super() aan te roepen ​
// Verkeerd
connectedCallback() {
// Fout: super.connectedCallback() ontbreekt
this.loadData();
}
// Juist
connectedCallback() {
super.connectedCallback();
this.loadData();
}3. Oneindige update loops ​
// Verkeerd
updated() {
// Fout: triggert een nieuwe update zonder voorwaarde
this.count++;
this.requestUpdate();
}
// Juist
updated(changedProperties) {
if (changedProperties.has('externalData') && !changedProperties.has('processedData')) {
// Veilig: update alleen onder specifieke voorwaarden
this.processedData = this.processData(this.externalData);
}
}4. Niet opruimen van resources ​
// Verkeerd
connectedCallback() {
super.connectedCallback();
document.addEventListener('scroll', this.handleScroll);
// Geen corresponderende cleanup in disconnectedCallback
}
// Juist
connectedCallback() {
super.connectedCallback();
this.boundHandleScroll = this.handleScroll.bind(this);
document.addEventListener('scroll', this.boundHandleScroll);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('scroll', this.boundHandleScroll);
}5. Race conditions met async operaties ​
// Verkeerd
async connectedCallback() {
super.connectedCallback();
const data = await this.fetchData();
// Fout: component kan al disconnected zijn
this.data = data;
}
// Juist
async connectedCallback() {
super.connectedCallback();
this.loading = true;
try {
const data = await this.fetchData();
// Check of component nog connected is
if (this.isConnected) {
this.data = data;
}
} finally {
if (this.isConnected) {
this.loading = false;
}
}
}Conclusie ​
De component lifecycle in LIT elements biedt een krachtig en voorspelbaar mechanisme om componenten te initialiseren, bij te werken en op te ruimen. Door de verschillende lifecycle hooks correct te gebruiken, kun je robuuste en performante webcomponenten bouwen.
Hier zijn de belangrijkste punten om te onthouden:
Initialisatiefase:
constructor(): Initiële setup, geen DOM-manipulatieconnectedCallback(): Resources initialiseren, data ladenfirstUpdated(): DOM-gerelateerde initialisatie
Updatefase:
willUpdate(): Voorbereidingen voor renderingrender(): Pure functie die de UI beschrijftupdated(): Post-rendering acties
Verwijderingsfase:
disconnectedCallback(): Resources opruimen
Door deze lifecycle hooks effectief te gebruiken, kun je componenten bouwen die correct initialiseren, efficiënt updaten, en netjes opruimen wanneer ze niet meer nodig zijn. Dit resulteert in betere prestaties, minder bugs, en een betere gebruikerservaring in je dcr-components.