For#
import { For } from 'janux';
<For each={state.rows} by={(row) => row.id}>
{(row) => <li>{row.label}</li>}
</For>;The fine-grained list primitive. {list.map(…)} is a single expression inside
the island's one render effect, so touching one row re-runs the whole view and
reconciles the whole subtree — a cost that does not depend on how much changed.
For gives every row its own reactive scope: the list level matches rows by key
and moves nodes, and a row's body re-runs only when that row's own data changes,
or when a signal only that row reads changes.
Props#
interface ForProps<T> {
each: readonly T[] | (() => readonly T[]) | null | undefined;
by?: (item: T, index: number) => string | number;
children: (item: T, index: () => number) => unknown;
}| Prop | Meaning |
|---|---|
each |
The list, or a thunk returning it. Read inside the enclosing render scope, so the list level subscribes to the container. |
by |
Row identity across renders. Defaults to the item itself. |
children |
The row body. Receives the item and an accessor for the index. |
by, not key#
JSX reserves key: the transform lifts it out of the props object entirely, so
a key prop never reaches a component. For spells row identity by.
Pass a stable field whenever the list lives in state. A write to state.rows
stores a defensive clone, so the array elements are new objects on every read
and their identity means nothing — by={(row) => row.id} is what lets a row
survive a list write and be moved rather than rebuilt.
Replace a row to update it#
A row re-renders when its item is no longer deep-equal to the one it rendered. Mutating an item in place leaves the row showing stale data:
// the row updates
state.rows = state.rows.map((row) => (row.id === id ? { ...row, done: true } : row));
// the row does NOT update
state.rows.find((row) => row.id === id).done = true;This is the contract every identity-keyed list primitive has — Solid's <For>
over a plain array and React's memo behave the same way.
Rows receive plain data, not the state proxy: a row's reactivity to its own item flows through this diff, and reactivity to anything else flows through the signals its body reads.
The index is an accessor#
<For each={state.rows} by={(row) => row.id}>
{(row, index) => (
<li>
{index()}: {row.label}
</li>
)}
</For>Rows that ignore their position — the common case — never subscribe to it, so permuting the list re-renders nothing.
Limits#
- A row body must render exactly one node.
- A row may not contain a nested island or a
foreign()root. Those need the parent's key/sequence bookkeeping, which a per-row scope cannot supply consistently across re-renders;Forthrows rather than mounting an island whose id would change on every row update.
On the server#
For is an ordinary function component that expands to the rows, so SSR markup
is byte-identical to the .map() it replaces. The client reconciler recognizes
it before calling it and drives the fine-grained path instead — including
adopting the server's rows on the first client render.
toRaw#
import { toRaw } from 'janux';
const rows = toRaw(state.rows);The plain value behind a state proxy; anything else passes through unchanged.
For uses it to walk a list once per render without registering a tracked path
and building a child proxy for every index. Reach for it when you need to hand
state data to something that must not see the proxy — remember that what you get
back is the live object, so treat it as read-only.
Related: Keys and lists · computed() · watch()