docs / Runtime
Lists and For
Rendering lists server-side: the native for loop (keyed) and the For component, and when to use which.
There are two list forms and they are not interchangeable today. The NATIVE for loop in statement mode works everywhere, including SSR, and it is what the framework is built around: the compiler turns it into a keyed region that reconciles by identity instead of diffing the list.
component Tasks() {
const &[tasks] = track([
{ id: 1, title: 'Write the docs' },
{ id: 2, title: 'Ship the release' },
])
<ul>
for (const task of tasks) {
<li>{task.title}</li>
}
</ul>
}
A for body is a REGION: it re-renders on its own when the list changes, and rows are adopted by identity (see Keyed Reconciliation). Give it a key when order can change and identity matters.
component Tasks() {
const &[tasks] = track([
{ id: 1, title: 'Write the docs' },
{ id: 2, title: 'Ship the release' },
])
<ul>
for (const task of tasks) {
<li key={task.id}>{task.title}</li>
}
</ul>
<button onClick={() => tasks = [...tasks].reverse()}>reverse</button>
}
The For component
For is the expression-position form, for when the list is handed to a helper rather than written out as a loop.
component Tasks() {
const &[tasks] = track(['Write the docs', 'Ship the release'])
const renderRow = (item: string) => <li>{item}</li>
<ul>
<For each={tasks}>{renderRow}</For>
</ul>
}
For renders its rows on the server AND on the client, with output identical to the native loop. It used to be server-empty: a function-valued child is not an IR node, so the JSX in its body was emitted verbatim and the render threw, and even once it compiled the child was wrapped in the text emitter so For called something that returned nothing.
Reach for the native for loop anyway when you want KEYED reconciliation: For renders rows but does not key them, so a reorder rebuilds them instead of moving them. When order can change and identity matters, the keyed loop is the right tool.