Reactieve update cycles vermijden β
Inleiding β
Een veel voorkomende waarschuwing bij het ontwikkelen van Lit componenten is:
Element [component-name] scheduled an update (generally because a property
was set) after an update completed, causing a new update to be scheduled.
This is inefficient and should be avoided unless the next update can only
be scheduled as a side effect of the previous update.Deze waarschuwing geeft aan dat je component een inefficiΓ«nt update patroon gebruikt dat kan leiden tot onnodige re-renders en prestatieproblemen. In deze gids leer je waarom dit gebeurt en hoe je het kunt voorkomen.
Wat is een update cycle? β
Lit componenten hebben een reactive update lifecycle die automatisch triggert wanneer properties veranderen:
Property Change β willUpdate() β update() β render() β updated()Het probleem ontstaat wanneer je tijdens deze cycle een property update triggert die een nieuwe cycle start:
Property Change β willUpdate() β **set andere property** β NIEUWE CYCLE START
β
Dit triggert warning!Oorzaken van dubbele updates β
1. Property updates in lifecycle methoden β
β Probleem: Property update in willUpdate() β
@customElement('user-list')
class UserList extends LitElement {
@property({ type: Array }) users: User[] = [];
@property({ type: Number }) count = 0; // β οΈ Public property
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has('users')) {
this.count = this.users.length; // β Triggert nieuwe update!
}
}
}Waarom is dit een probleem?
usersproperty verandert β update cycle startwillUpdate()wordt aangeroepenthis.countwordt geset β nieuwe update cycle wordt gescheduled- Lit waarschuwt dat dit inefficiΓ«nt is
β Probleem: Property update in updated() β
@customElement('data-table')
class DataTable extends LitElement {
@property({ type: Boolean }) open = false;
@property({ type: Boolean }) hasData = false; // β οΈ Public property
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has('open') && this.open) {
this.hasData = this._checkForData(); // β Triggert nieuwe update!
}
}
}β Probleem: Property update in event handlers tijdens lifecycle β
@customElement('search-box')
class SearchBox extends LitElement {
@property({ type: String }) query = '';
@property({ type: Number }) resultCount = 0; // β οΈ Public property
render() {
return html` <input .value=${this.query} @input=${this.handleInput} /> `;
}
private handleInput(e: Event): void {
const input = e.target as HTMLInputElement;
this.query = input.value; // Update 1
this.resultCount = this._search(this.query).length; // β Update 2!
}
}2. Slot change handlers β
Slot change events kunnen tijdens de lifecycle optreden, vooral bij eerste render:
@customElement('card-container')
class CardContainer extends LitElement {
@property({ type: Boolean }) hasContent = false; // β οΈ Public property
render() {
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}
private handleSlotChange(event: Event): void {
const slot = event.target as HTMLSlotElement;
this.hasContent = slot.assignedElements().length > 0; // β Kan warning trigger!
}
}Oplossingen β
Oplossing 1: Gebruik @state() in plaats van @property() β
De simpelste oplossing: als een property alleen intern wordt gebruikt en niet van buitenaf wordt gezet, gebruik dan @state():
@customElement('user-list')
class UserList extends LitElement {
@property({ type: Array }) users: User[] = [];
@state() private count = 0; // β
Internal state
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has('users')) {
this.count = this.users.length; // β
Nu OK - geen warning!
}
}
}Waarom werkt dit?
@state()is bedoeld voor interne, afgeleide state- Lit verwacht dat
@state()properties kunnen veranderen tijdens lifecycle - Geen warning, component functioneert correct
Wanneer gebruiken:
| Decorator | Gebruik wanneer | Van buitenaf setbaar? |
|---|---|---|
@property() | Public API van component | β Ja |
@state() | Interne, afgeleide state | β Nee |
Oplossing 2: Gebruik getters (computed properties) β
Voor afgeleide waardes, gebruik getters in plaats van opgeslagen state:
@customElement('user-list')
class UserList extends LitElement {
@property({ type: Array }) users: User[] = [];
// β
Getter: geen stored state, berekend on-demand
private get count(): number {
return this.users.length;
}
render() {
return html` <div>Aantal gebruikers: ${this.count}</div> `;
}
}Voordelen:
- β Geen dubbele update cycles
- β Altijd gesynchroniseerd met source of truth
- β Geen extra memory voor opslag
- β Eenvoudig en idiomatisch
Nadelen:
- β Herberekend bij elke access (niet gecached)
- β Niet geschikt voor expensive computations
Best practice voor getters:
@customElement('data-grid')
class DataGrid extends LitElement {
@property({ type: Array }) items: DataItem[] = [];
@property({ type: String }) filter = '';
// β
Simple getter - snel genoeg
private get itemCount(): number {
return this.items.length;
}
// β
Computed filter - ook OK
private get filteredItems(): DataItem[] {
if (!this.filter) return this.items;
return this.items.filter((item) => item.name.toLowerCase().includes(this.filter.toLowerCase()));
}
render() {
return html`
<div>Gevonden: ${this.filteredItems.length} van ${this.itemCount}</div>
${this.filteredItems.map((item) => html`<div>${item.name}</div>`)}
`;
}
}Oplossing 3: Gecachte computed properties met @state() β
Voor expensive computations, gebruik @state() met expliciete cache invalidation:
@customElement('data-processor')
class DataProcessor extends LitElement {
@property({ type: Array }) data: number[] = [];
@state() private processedData: ProcessedData | null = null;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
// β
Update cache alleen als source veranderd
if (changedProperties.has('data')) {
this.processedData = this._expensiveComputation(this.data);
}
}
private _expensiveComputation(data: number[]): ProcessedData {
// Zware berekening...
return {
/* ... */
};
}
}Wanneer caching gebruiken:
- β Computation is expensive (>1ms)
- β Source data verandert niet vaak
- β Resultaat wordt meerdere keren gebruikt in template
Oplossing 4: Slot change optimalisatie β
Voor slot changes, gebruik getters of defensive checking:
Optie A: Getter (aanbevolen) β
@customElement('card-container')
class CardContainer extends LitElement {
// β
Geen stored state, direct check
private get hasContent(): boolean {
const slot = this.shadowRoot?.querySelector('slot');
return (slot?.assignedElements().length ?? 0) > 0;
}
render() {
return html`
<div class=${classMap({ 'has-content': this.hasContent })}>
<slot></slot>
</div>
`;
}
}Optie B: State met requestUpdate β
@customElement('card-container')
class CardContainer extends LitElement {
@state() private hasContent = false;
render() {
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}
private handleSlotChange(event: Event): void {
const slot = event.target as HTMLSlotElement;
const newHasContent = slot.assignedElements().length > 0;
// β
Defensieve check: alleen updaten als waarde echt veranderd
if (this.hasContent !== newHasContent) {
this.hasContent = newHasContent;
}
}
}Optie C: Async update β
@customElement('card-container')
class CardContainer extends LitElement {
@state() private hasContent = false;
private handleSlotChange(event: Event): void {
const slot = event.target as HTMLSlotElement;
// β
Defer tot volgende microtask
queueMicrotask(() => {
this.hasContent = slot.assignedElements().length > 0;
});
}
}Oplossing 5: Multi-property updates batchen β
Als je meerdere gerelateerde properties moet updaten:
@customElement('form-validator')
class FormValidator extends LitElement {
@property({ type: Object }) formData = {};
@state() private isValid = false;
@state() private errorCount = 0;
@state() private errors: ValidationError[] = [];
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has('formData')) {
// β
Bereken alles tegelijk
const validationResult = this._validate(this.formData);
// β
Set alle properties in één statement
this.isValid = validationResult.isValid;
this.errorCount = validationResult.errors.length;
this.errors = validationResult.errors;
// Lit batcht deze updates automatisch!
}
}
}Lifecycle guideline β
Overzicht van wat je wel en niet moet doen in elke lifecycle methode:
| Lifecycle Method | β Toegestaan | β Vermijd | Best Practice |
|---|---|---|---|
| constructor() | Property initialisatie | DOM access | Minimale setup |
| connectedCallback() | Event listeners toevoegen | Properties setten | Setup external resources |
| disconnectedCallback() | Cleanup | Properties setten | Remove listeners |
| willUpdate() | Read properties, cache berekeningen | β οΈ Set @property() | Gebruik @state() voor afgeleide state |
| render() | Return template | Side effects, property updates | Pure function |
| updated() | DOM queries, focus management | β οΈ Set properties | Gebruik defensive checking |
| firstUpdated() | One-time DOM setup | - | β Property updates OK hier |
Gedetailleerde lifecycle regels β
willUpdate() - Property preparation β
β Goed:
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
// β
Update @state() properties
if (changedProperties.has('items')) {
this.filteredItems = this._filter(this.items);
}
// β
Bereken waardes voor gebruik in render()
if (changedProperties.has('data')) {
this.processedData = this._process(this.data);
}
}β Vermijd:
protected override willUpdate(changedProperties: PropertyValues): void {
// β DOM manipulation
this.shadowRoot.querySelector('.item')?.remove();
// β Set @property() (alleen @state() is OK)
this.publicProp = 'value';
// β Async operations
this.fetchData();
}render() - Template creation β
β Goed:
render() {
// β
Pure template generation
return html`
<div class=${this.className}>
${this.items.map(item => html`<span>${item}</span>`)}
</div>
`;
}β Vermijd:
render() {
// β Side effects
console.log('Rendering...');
this.trackAnalytics();
// β Property updates
this.count = this.items.length;
// β DOM manipulation
this.querySelector('.button')?.remove();
return html`<div>...</div>`;
}updated() - Post-render operations β
β Goed:
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
// β
DOM queries
const element = this.shadowRoot.querySelector('.target');
// β
Focus management
if (changedProperties.has('open') && this.open) {
this.shadowRoot.querySelector('input')?.focus();
}
// β
Measure DOM
const height = this.offsetHeight;
}β οΈ Defensief met property updates:
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
// β οΈ Alleen als echt noodzakelijk
if (changedProperties.has('items')) {
const newCount = this._countVisible();
// Defensive check!
if (this.visibleCount !== newCount) {
this.visibleCount = newCount;
}
}
}Praktijkvoorbeelden β
Voorbeeld 1: Dialog component met slot tracking β
β Probleem:
@customElement('dcr-dialog')
class DcrDialog extends LitElement {
@property({ type: String }) type: DialogType = 'custom';
@property({ type: Boolean }) hasActions = false; // β οΈ Public property
render() {
return html` <slot name="actions" @slotchange=${this.handleSlotChange}></slot> `;
}
private handleSlotChange(event: Event): void {
const slot = event.target as HTMLSlotElement;
// β Warning: property update tijdens mogelijk lifecycle
this.hasActions = slot.assignedElements().length > 0;
}
}β Oplossing 1: @state()
@customElement('dcr-dialog')
class DcrDialog extends LitElement {
@property({ type: String }) type: DialogType = 'custom';
@state() private hasActions = false; // β
Internal state
private handleSlotChange(event: Event): void {
const slot = event.target as HTMLSlotElement;
this.hasActions = slot.assignedElements().length > 0; // β
OK
}
}β Oplossing 2: Getter
@customElement('dcr-dialog')
class DcrDialog extends LitElement {
@property({ type: String }) type: DialogType = 'custom';
// β
Berekend on-demand
private get hasActions(): boolean {
if (this.type !== 'custom') return true;
const slot = this.shadowRoot?.querySelector('slot[name="actions"]');
return (slot?.assignedElements().length ?? 0) > 0;
}
render() {
return html`
<div class=${classMap({ 'has-actions': this.hasActions })}>
<slot name="actions"></slot>
</div>
`;
}
}Voorbeeld 2: Data filtering β
β Probleem:
@customElement('product-list')
class ProductList extends LitElement {
@property({ type: Array }) products: Product[] = [];
@property({ type: String }) filter = '';
@property({ type: Array }) filteredProducts: Product[] = []; // β Public
protected override willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has('products') || changedProperties.has('filter')) {
// β Warning!
this.filteredProducts = this.products.filter((p) => p.name.includes(this.filter));
}
}
}β Oplossing: Getter
@customElement('product-list')
class ProductList extends LitElement {
@property({ type: Array }) products: Product[] = [];
@property({ type: String }) filter = '';
// β
Computed property
private get filteredProducts(): Product[] {
if (!this.filter) return this.products;
return this.products.filter((p) => p.name.toLowerCase().includes(this.filter.toLowerCase()));
}
render() {
return html` ${this.filteredProducts.map((p) => html`<div>${p.name}</div>`)} `;
}
}Voorbeeld 3: Form validation β
β Probleem:
@customElement('login-form')
class LoginForm extends LitElement {
@property() email = '';
@property() password = '';
@property({ type: Boolean }) isValid = false; // β Public
protected override willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has('email') || changedProperties.has('password')) {
// β Warning!
this.isValid = this.email.includes('@') && this.password.length >= 8;
}
}
}β Oplossing 1: @state() met willUpdate
@customElement('login-form')
class LoginForm extends LitElement {
@property() email = '';
@property() password = '';
@state() private isValid = false; // β
Internal
protected override willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has('email') || changedProperties.has('password')) {
this.isValid = this._validate(); // β
OK
}
}
private _validate(): boolean {
return this.email.includes('@') && this.password.length >= 8;
}
}β Oplossing 2: Getter
@customElement('login-form')
class LoginForm extends LitElement {
@property() email = '';
@property() password = '';
// β
Altijd up-to-date
private get isValid(): boolean {
return this.email.includes('@') && this.password.length >= 8;
}
render() {
return html` <button .disabled=${!this.isValid}>Login</button> `;
}
}Debugging tips β
1. Identificeer welke property de warning veroorzaakt β
Voeg tijdelijke logging toe:
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
// Debug: log property changes
changedProperties.forEach((oldValue, propName) => {
console.log(`Property "${propName}" changed:`, oldValue, 'β', this[propName]);
});
// Je code...
}2. Gebruik browser DevTools β
Enable verbose Lit warnings in development:
// In je main.ts of app entry
if (import.meta.env.DEV) {
// @ts-ignore
window.litDisableWarnings = false;
}3. Check stack trace β
De warning in console bevat een stack trace die toont:
- Welke property werd gezet
- Vanuit welke methode
- Tijdens welke lifecycle fase
Geavanceerde Edge Cases β
1. Mixins en requestUpdate β
Als je mixins gebruikt die interne state beheren (zoals ARIA delegation), let op met requestUpdate() calls.
β Probleem: Een mixin roept requestUpdate() aan wanneer een attribuut verandert, maar dit gebeurt soms tijdens de initialisatie van de component.
override attributeChangedCallback(name, old, new) {
super.attributeChangedCallback(name, old, new);
// ... logica ...
this.requestUpdate(); // β Kan warning triggeren tijdens init!
}β Oplossing: Laat Lit's automatische change detection zijn werk doen, of check of een update nodig is:
// Check of update al pending is
if (this.isConnected && !this.isUpdatePending && this.hasUpdated) {
this.requestUpdate();
}2. Reactive Controllers β
Controllers die hostUpdate of hostUpdated implementeren, mogen daarin geen nieuwe update triggeren via host.requestUpdate().
β Probleem:
class PositionController implements ReactiveController {
hostUpdated() {
this.calculatePosition();
this.host.requestUpdate(); // β Trigger warning: update na update!
}
}β
Oplossing: Gebruik isUpdatePending check of defer de update als het echt nodig is (bv. na metingen):
if (!(this.host as any).isUpdatePending) {
this.host.requestUpdate();
}3. Child Components creΓ«ren in firstUpdated β
Als je in firstUpdated nieuwe child components creΓ«ert die zelf updates triggeren, kan dit een warning geven.
β Probleem:
override firstUpdated() {
// CreΓ«ert component die direct update triggert
this.appendChild(document.createElement('my-child'));
}β
Oplossing: Defer met queueMicrotask
override firstUpdated() {
// Defer naar na de huidige update cycle
queueMicrotask(() => {
this.appendChild(document.createElement('my-child'));
});
}4. Reflected Properties in firstUpdated β
Als je een property met reflect: true set in firstUpdated, triggert dit attributeChangedCallback en een nieuwe update.
β
Oplossing: Ook hier helpt queueMicrotask:
override firstUpdated() {
queueMicrotask(() => {
this.reflectedProp = 'value';
});
}Samenvatting β
Beslissingsboom β
Property moet geupdatet worden tijdens lifecycle?
β
ββ Is het een PUBLIC API property (@property)?
β β
β ββ Ja β β NIET DOEN
β β Gebruik @state() of getter
β β
β ββ Nee β Ga verder
β
ββ Is het een afgeleide waarde (computed)?
β β
β ββ Ja, en berekening is goedkoop (<1ms)
β β ββ β
Gebruik getter
β β
β ββ Ja, maar berekening is duur (>1ms)
β ββ β
Gebruik @state() met caching in willUpdate()
β
ββ Is het interne state die kan veranderen?
ββ β
Gebruik @state()Key takeaways β
- @property() = Public API, niet updaten tijdens lifecycle
- @state() = Internal state, mag wel updaten tijdens lifecycle
- Getters = Best voor afgeleide waardes
- willUpdate() = OK voor @state() updates
- render() = Alleen template, geen side effects
- updated() = Defensief property updates met checks
Volg deze richtlijnen en je component blijft efficiΓ«nt en warning-vrij! π―