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.
<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 slds-hide class instead.
for:each
<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.
<!-- 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.
<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.
