Templates and Reactivity · Lesson 7 of 25
Rendering Lists in LWC: for:each and iterator
Lists are everywhere in Salesforce UI: contacts on an account, line items on a quote, tasks for today. LWC renders arrays with two template directives: for:each for ordinary lists and iterator when you need to know which item is first or last. Both require a key on each rendered item.
In this lesson
for:eachand thekeyattribute- Preparing rows in JavaScript
iteratorwith first and last- Handling clicks on list items
for:each
<!-- contactList.html -->
<template>
<lightning-card title={title}>
<ul class="slds-p-horizontal_small">
<template for:each={rows} for:item="contact">
<li key={contact.Id}>
{contact.Name} – {contact.Title} {contact.badge}
</li>
</template>
</ul>
</lightning-card>
</template>
// contactList.js
import { LightningElement, api } from 'lwc';
export default class ContactList extends LightningElement {
@api contacts = [
{ Id: '003A', Name: 'Asha Verma', Title: 'CTO', IsPrimary: true },
{ Id: '003B', Name: 'Ravi Kumar', Title: 'Developer', IsPrimary: false },
{ Id: '003C', Name: 'Meera Iyer', Title: 'Admin', IsPrimary: false },
];
get title() {
return `Contacts (${this.contacts.length})`;
}
// shape the data for the template here, not with expressions in HTML
get rows() {
return this.contacts.map((c) => ({ ...c, badge: c.IsPrimary ? '★ Primary' : '' }));
}
}
▌Contacts (3) • Asha Verma – CTO ★ Primary • Ravi Kumar – Developer • Meera Iyer – Admin
for:each={array}withfor:item="name"(and optionallyfor:index="i") goes on a<template>.- The first element inside needs a unique, stable
key– the record Id is perfect. LWC uses it to update only the rows that changed. - Never use the index as the key for lists that can be reordered, filtered or edited – rows get mixed up, just like in React.
- Build display values (badges, labels, CSS classes) in a getter that maps the data, since templates can't contain expressions.
iterator: first and last
<!-- approvalSteps.html -->
<template>
<template iterator:it={steps}>
<div key={it.value.id}>
<template lwc:if={it.first}><p>▶ Start</p></template>
<p>Step {it.value.order}: {it.value.label}</p>
<template lwc:if={it.last}><p>■ End of approval chain</p></template>
</div>
</template>
</template>
// approvalSteps.js
import { LightningElement } from 'lwc';
export default class ApprovalSteps extends LightningElement {
steps = ['Sales manager', 'Finance', 'CFO'].map((label, i) => ({ id: i + 1, order: i + 1, label }));
}
▶ Start Step 1: Sales manager Step 2: Finance Step 3: CFO ■ End of approval chain
With iterator:it, each item is it.value, and it.index, it.first and it.last are available – handy for separators, borders or "start/end" labels.
Clicking a row
A handler is shared by all rows, so store the row's Id in a data- attribute and read it from the event:
<!-- taskList.html -->
<template>
<template for:each={tasks} for:item="task">
<p key={task.id}>
{task.label} {task.status}
<lightning-button label={task.buttonLabel} data-id={task.id} onclick={toggle}></lightning-button>
</p>
</template>
<p>{doneCount} of {tasks.length} done</p>
</template>
// taskList.js
import { LightningElement } from 'lwc';
export default class TaskList extends LightningElement {
data = [
{ id: 't1', label: 'Call Acme', done: false },
{ id: 't2', label: 'Send quote', done: true },
];
get tasks() {
return this.data.map((t) => ({
...t,
status: t.done ? '✔' : '',
buttonLabel: t.done ? `Reopen ${t.label}` : `Complete ${t.label}`,
}));
}
get doneCount() {
return this.data.filter((t) => t.done).length;
}
toggle(event) {
const id = event.target.dataset.id; // data-id="…" → dataset.id
this.data = this.data.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
}
}
Call Acme [Complete Call Acme] Send quote ✔ [Reopen Send quote] 1 of 2 done › click “Complete Call Acme” Call Acme ✔ [Reopen Call Acme] Send quote ✔ [Reopen Send quote] 2 of 2 done › click “Reopen Send quote” Call Acme ✔ [Reopen Call Acme] Send quote [Complete Send quote] 1 of 2 done
event.target vs event.currentTarget
If the clickable element contains other elements (an icon inside a link), event.target may be the inner element without your data-id. event.currentTarget is always the element the handler is attached to – the safer choice for row clicks.
Practice
Try it yourself
Render a numbered list of products with prices, and a total line, from an array of objects.
Show solution
<!-- quoteLines.html --> <template> <ol> <template for:each={lines} for:item="line"> <li key={line.sku}>{line.name} × {line.qty} = {line.amount}</li> </template> </ol> <p>Total: {total}</p> </template>// quoteLines.js import { LightningElement } from 'lwc'; const fmt = (n) => `$${n.toLocaleString('en-US')}`; const items = [ { sku: 'LIC-01', name: 'Sales Cloud licence', qty: 10, price: 165 }, { sku: 'SUP-01', name: 'Premier support', qty: 1, price: 4000 }, ]; export default class QuoteLines extends LightningElement { get lines() { return items.map((i) => ({ ...i, amount: fmt(i.qty * i.price) })); } get total() { return fmt(items.reduce((sum, i) => sum + i.qty * i.price, 0)); } }1. Sales Cloud licence × 10 = $1,650 2. Premier support × 1 = $4,000 Total: $5,650
Quick check
Where does the key attribute go?
On the first element inside the for:each or iterator template.
When do you use iterator instead of for:each?
When you need first or last (or prefer it.index).
How do you know which row's button was clicked?
Put the Id in a data- attribute and read event.currentTarget.dataset.
Summary
- for:each with for:item renders arrays; every item needs a stable, unique key.
- iterator adds first, last and index.
- Prepare display values in getters; pass row Ids through data- attributes.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.