On this pageKeying
Keying
Foldkit uses Snabbdom for virtual DOM diffing. When a view renders different content at the same DOM position, Snabbdom will try to patch one version into the other. This can cause stale input state, mismatched event handlers, and carried-over focus.
Always key branch points
If the same DOM position renders different content depending on your model, key it. Without a key, Snabbdom patches where it should replace.
The keyed function tells Snabbdom that when the key changes, the old tree should be fully removed and the new tree inserted fresh: no diffing, no patching, no carryover.
There are three places in a view where keying matters:
- Branching views: a position rendering different content based on a value
- Mapped list items: children rendered by mapping over an array
- Conditional inserts: all children in a list where any appear conditionally
Use a discriminating string as the key, typically a tag:
import { Match as M } from 'effect'
import { Document, html } from 'foldkit/html'
const { div, header, main, keyed } = html<Message>()
const view = (model: Model): Document => {
const routeContent = M.value(model.route).pipe(
M.tagsExhaustive({
Products: () => productsView(model),
Cart: () => cartView(model),
Checkout: () => checkoutView(model),
NotFound: ({ path }) => notFoundView(path),
}),
)
return {
title: `${model.route._tag} — Shop`,
body: div(
[],
[
header([], [navigationView(model.route)]),
main([], [keyed('div')(model.route._tag, [], [routeContent])]),
],
),
}
}The same rule applies to any control-flow branch that produces different content: Match, if/else, and ternaries.
Key list items by a stable model identifier (an id, a UUID), never by array position:
import { html } from 'foldkit/html'
const { input, ul, keyed, Value, OnInput } = html<Message>()
const entryListView = (entries: ReadonlyArray<Entry>): Html =>
ul(
[],
entries.map(entry =>
keyed('li')(
entry.id,
[],
[
input([
Value(entry.text),
OnInput(text => EditedEntry({ id: entry.id, text })),
]),
],
),
),
)Positional diffing looks correct until an entry is removed from the middle of the list or the list is reordered. Snabbdom then patches the old row’s DOM into what should be a different row.
When a child appears or disappears between stable siblings, key each of them. Given children like [a, ...(cond ? [b] : []), c], give all three a key:
import { html } from 'foldkit/html'
const { div, keyed } = html<Message>()
const cartView = (model: Model): Html =>
div(
[],
[
keyed('div')('summary', [], [summaryView(model)]),
...(model.hasDiscount
? [keyed('div')('discount', [], [discountView(model)])]
: []),
keyed('div')('checkout', [], [checkoutView(model)]),
],
)Snabbdom’s diff can often handle conditional inserts correctly by matching elements on their tag and classes, but that is implicit behavior. Explicit keys make the intent clear and stay correct across refactors.