Domphy

Refs & DOM Access

Domphy does not have a ref API like React. Instead, use the _onMount and _onRemove lifecycle hooks to access the underlying DOM node.

_onMount

Called after the element is inserted into the DOM. The argument is the ElementNode wrapper with a .domElement property:

import type { ElementNode } from "@domphy/core"

const Canvas = {
  canvas: null,
  width: 400,
  height: 300,
  _onMount: (node: ElementNode) => {
    const canvas = node.domElement as HTMLCanvasElement
    const ctx = canvas.getContext("2d")!
    ctx.fillStyle = "#6366f1"
    ctx.fillRect(0, 0, 400, 300)
  },
}

_onRemove

Called before the element is removed from the DOM. Use it to clean up timers, observers, and event listeners:

let interval: ReturnType<typeof setInterval>

const LiveClock = {
  time: null,
  _onMount: (node: ElementNode) => {
    const el = node.domElement as HTMLTimeElement
    interval = setInterval(() => {
      el.textContent = new Date().toLocaleTimeString()
    }, 1000)
  },
  _onRemove: () => {
    clearInterval(interval)
  },
}

Storing element references

Close over a variable to keep a ref you can use later:

import { toState } from "@domphy/core"
import type { ElementNode } from "@domphy/core"

let inputRef: HTMLInputElement | null = null

const SearchInput = {
  input: null,
  type: "search",
  placeholder: "Search…",
  _onMount: (node: ElementNode) => {
    inputRef = node.domElement as HTMLInputElement
  },
  _onRemove: () => {
    inputRef = null
  },
}

// Programmatically focus the input
function focusSearch() {
  inputRef?.focus()
}

const SearchButton = {
  button: "Focus search",
  onClick: focusSearch,
}

toState as a ref

Use toState<HTMLElement | null>(null) to make the ref reactive — useful when other elements need to observe it:

import { toState } from "@domphy/core"
import type { ElementNode } from "@domphy/core"

const anchorRef = toState<HTMLButtonElement | null>(null)

const Anchor = {
  button: "Open",
  _onMount: (node: ElementNode) => {
    anchorRef.set(node.domElement as HTMLButtonElement)
  },
  _onRemove: () => anchorRef.set(null),
}

const Popover = {
  div: "Popover content",
  // Position relative to the anchor
  style: (l) => {
    const anchor = anchorRef.get(l)
    if (!anchor) return { display: "none" }
    const rect = anchor.getBoundingClientRect()
    return {
      position: "fixed",
      top: `${rect.bottom + 8}px`,
      left: `${rect.left}px`,
    }
  },
}

Wiring third-party libraries

_onMount is the right place to integrate any library that requires a DOM node:

// Chart.js integration
import { Chart } from "chart.js/auto"

let chart: Chart | null = null

const ChartCanvas = {
  canvas: null,
  _onMount: (node: ElementNode) => {
    chart = new Chart(node.domElement as HTMLCanvasElement, {
      type: "bar",
      data: { labels: ["A", "B", "C"], datasets: [{ data: [1, 2, 3] }] },
    })
  },
  _onRemove: () => {
    chart?.destroy()
    chart = null
  },
}

Forwarding refs to child elements

When a component needs to expose a DOM ref to its parent, accept a callback:

import type { ElementNode } from "@domphy/core"

interface InputProps {
  placeholder?: string
  onRef?: (el: HTMLInputElement | null) => void
}

function Input({ placeholder, onRef }: InputProps) {
  return {
    input: null,
    type: "text",
    placeholder: placeholder ?? "",
    _onMount: (node: ElementNode) => onRef?.(node.domElement as HTMLInputElement),
    _onRemove: () => onRef?.(null),
  }
}

// Usage: parent gets the DOM element
let ref: HTMLInputElement | null = null
const field = Input({ placeholder: "Name", onRef: (el) => { ref = el } })

ResizeObserver

Observe size changes without polling:

import { toState } from "@domphy/core"

const width = toState(0)
let observer: ResizeObserver | null = null

const ResponsiveBox = {
  div: (l) => `Width: ${width.get(l)}px`,
  _onMount: (node) => {
    observer = new ResizeObserver(([entry]) => {
      width.set(Math.round(entry.contentRect.width))
    })
    observer.observe(node.domElement)
  },
  _onRemove: () => {
    observer?.disconnect()
    observer = null
  },
}

IntersectionObserver

Trigger lazy loading or animations when an element enters the viewport:

import { toState } from "@domphy/core"

function lazyLoad(src: string) {
  const visible = toState(false)
  let observer: IntersectionObserver | null = null

  return {
    img: null,
    src: (l) => visible.get(l) ? src : "",
    alt: "lazy image",
    _onMount: (node) => {
      observer = new IntersectionObserver(([entry]) => {
        if (entry.isIntersecting) {
          visible.set(true)
          observer?.disconnect()
        }
      }, { rootMargin: "200px" })
      observer.observe(node.domElement)
    },
    _onRemove: () => observer?.disconnect(),
  }
}

Comparing to React

ReactDomphy
useRef<T>(null)ref.currentlet ref: T | null = null closed over in _onMount
ref={myRef} on JSX_onMount: (node) => { ref = node.domElement }
useCallback refCallback passed as prop, called in _onMount
useImperativeHandleReturn an object with methods from a function, close over DOM ref
forwardRefAccept onRef callback as prop