Lists

each renders reactive lists with keyed reconciliation:

import { each, tags } from "ruri"

const { ul, li } = tags
const todos = new Signal([{ id: 1, text: "one" }])

ul({}, each(todos, (todo, index) => li({}, `${index.value}: ${todo.text}`), {
  key: (todo) => todo.id,
}))
  • rows keep their DOM nodes across reorders (minimal-move via longest

increasing subsequence)

  • replacing an item object for an existing key re-renders only that row
  • the render function receives a per-row index signal that tracks position
  • on the server rows render statically; hydration adopts them in place

Try it

Every second row toggles its label - watch the rows move without being rebuilt:

import { each, Signal, tags } from "ruri"

const { ol, li, button } = tags
const rows = [{ id: 1, text: "alpha" }, { id: 2, text: "beta" }, { id: 3, text: "gamma" }]
const items = new Signal(rows)
let flipped = false

output.append(
    ol({}, each(items, (row) => li({}, row.text), { key: (row) => row.id })),
    button({
      onclick: () => {
        flipped = !flipped
        items.value = flipped ? [...items.peek()].reverse() : [...items.peek()]
      },
    }, "reverse"),
)
running...