Skip to main content
On this pageOverview

Mount

Overview

Most Foldkit code is declarative. The view is a function from Model to Html. It doesn’t reach into the DOM, it doesn’t hold references, it doesn’t run side effects. That purity is what makes Foldkit programs predictable.

Mount is the moment an element enters the live DOM, when the virtual DOM becomes real. OnMount is the seam where view code can drop down to imperative work at that moment. It runs an Effect with the live Element, and pairs it with cleanup that fires when the element unmounts. The Effect resolves to a Message that flows back through update like every other side effect in the architecture.

Functional core, imperative shell

The view describes what should be on screen. OnMount describes what to do at the boundary where the virtual DOM meets the real one. Mount-time async work stays expressed as an Effect, so its outcome flows back through update like any other Message. The cleanup is data, not a separate hook: paired with the setup as a single value the runtime owns.

Side Effects on Mount

The simplest mount-time work is setup that runs once and needs no teardown: focusing an input as soon as it enters the page, scrolling to a saved position, firing an analytics event. Pass Function.constVoid as the cleanup; the Completed* Message marks the lifecycle without forcing a meaningful response in update.

import { Effect, Function } from 'effect'
import { Mount } from 'foldkit'
import type { Html, MountResult } from 'foldkit/html'
import { m } from 'foldkit/message'

import { Class, OnMount, Type, input } from '../html'

const CompletedFocusInput = m('CompletedFocusInput')

// Setup-only actions pass Function.constVoid as the cleanup. The
// runtime still tracks the lifecycle, but there is nothing to undo
// when the input unmounts. The Completed* Message marks the dispatch
// without forcing a meaningful response in update.

const FocusInput = Mount.define('FocusInput', CompletedFocusInput)

const focusInput = FocusInput(
  (element): Effect.Effect<MountResult<typeof CompletedFocusInput.Type>> =>
    Effect.sync(() => {
      if (element instanceof HTMLInputElement) {
        element.focus()
      }
      return {
        message: CompletedFocusInput(),
        cleanup: Function.constVoid,
      }
    }),
)

const searchInputView = (): Html =>
  input([
    Type('search'),
    Class('w-full px-3 py-2 rounded-md border'),
    OnMount(focusInput),
  ])

Third-Party Libraries

OnMount really earns its keep when a library owns its own DOM. Charts, code editors, map renderers, force-directed graphs: each expects a real element to render into and a way to be torn down later.

It takes an (element: Element) => Effect<MountResult<Message>>. The Effect resolves to a { message, cleanup } record. The runtime dispatches the Message and stashes the cleanup. When Snabbdom later removes the node, OnMount invokes the cleanup automatically. For setup with no cleanup, pass Function.constVoid as the cleanup.

import { Effect, Function, Schema as S } from 'effect'
import { Mount } from 'foldkit'
import type { Html, MountResult } from 'foldkit/html'
import { m } from 'foldkit/message'

import { Class, OnMount, div } from '../html'

const SucceededMountChart = m('SucceededMountChart')
const FailedMountChart = m('FailedMountChart', { reason: S.String })

// Mount.define gives the action a name and constrains what Messages it can
// produce. The runtime invokes the wrapped function on insert, dispatches the
// Message, and stashes the cleanup. When Snabbdom unmounts the element,
// OnMount calls the cleanup automatically.

const MountChart = Mount.define(
  'MountChart',
  SucceededMountChart,
  FailedMountChart,
)

const mountChart = (data: ChartData) =>
  MountChart(
    (element: Element): Effect.Effect<MountResult<Message>> =>
      Effect.gen(function* () {
        const { Chart } = yield* Effect.tryPromise(
          () => import('some-chart-library'),
        )
        const chart = new Chart(element, { data })
        return {
          message: SucceededMountChart(),
          cleanup: () => chart.destroy(),
        }
      }).pipe(
        Effect.catchAll(error =>
          Effect.succeed({
            message: FailedMountChart({
              reason: error instanceof Error ? error.message : String(error),
            }),
            cleanup: Function.constVoid,
          }),
        ),
      ),
  )

const chartView = (data: ChartData): Html =>
  div([Class('w-[480px] h-[320px]'), OnMount(mountChart(data))], [])

The Model owns the data going in. The library owns its rendered subtree. The runtime owns the lifecycle.

What if the Effect is still in flight when the element is removed?

The runtime tracks both states. If unmount happens before the Effect resolves, the cleanup runs as soon as it arrives and the Message is suppressed. The chart never leaks, the Model never sees a mounted Message for an element that’s already gone.

Pairing with Subscriptions

To handle events from a mounted DOM node, like a code editor’s buffer changes, a map’s viewport, or a zoom gesture’s deltas, pair OnMount with a Subscription. Dispatch a stable identifier for the element from the mount Message, store it in the Model, and key a Subscription on that identifier. The Subscription attaches the library’s listeners and emits a Message for each event. OnMount announces the seam exists. The Subscription carries the traffic.

The Map example demonstrates the pattern: OnMount constructs the MapLibre instance and dispatches the host id; the Subscription keyed on that id wires up moveend and marker-click listeners and emits MovedMap / ClickedMarker Messages back into the Model.

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson