More than once, I’ve been surprised by a web component either showing data that should no longer be there, or not showing data that should be there. This page aims to reason through such cases for a better mental model of web components.
Rendering Lists
Both <search-bar>
and <search-results>
need to render a collection
of N items. offers two options: looping, or using
the repeat(items, keyFunction, itemTemplate)
directive.
const cards = html`
${cards.map(
(card) => html`<div><input type="checkbox">${card.title}</div>`
)}
`;
const template2 = html`
${repeat(
cards, (card) => card.id
(card) => html`<div><input type="checkbox">${card.id}</div>`
)}
`;
When performing updates, repeat
moves DOM nodes, while map
reuses
DOM nodes. This is also beneficial when there is some part of the node
that isn’t controlled by a template expression because repeat
will
keep that state, e.g, in the example above, the checked
property. If
none of these apply, then map
or loops can be used over repeat
.
Re-renders on Tagged Templates
Given a component like:
@customElement('sample-app')
export class SampleApp extends LitElement {
@state() private content = 'Hello world';
@state() private counter = 0;
constructor() {
super();
setInterval(() => this.counter++, 1000);
}
render() {
return html`<p>${this.content}</p>`;
}
}
SampleApp.render
will be called every second . The fact that this.counter
is not referenced in the rendered
template does not prevent an update cycle. In this case, this.counter
need not be reactive; a vanilla instance variable will do.
In this example though:
function getTimeString() {
let now = new Date(Date.now());
return `${now.getMinutes()}:${now.getSeconds()}`;
}
@customElement('input-wrapper')
class InputWrapperElement extends LitElement {
render() {
return html`<input value=${getTimeString()} >`;
}
}
@customElement('sample-app')
export class SampleApp extends LitElement {
@state() content = 'Hello world';
constructor() {
super();
setInterval(this.updateContent.bind(this), 1000);
}
render() {
return html`
<div>Expected value: ~${this.content}</div>
<input-wrapper></input-wrapper>
`;
}
private updateContent() {
this.content = getTimeString();
}
}
<input>
does not update every second .
This might be because <sample-app>
decides that <input-wrapper>
should not be updated.
What if we want to reset <input>
from SampleApp.updateContent()
?
Exposing InputWrapperElement.reset()
could do the trick, but that
breaks away from the “events go up; properties come down” philosophy for
encapsulated web components. Syncing InputWrapperElement.value
with
the <input>
’s value does not work either.
While React does not have a dedicated
repeat
equivalent, React issues aWarning: Each child in a list should have a unique "key" prop
when rendering a list without specifying keys for the individual items.The recommended behaviors in Lit and React make it seem like using keys is almost always the correct approach to use. We’d need perf numbers to justify not using keys.