Lifecycles

Kiru provides several lifecycle primitives for working with component setup, side effects, and hot reloading.

Stateful Kiru components (those that return a render function) are written in two phases:

  • setup - the outer function body that runs once per component instance. This is where you initialize state, effects, and lifecycle hooks.
  • render - the inner function you return, which is called every time the component re-renders

Because setup only runs once per instance and Kiru tracks which signals each component actually observes, stateful components behave as though they are "auto-memoized" - work in setup is not repeated on every render, and render only runs again when an explicitly observed signal or props change.

The setup lifecycle exposes a helper function, setup<Props>(), that lets you derive props into signals or access a stable, deterministic id for the current position in the virtual DOM (equivalent to useId in React).

interface CounterProps {
  initialCount?: number
}

const Counter: Kiru.FC<CounterProps> = () => {
  const { derive } = setup<CounterProps>()
  // Alternatively, setup<typeof Counter>()

  const count = derive((props) => props.initialCount ?? 0)
  // When "initialCount" changes, count is updated.

  return () => (
    <>
      <p>Count: {count}</p>
      <button onclick={() => count.value++}>Increment</button>
      <button onclick={() => count.value--}>Decrement</button>
    </>
  )
}

setup() returns an object with:

  • derive(selector): takes a selector (props) => value and returns a Signal that is kept in sync with the latest props passed to the component.
  • id: a lazily-created Signal<string> that holds a deterministically generated id for this spot in the virtual DOM. This is ideal for wiring label/input pairs:
const Field: Kiru.FC = () => {
  const { id } = setup()

  return () => (
    <>
      <label htmlFor={id}>Name</label>
      <input id={id} type="text" />
    </>
  )
}

onMount runs a callback after the component is first painted to the DOM.

import { signal, onMount } from "kiru"

function Timer() {
  const seconds = signal(0)

  onMount(() => {
    const interval = setInterval(() => {
      seconds.value++
    }, 1000)

    return () => clearInterval(interval)
  })

  return () => <div>Elapsed: {seconds}s</div>
}

onBeforeMount runs a callback before the component is painted — synchronously, after the DOM nodes are created but before the browser has had a chance to render.

Use this when you need to read layout or make DOM mutations that should be invisible to the user.

import { signal, onBeforeMount } from "kiru"

function Modal() {
  onBeforeMount(() => {
    const prev = document.body.style.overflow
    document.body.style.overflow = "hidden"

    return () => {
      document.body.style.overflow = prev
    }
  })

  return () => <div className="modal">...</div>
}

onCleanup registers a teardown function that runs when the component is unmounted. It can be called inside any lifecycle hook, or at the top level of a component.

import { signal, onCleanup } from "kiru"

function MouseTracker() {
  const x = signal(0),
    y = signal(0)

  const handler = (e: MouseEvent) => {
    x.value = e.clientX
    y.value = e.clientY
  }

  window.addEventListener("mousemove", handler)
  onCleanup(() => window.removeEventListener("mousemove", handler))

  return () => (
    <div>
      x: {x}, y: {y}
    </div>
  )
}

onHmr lets you register callbacks that run when Hot Module Reload (HMR) replaces code during development.

import { onHmr } from "kiru"

// Start an interval in module scope
const interval = setInterval(() => {
  console.log("tick")
}, 1000)

// Stop the interval the next time this file is reloaded
onHmr(() => clearInterval(interval))
  • When called during module evaluation (top-level scope), the callback runs the next time that module is replaced.
  • When called anywhere else (e.g. inside an event handler), the callback runs on the next HMR update, regardless of which module changed.
  • Each callback only runs once per registration.