5.2 KiB
Reactive Lists: each( )
The each function is a high‑performance keyed list renderer. It maps a reactive array to DOM nodes and surgically updates only the items that changed (added, removed, or reordered). Unlike a simple .map(), each reuses DOM nodes and preserves internal state.
Function Signature
each(
source: Signal<any[]> | (() => any[]) | any[],
itemFn: (item: any, index: number) => Node | (() => Node),
keyField?: string
): HTMLElement
| Parameter | Type | Required | Description |
|---|---|---|---|
source |
Signal, () => any[], or any[] |
Yes | The reactive array to iterate over. |
itemFn |
(item, index) => Node |
Yes | Returns a DOM node (or a function that returns a node) for each item. |
keyField |
string |
No | Name of the property to use as unique key (e.g., "id"). Default: item?.id ?? index. |
Returns: A div with style="display: contents" that contains the live list.
Usage Patterns
1. Basic Keyed List (Recommended)
Pass the name of the property that contains the unique identifier (e.g., "id"). This allows SigPro to reuse DOM nodes when the list is reordered or filtered.
const users = $([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]);
ul({ class: "list" }, [
each(users,
(user) => li({ class: "p-2" }, user.name),
"id" // ← use property "id" as stable key
)
]);
2. Automatic Key (Simple Lists)
If you omit the keyField, each defaults to item?.id ?? index. For primitive arrays or objects without an id, the index is used.
const tags = $(["Tech", "JS", "Web"]);
div({ class: "flex gap-1" }, [
each(tags, (tag) => span({ class: "badge" }, tag))
// key defaults to index (0,1,2) – fine for static order
]);
3. Using a Different Property Name
If your unique identifier is not called id (e.g., _id, userId, slug), just pass the property name as the third parameter:
const products = $([
{ _id: 101, name: "Laptop" },
{ _id: 102, name: "Mouse" }
]);
each(products, (item) => li(item.name), "_id");
4. Dynamic Content Using Functions
If your itemFn returns a function, that function is re‑executed every time the item’s data changes (but the node is reused).
const todos = $([
{ id: 1, text: "Learn SigPro", done: false }
]);
each(todos,
(todo) => div([
input({ type: "checkbox", checked: () => todo.done, onInput: e => todo.done = e.target.checked }),
span(() => todo.done ? s(todo.text) : todo.text)
]),
"id"
);
5. Source as a Plain Array or Function
source can be a plain array (non‑reactive) or a function that returns an array – it will still react to changes if signals are read inside the function.
const filter = $("all");
const filteredTodos = () => {
const all = todos();
if (filter() === "active") return all.filter(t => !t.done);
return all;
};
each(filteredTodos, (todo) => li(todo.text), "id");
How It Works (Reconciliation)
When the source changes, each:
- Compares keys between the old and new items using the specified
keyField(oritem.id/ index). - Reuses existing DOM nodes for keys that stay the same.
- Moves nodes if order changed (no recreation).
- Creates new nodes for new keys.
- Destroys nodes for removed keys – cleans up all effects, event listeners, and child components.
This is much more efficient than destroying and rebuilding the whole list on every update.
Performance Tips
- Stable keys – Use a property that never changes (like a database primary key). Avoid
Math.random()or arrayindexfor lists that can be reordered. - State preservation – If a list item contains an input or local state, using a stable key ensures that state is preserved even when the list is filtered or sorted.
- Lazy item functions – If an item is expensive to render, wrap it in a function:
() => ExpensiveComponent(item). The component is only created when the item actually appears in the DOM.
Summary Comparison
| Feature | Standard Array.map |
SigPro each |
|---|---|---|
| Re‑renders on change | Re‑creates entire list | Only adds/removes/moves changed items |
| DOM nodes | New nodes every time | Reused via keys |
| Memory cleanup | Manual (or leak) | Automatic (destroy on removal) |
| Internal state per item | Lost on every update | Preserved (if key stable) |
| Reactivity | None (manual re‑render) | Built‑in, fine‑grained |
Complete Example
const items = $([
{ id: 1, name: "Apple", price: 1.2 },
{ id: 2, name: "Banana", price: 0.8 }
]);
const addItem = () => {
const newId = Date.now();
items([...items(), { id: newId, name: `Item ${newId}`, price: 1.0 }]);
};
const removeItem = (id) => {
items(items().filter(i => i.id !== id));
};
const App = () =>
div([
button({ onClick: addItem }, "Add item"),
ul(
each(items,
(item) => li([
span(`${item.name} – $${item.price}`),
button({ onClick: () => removeItem(item.id) }, "X")
]),
"id"
)
)
]);
mount(App, '#app');