---
title: "lwc:if, for:each, and the key requirement"
url: "https://trailassembly.com/lwc-template-directives/"
topic: "LWC and Front-End"
published: "2026-09-18T12:34:31+00:00"
updated: "2026-10-06T13:58:13+00:00"
---

# lwc:if, for:each, and the key requirement

**The short version:** use `lwc:if`, `lwc:elseif` and `lwc:else` for conditionals, `for:each` for ordinary lists, and `iterator` only when you need to know which item is first or last. Every list item needs a `key`, that key must be a stable value that belongs to the record, and using the loop index is the single most common way people break rendering in a way that only shows up after a delete.

## Conditionals

The modern directives arrived with API version 58 and replaced `if:true` and `if:false`.

```apex
<template lwc:if={isLoading}>
    <lightning-spinner alternative-text="Loading"></lightning-spinner>
</template>
<template lwc:elseif={hasError}>
    <p class="slds-text-color_error">{errorMessage}</p>
</template>
<template lwc:else>
    <c-result-list results={results}></c-result-list>
</template>
```

The three have to be siblings with nothing between them, not even a comment or a stray text node. `lwc:else` takes no value. You can put the directives on a real element rather than a `<template>` tag when you want the wrapper in the output anyway.

Why the new form is better: the old `if:true` and `if:false` pair had no concept of a chain, so an if/else meant evaluating two separate expressions that could both be true or both be false. With `lwc:elseif` the branches are mutually exclusive by construction.

Expressions are still not allowed. `lwc:if={count > 0}` does not compile. Write a getter called `hasRows` and bind that.

One behavioural detail worth knowing: a false branch is not hidden, it is not rendered at all. The child components inside it are destroyed, and their `disconnectedCallback` runs. Toggling a heavy subtree back and forth means rebuilding it each time. If a section is expensive and toggles often, consider rendering it once and controlling visibility with the [SLDS](https://trailassembly.com/lwc-css-and-styling/) `slds-hide` class instead.

## for:each

```apex
<template for:each={contacts} for:item="contact" for:index="i">
    <li key={contact.Id} class="slds-item">
        {contact.Name} <span>{contact.Email}</span>
    </li>
</template>
```

`for:item` names the current element and `for:index` names its position, both as string literals. The `key` goes on the first element inside the loop, not on the `<template>` tag.

Nested loops need different item names, because the inner scope shadows the outer one if you reuse the name. That produces a component that renders the wrong data with no error at all.

## The key requirement

The compiler rejects a loop with no key, so nobody forgets it entirely. What people do instead is satisfy the compiler with whatever is closest to hand, which is usually the index.

```apex
<!-- wrong -->
<li key={i}>{row.Name}</li>

<!-- right -->
<li key={row.Id}>{row.Name}</li>
```

The key identifies a row across renders so the engine can tell an update apart from an insertion. An index does not identify anything, it describes a position. Delete the second row of five and every row below it shifts up by one, so every key from that point on now points at different data. The engine believes four rows changed their contents rather than one row leaving.

With plain text output you might never see the difference. With input elements you will, because the DOM node is reused while the data behind it moved, and the user’s half-typed value ends up attached to the wrong record. The same applies to checkbox state, focus, and any component holding internal state.

Two more rules. The key must be a string or a number, never an object, and it must be unique within that loop. If your data genuinely has no identifier, build one when the data arrives rather than in the template, because keys cannot be expressions either.

## iterator

When you need first-and-last information, switch directive.

```apex
<template iterator:it={steps}>
    <li key={it.value.id} class={it.first ? 'is-first' : ''}>
        {it.value.label}
        <template lwc:if={it.last}><span>done</span></template>
    </li>
</template>
```

Each iteration gives you an object with `value`, `index`, `first` and `last`. Note that the item now lives under `it.value`, which is the detail everyone forgets when converting a `for:each`. The ternary in the class binding above is allowed because class is computed from a property, but anything more involved should still be a getter or a precomputed property on the row.

Use `iterator` only when you need those flags. It is more verbose and slightly more work per item.

## Frequently asked

**Can I use an index as a key if the list never changes?** Technically yes. Lists that never change have a habit of changing later, and the bug that results is hard to trace, so it is not worth the saving.

**Why is my key not accepted?** Keys must be primitives. Passing the record object itself is a common mistake and gives a template error at compile time.

**Do if:true and if:false still work?** They still compile, and there is no urgency to convert working code. Write new markup with the `lwc:` directives.

**Can I loop over an object?** Not directly. Convert it to an array of key-value pairs in JavaScript first, with `Object.entries`.

---

Source: https://trailassembly.com/lwc-template-directives/
Trail Assembly — independent Salesforce write-ups.
