173 lines
5.2 KiB
Markdown
173 lines
5.2 KiB
Markdown
# 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
|
||
|
||
```typescript
|
||
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.
|
||
|
||
```javascript
|
||
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.
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```javascript
|
||
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).
|
||
|
||
```javascript
|
||
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.
|
||
|
||
```javascript
|
||
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` |
|
||
| :--- | :--- | :--- |
|
||
| **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
|
||
|
||
```javascript
|
||
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');
|
||
``` |