<For>
List rendering over a reactive array. When the compiler runs it lowers to mapArray; without it, the list is rebuilt on every change.
<For each={list}>
{(item, index) => <Item />}
</For>
Props
| Prop | Type | Description |
|---|---|---|
each |
() => T[] |
The array to iterate over. Pass the signal itself, or a thunk. A plain array works only on the uncompiled runtime path; compiled output calls each() and throws source is not a function on an array (so each={items()} throws too). |
key |
(item: T) => string | number |
Optional, compiled path only. Identifies rows so they can be moved instead of rebuilt. With a key, the render function receives a signal accessor rather than the raw item. The runtime For ignores this prop. |
fallback |
Element | null |
Optional. What to render in place of the rows when the array is empty or null. Honoured on both paths, and it composes with key. |
children |
(item, index: number) => VNodeChild |
A render function returning the content for one row. index is captured when the row is created and is not refreshed when rows move. item is the raw value, unless a key is set on a compiled <For>, in which case it is an accessor you call. |
Returns
A reactive region. For returns a thunk that the renderer installs as a fine-grained effect; you never handle the return value yourself. On the compiled path the <For> tag is erased entirely and replaced with a mapArray inserter.
Usage
Basic List
import { For, signal } from 'what-framework';
const items = signal([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' },
]);
<ul>
<For each={items}>
{item => (
<li>{item.name}</li>
)}
</For>
</ul>
Empty State
A fallback prop on <For> replaces the rows while the array is empty or null, on both paths:
import { For } from 'what-framework';
<ul>
<For each={items} fallback={<li>No items yet.</li>}>
{item => <li>{item.name}</li>}
</For>
</ul>
With Reactive Updates
const todos = signal([]);
// Add a todo
todos.set(t => [...t, { id: Date.now(), text: 'New item' }]);
// Remove a todo
todos.set(t => t.filter(item => item.id !== targetId));
// The list updates automatically
<For each={todos}>
{todo => (
<div>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</div>
)}
</For>
Primitive Lists
const tags = signal(['react', 'vue', 'what']);
<For each={tags}>
{tag => <span className="tag">{tag}</span>}
</For>
Keyed Lists
// Compiled path only. The key goes on <For> itself, and the
// render function then receives an accessor, not the raw item.
<For each={todos} key={todo => todo.id}>
{todo => <li>{() => todo().text}</li>}
</For>
Keys
A key prop on the element the render function returns does nothing. The runtime render path never reads vnode.key, and the compiler strips key from an element it lowers into a template. Only two spellings actually key a list, and both require the compiler:
{() => items().map(item => <li key={item.id}>…</li>)}, which the compiler auto-lowers to a keyedmapArrayin raw mode (the render function keeps the raw item). This is the recommended form, and a dev build prints a compiler advisory pointing at it for any file that uses a<For>.<For each={items} key={item => item.id}>, which keys in accessor mode: the item arrives as a signal getter, so an item that is replaced but keeps its key updates in place instead of recreating its DOM.
Without the compiler there is no keyed reconciliation at all. Every row is disposed and rebuilt on every list change, so focus is lost, an open <details> closes, and CSS transitions restart. This is tracked for 0.13.4.
How It Works
There are two paths, and they are not identical.
Uncompiled (plain h() or the automatic JSX runtime): For runs as an ordinary component and returns a reactive thunk. On each run it reads each, returns fallback if the list is empty or null, and otherwise maps every item through the render function with the raw item and its index. There is no reconciliation: the whole region is rebuilt.
Compiled: the babel plugin erases the <For> tag and emits mapArray(each, renderFn, { key }), an inserter that diffs the list in place. With a key it identifies rows by key; without one it identifies them by item reference, so a replaced object is a new row. Either way it skips the common prefix and suffix and then positions the remainder with a Longest Increasing Subsequence pass, moving only the rows that actually have to move.
A fallback adds a memoised emptiness test alongside that same inserter rather than wrapping it, so the list is still built once and rows are still reconciled across an empty-and-refill cycle.
Notes
- The
childrenof<For>must be a single render function, not JSX elements. The runtimeForwarns and renders the fallback; the compiler warns at build time and falls back toh(), which puts a literal<for>element in the page. - Do not derive anything user-visible from
index. It is a snapshot taken when the row was created, so a row number computed from it goes stale as soon as anything is inserted above it. - A reactive
.map()with akeyprop is the default What idiom for lists. Reach for<For>when you want the signal-wrapped item accessors that keyed mode provides. <For>does render on the server as of 0.13.4, and uncompiled its rows are reused on hydration: the row nodes on screen are the ones the server sent, and afallbackstanding in for an empty list is reused too. Compiled, the tag is gone by then (it lowers to a list inserter), and that inserter builds its own rows rather than claiming the server's, so every row is discarded and rebuilt. This is not a<For>-only caveat: the recommended.map()with akeyprop lowers to the same inserter and pays the same cost, keyed or not. What you lose is only the reuse. The rows come out with the right count, the right order and the right text, nothing warns (there is no hydration mismatch to report), and the markup around the list is still claimed normally, so the cost is confined to the rows themselves. Reusing them needs list boundary markers in the server HTML, tracked for 0.13.4.