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.

source
<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>
output

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:

source
<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>
output

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>
output

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>
output

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.

source
<!-- 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:

source
<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.

source
<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>
output

Alice

Charlie