Files
sigpro/docs/api/each.md
2026-04-26 15:38:10 +02:00

5.2 KiB
Raw Permalink Blame History

Reactive Lists: each( )

The each function is a highperformance 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

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 reexecuted every time the items 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 (nonreactive) 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:

  1. Compares keys between the old and new items using the specified keyField (or item.id / index).
  2. Reuses existing DOM nodes for keys that stay the same.
  3. Moves nodes if order changed (no recreation).
  4. Creates new nodes for new keys.
  5. 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 array index for 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
Rerenders on change Recreates 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 rerender) Builtin, finegrained

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');