function / @typed/template

many

Efficiently renders a reactive list of items by using keys to minimize DOM operations and maintain component state.

many returns a renderer descriptor rather than an Fx. The active renderer consumes its source directly and can therefore retain keyed entries without flattening each child back through a generic collection stream.

Package version
1.0.0-beta.7
Category
Keyed collection rendering
Since
1.0.0

Import

import { many } from "@typed/template";

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. 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.

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.

Source