LearnFrenzy
Sign in Take a free test

Templates and Reactivity · Lesson 7 of 25

Rendering Lists in LWC: for:each and iterator

5 min read Updated Nov 8, 2026 Saurabh Samir

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:each and the key attribute
  • Preparing rows in JavaScript
  • iterator with 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' : '' }));
    }
}

Result

▌Contacts (3)
• Asha Verma – CTO ★ Primary
• Ravi Kumar – Developer
• Meera Iyer – Admin
  • for:each={array} with for:item="name" (and optionally for: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 }));
}

Result

▶ 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));
    }
}

Result

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

  1. 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));
        }
    }

    Result

    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.

Next lesson

Search LearnFrenzy

Esc

Quick links