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) => valueand returns aSignalthat is kept in sync with the latest props passed to the component.id: a lazily-createdSignal<string>that holds a deterministically generated id for this spot in the virtual DOM. This is ideal for wiringlabel/inputpairs:
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.