Import
import { many } from "@typed/template/many";Signatures
export declare function many<A, E, R, B extends PropertyKey, R2, E2>(values: Fx.Fx<ReadonlyArray<A>, E, R>, getKey: (a: A) => B, render: (value: RefSubject.RefSubject<A>, key: B) => Fx.Fx<RenderEvent, E2, R2 | Scope>): Many<A, E | E2 | Cause.IllegalArgumentError, R | R2 | Scope | RenderTemplate>;Why
Keys turn collection identity into a local rendering contract. The DOM
renderer keeps one entry map for the dynamic range: a new key starts one
child, a removed key closes one child Scope, a retained changed value updates
that child’s RefSubject, and a pure reorder does not publish unchanged item
data. Stable keyed entries feed the same diff algorithm as ordinary DOM
children; insert, move, and remove operations act directly on each entry’s
live range. There is no second diff over a flattened list of DOM nodes.
Removal detaches the range immediately and closes its child Scope in the
background; the parent Scope owns that cleanup through completion.
The same descriptor lets the HTML renderer serialize the first array in
source order and emit compatible hydration markers.
Ownership and lifetime
Each DOM key owns a forked child Scope. Removing the key closes that Scope;
interruption closes every remaining child. Both DOM and HTML rendering reject
duplicate keys with Cause.IllegalArgumentError. Hydratable output also
rejects local symbols because their identity cannot survive serialization;
use strings, numbers, or Symbol.for() keys across the server boundary.
Cost model and moves
Every source array requires O(n) key validation and ordering work; many does
not pretend an arbitrary list change is O(1). Within that pass, retained-key
lookup is O(1) on average, unchanged values skip RefSubject.set, and only
added or removed keys allocate or close child Scopes. DOM reconciliation is
confined to this range and uses equal-edge, append/remove, and reverse-swap
fast paths before an O(n) map fallback. An already-connected node is moved
with ParentNode.moveBefore when supported, preserving browser-managed state;
insertBefore is the compatibility fallback. HTML setup is O(n) for the
initial array and performs no live DOM reconciliation. A child’s later output
reconciles only that child’s range, without scanning other keys or nodes.
Nonempty client-rendered items use their own node boundaries; only empty or
pending children require a placeholder comment.
Examples
import { Effect, Layer } from "effect"
import { Fx, RefSubject } from "@typed/fx"
import { html, many } from "@typed/template"
import { DomRenderTemplate, render } from "@typed/template/Render"
interface Todo {
readonly id: string
readonly text: string
readonly completed: boolean
}
const program = Effect.gen(function* () {
const todos = yield* RefSubject.make<Todo[]>([
{ id: "1", text: "Learn Effect", completed: false },
{ id: "2", text: "Build app", completed: false }
])
const todoList = many(
todos,
(todo) => todo.id, // Key function
(todoRef, key) => // Render function receives RefSubject
html`<li>
${RefSubject.map(todoRef, (todo) => todo.text)}
<button onclick=${RefSubject.update(todoRef, (todo) =>
({ ...todo, completed: !todo.completed })
)}>Toggle</button>
</li>`
)
const template = html`<ul>${todoList}</ul>`
return yield* render(template, document.body).pipe(
Fx.drainLayer,
Layer.provide(DomRenderTemplate),
Layer.launch
)
})Other public imports
These import paths expose the same declaration. Each page retains its own public name and signature.