For
<for> renders one copy of its body per array item, keys every row for
you, and has a built-in empty state.
<For> is the same component.
<script>
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
]
</script>
<for each={users} as="user">
<p>{user.name}</p>
</for>
Alice
Bob
Charlie
each must be a {expression}.
Anything that is not an array - including the string each="users" - is
treated as empty.
Name the index with index:
<script>
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
]
</script>
<for each={users} as="user" index="i">
<p>{i + 1}. {user.name}</p>
</for>
Keying
When the array changes, <for> diffs by key and does the minimum work:
surviving rows keep their exact DOM nodes and are moved into place, only removed rows run
their cleanup, only new rows mount.
So focus, typed text, scroll position and media playback all survive a reorder.
Type into a row, then reverse the order. The text follows its row.
<script>
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
]
function reverse() {
users = [...users].reverse()
}
</script>
<button onclick="reverse()">Reverse the order</button>
<for each={users} as="user">
<p>{user.name} <input placeholder="type something here" /></p>
</for>
<style>
button {
margin-bottom: 0.5rem;
padding: 0.35rem 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
p {
padding: 0.15rem 0;
}
</style>Alice
Bob
Charlie
With no key, rows are keyed by identity - objects by the object itself,
primitives by their value. That is usually right, and it survives immutable updates:
[...items], items.filter(...) and reverse() all keep
the surviving objects, so they keep their rows.
Pass key when a refresh replaces your items with brand new objects carrying
the same ids.
key takes a property name as a plain string.
key="id" is right; key={user.id} is wrong and silently falls
back to index keys, which throws away every guarantee above. So does naming a property
the items do not have.
Duplicate keys log one warning and those rows lose their DOM identity.
Empty State
fallback renders when the array is empty, null, or not an array at all:
<script>
let users = []
let emptyMessage = 'No users yet.'
function addUser() {
users = [...users, { id: users.length + 1, name: 'User ' + (users.length + 1) }]
}
function clearUsers() {
users = []
}
</script>
<button onclick="addUser()">Add a user</button>
<button onclick="clearUsers()">Clear</button>
<for each={users} as="user" key="id" fallback="<p><em>{emptyMessage}</em></p>">
<p>{user.name}</p>
</for>
<style>
button {
margin: 0 0.4rem 0.5rem 0;
padding: 0.35rem 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
</style>No users yet.
fallback is markup, and it is mounted, so components inside it work.
Its expressions are compiled against your component's state, so
fallback="<p>{emptyMessage}</p>" renders the message and keeps
rendering it as the message changes. The binding is not detached by emptying and refilling
the list.
The <for> fallback is the one that interpolates. A
<show>, <switch>,
<suspense> or <error-boundary> fallback is literal
markup, and an expression in one of those is a build error.
What Works in a Row
A row is your component's own markup, finished by your component when the row mounts.
Text, interpolated attributes, directives and handlers all bind per row with the loop variable in scope:
<script>
let tasks = [
{ id: 1, title: 'Write the docs', done: true },
{ id: 2, title: 'Reread the docs', done: false }
]
function toggle(id) {
tasks = tasks.map(function (t) {
return t.id === id ? { ...t, done: !t.done } : t
})
}
</script>
<for each={tasks} as="task">
<p class="task" class:done={task.done} title="Task {task.id}">
{task.title}
<button onclick="toggle({task.id})">toggle</button>
</p>
</for>
<style>
.task {
padding: 0.25rem 0;
}
.done {
text-decoration: line-through;
opacity: 0.55;
}
button {
margin-left: 0.5rem;
padding: 0.15rem 0.5rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
</style>Write the docs
Reread the docs
That includes every event modifier - on:click|once,
on:keydown|escape, |capture, |passive and the rest,
with |once counted per row rather than per list - a brace-bodied handler such
as on:click={() => { picked = task.id }}, the brace-arrow attribute
onclick={() => pick(task.id)}, which closes over the row exactly as it
reads, and bind:value={task.title}, which writes back into that row's own
object.
An interpolated attribute in a row is a real binding: when
title="Task {task.id}" changes the row is patched, so focus and anything typed
into it survive.
A handler on a component tag in a row works the same way:
<Card onclick="pick(task.id)" /> is your component's handler, reading that
row's variables, attached to the card's own root element.
Two things do not work in a row, and it is worth knowing them up front:
| In a row | Why |
|---|---|
ref:box |
Declined on purpose: one name, many elements. It is a build error, not a silent miss - use use:, which runs once per row on that row's element. |
<Card task={task} /> |
An object prop stringifies to [object Object]. String props such as
title="{task.title}" are fine.
|
animate:, in: and out: are build errors here too, but
that is not about rows - they are unimplemented everywhere.
transition: works on a row element: a genuinely
new key animates in, a vanished one animates out.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
each |
{expression} |
- | The array. Non-arrays are treated as empty. |
as |
string |
"item" |
Name bound to each item inside the body. |
index |
string |
"index" |
Name bound to the zero-based index. |
key |
string |
identity | Property name to key by. Not an expression. |
fallback |
string |
- | Markup for the empty case. Mounted, and its expressions are compiled. |
For or .map()
{items.map(...)} does the same job in plain JavaScript.
A .map() row is markup an expression produces at render time, so there
is nothing for the compiler to compile until it runs. A <for> body is
markup, compiled once into a template unit - which is why keying, handlers, directives and
child components all work there, and why the shapes below that a row cannot express are
named build errors rather than silent losses:
.map() |
<for> |
|
|---|---|---|
| Keying | key={item.id} keys, and hands the row to JetShake |
Automatic; key="prop" to override |
| Empty state | Ternary or && |
fallback prop |
| Index | Second callback argument (a keyed row may not take one) | index prop |
| Interpolated attributes in the body | Yes | Yes |
Directives - class:, style:, bind:, use: |
No - use interpolated attributes | Yes |
A string handler in the body, onclick="fn({item.id})" |
Only in a keyed row, where it becomes the compiled row's own listener | Yes |
Arrow handler in an attribute, onclick={() => ...} |
Build error, keyed or not | Yes |
| A component tag in the body | Build error | Yes |
<input> and the other form controls in the body |
Only in an unkeyed row - JetShake declines them | Yes |
| Object props | No - props are strings everywhere in JAF | No |
| Wrapper element | None | None |
@static for very large lists |
Yes | No |
A keyed .map() is its own compilation
key={...} on a .map() row is real keying, and it is compiled
differently from every other list in JAF: the row is handed to JetShake,
which turns it into a module of its own with a keyed reconciler in it, and the page mounts
that module at its anchor. That is the fastest list path JAF has.
<!-- keyed and hosted: rows move rather than being patched in place -->
{users.map(user => (
<li key={user.id} class="{user.active ? 'on' : ''}">
<a onclick="select({user.id})">{user.name}</a>
</li>
))}
A hostable row is plain HTML elements only (no component tag, and none of
<input>, <select>, <option>,
<textarea>, <svg>, <math>,
<template>, <script>, <style>,
<slot>), text and interpolated attributes, string handlers but not
brace-arrow ones, no directives, a plain list expression
(users, data.rows - not users.filter(...)), and no
index parameter. When the row is not hostable the compiler says so, and the list falls back
to an ordinary .map() binding with that binding's rules - which for most of
those shapes is a build error.
Two conditions are about the project rather than the row:
jetshake is not a dependency of jafjs, so install it
(npm install --save-dev jetshake) or the list is not hosted, and
JAF_JSX_LIST_CODEGEN=0 turns the path off for a build.
<for> is still the one to reach for: it takes every shape and its keying
is automatic. Reach for a keyed .map() when a large list's update cost is what
you are optimising, and measure it.
.map() is not auto-keyed. Without an explicit
key={...} it patches by position, so a reorder or removal rebuilds rows
and loses focus and typed text. Add key={item.id} whenever the list can
change shape, or use <for>.
Neither form wraps its rows. A <for> inside a
<tbody> puts its rows in that <tbody>, and a list
in a flex or grid parent has its items as that parent's own children.
Alpha code often carries
[data-jaf-component="For"] { display: contents } for the wrapper
<div> that used to be there. Delete it - it matches nothing now.
Large read-mostly lists: @static
Every row of a list is normally handed to the template as a reactive object, so writing
rows[5].label = 'x' updates that one cell. That costs a proxy per row and a
dependency record per property read on it - invisible on fifty rows, and the largest thing
on the page at ten thousand.
{@static ...} in front of a .map() hands the rows over raw
instead:
<script>
let rows = [
{ id: 1, label: 'one' },
{ id: 2, label: 'two' }
]
// Changing a row: build a new row object. This updates.
function rename(id, label) {
rows = rows.map(row => row.id === id ? { ...row, label } : row)
}
// This does NOT update under @static - the row object never changed.
// rows[0].label = 'one!'
</script>
<table>
<tbody>
{@static rows.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.label}</td>
</tr>
))}
</tbody>
</table>
On a 10,000-row table that is about 6 MB, roughly a fifth of the page's live heap.
What still updates:
- Assigning the list -
rows = [...],rows = rows.filter(...) - Replacing a row with a new object at the same key - the row's cells update in place
- Appending, removing and reordering, done by assigning the new array
- Every other variable the row template reads - a selected id, a handler, a format string
Mutating a row in place does not update.
rows[5].label = 'x' changes the data and nothing on screen; so does
rows.push(row). There is no proxy on the rows, so there is nothing to
notice the write - that is the whole of what you are trading, and it is why the prefix
is named after the cost rather than the benefit.
Build a new row object, or a new array, and assign it.
Nothing warns you: a warning would need the very proxy the prefix removes. Reach for
@static on big, read-mostly lists you replace wholesale, and leave it off
everywhere else.
@static goes in front of the expression, inside the braces, and works
wherever a .map() list does - keyed or not, and whichever renderer the build
chooses for it. It is a prefix like @unsafe, and the two compose in either
order.
There is no equivalent for <for>.
Filtering and Sorting
Do both in the script, not in each. An arrow function inside the attribute
contains a >, which ends the attribute early and mangles the tag.
A derived let recomputes on every change and is what you want anyway - and
copy before sorting, because sort() mutates in place.
<script>
let users = [
{ id: 1, name: 'Charlie', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Alice', active: true }
]
// Filter and sort here, not inside each={...}.
let activeUsers = users.filter(u => u.active)
let sortedUsers = [...activeUsers].sort((a, b) => a.name.localeCompare(b.name))
</script>
<for each={sortedUsers} as="user" key="id">
<p>{user.name}</p>
</for>
Alice
Charlie