On this pageKeying
Keying
Foldkit uses Snabbdom for virtual DOM diffing. When a view branches into structurally different trees in the same DOM position, Snabbdom will try to patch one tree into the other. This causes stale input state, mismatched event handlers, broken focus, and bugs that are extremely hard to track down.
Always key branch points
Any time your view switches between structurally different trees — routes, layouts, or model states — wrap the branch in a keyed element. Without it, the virtual DOM patches instead of replacing, which causes subtle and hard-to-diagnose bugs.
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. In React, this happens automatically when component types differ. In Foldkit, you opt in explicitly.
The most common case. When rendering route content, key by model.route._tag so navigating between routes replaces the DOM rather than patching it:
import { html } from 'foldkit/html'
import * as M from 'foldkit/match'
const { div, header, main, keyed } = html<Message>()
const view = (model: Model): Html => {
const routeContent = M.value(model.route).pipe(
M.tagsExhaustive({
Products: () => productsView(model),
Cart: () => cartView(model),
Checkout: () => checkoutView(model),
NotFound: ({ path }) => notFoundView(path),
}),
)
return div(
[],
[
header([], [navigationView(model.route)]),
main([], [keyed('div')(model.route._tag, [], [routeContent])]),
],
)
}When the app switches between entirely different layouts — a landing page vs. a docs layout with sidebar vs. a dashboard — key the outermost container of each branch with a stable string:
import { html } from 'foldkit/html'
import * as M from 'foldkit/match'
const { div, keyed } = html<Message>()
const view = (model: Model): Html =>
M.value(model.route).pipe(
M.tagsExhaustive({
Landing: () => keyed('div')('landing', [], [heroView(), featuresView()]),
Docs: () =>
keyed('div')('docs', [], [sidebarView(), docsContentView(model)]),
Dashboard: () =>
keyed('div')('dashboard', [], [dashboardNavView(), panelsView(model)]),
}),
)Without this, navigating from a full-width landing page to a sidebar docs layout would cause Snabbdom to try to morph one into the other — reusing DOM nodes across completely different structures.
When the model itself is a discriminated union with structurally different views per variant, key on model._tag:
import { html } from 'foldkit/html'
import * as M from 'foldkit/match'
const { div, keyed } = html<Message>()
const view = (model: Model): Html =>
div(
[],
[
keyed('div')(
model._tag,
[],
[
M.value(model).pipe(
M.tagsExhaustive({
LoggedOut: loggedOutModel =>
LoginForm.view(loggedOutModel, message =>
GotLoginFormMessage({ message }),
),
LoggedIn: loggedInModel =>
Dashboard.view(loggedInModel, message =>
GotDashboardMessage({ message }),
),
}),
),
],
),
],
)This ensures that logging in fully tears down the login form and builds the dashboard from scratch, rather than patching one into the other.