Skip to main content
On this pageKeying

Keying

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.

Route Views

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])]),
    ],
  )
}

Layout Branches

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.

Model State Branches

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.

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson