Components
Everything in Kraft is a component. Here's how they work.
The component model
Every Kraft component is a Kotlin class that extends Component<PROPS> and implements one method: render().
class Greeting(ctx: Ctx<Props>) : Component<Greeting.Props>(ctx) {
data class Props(val name: String)
override fun VDom.render() {
h2 { +"Hello, ${props.name}!" }
}
} Alongside the class, you define a factory function so other components can use it in the HTML DSL:
@Suppress("FunctionName")
fun Tag.Greeting(name: String) = comp(
Greeting.Props(name = name)
) { Greeting(it) } Now any parent component can write:
override fun VDom.render() {
Greeting(name = "World")
} Component types
Component with props
The most common type. Props are a data class — immutable, type-safe, and IDE-friendly.
class UserCard(ctx: Ctx<Props>) : Component<UserCard.Props>(ctx) {
data class Props(
val name: String,
val email: String,
val role: String = "member", // Default values work
)
override fun VDom.render() {
ui.card {
noui.content {
ui.header { +props.name }
noui.description { +props.email }
}
noui.extra.content {
ui.label { +props.role }
}
}
}
} PureComponent (no props)
For components that don't need external data — pages, layouts, root components:
class DashboardPage(ctx: NoProps) : PureComponent(ctx) {
override fun VDom.render() {
ui.container {
ui.header H1 { +"Dashboard" }
// ...
}
}
} PureComponent is a convenience alias for Component<Any?> with NoProps context.
Functional components
For simple, stateless rendering — no class needed:
val Badge = component { name: String, color: String ->
ui.with(color).label { +name }
}
// Usage
override fun VDom.render() {
Badge("Admin", "red")
Badge("Active", "green")
} Functional components support up to 10 parameters.
Lifecycle hooks
Components register lifecycle handlers in their init block via the lifecycle DSL —
onMount, onUpdate, onUnmount, onNextProps,
onError
(error boundaries), onResize, and browser window events.
init {
lifecycle {
onMount { /* component is in the DOM */ }
onUpdate { /* DOM updated after re-render */ }
onUnmount { /* component removed */ }
onNextProps { new, old -> /* parent sent new props */ }
onError { e -> /* caught from render or child */ }
}
} See the Lifecycle Hooks page for the full reference with examples for each hook.
Key point: Stream subscriptions (via subscribingTo()) auto-unsubscribe when the
component unmounts. You don't need to clean them up manually.
Component references
Sometimes a parent needs to call methods on a child. Use ComponentRef:
class DrawingApp(ctx: NoProps) : PureComponent(ctx) {
// Create a ref tracker for the SignaturePad component
private val padRef = ComponentRef.Tracker<SignaturePad>()
override fun VDom.render() {
// The SignaturePad component, tracked by our ref
SignaturePad {
// configuration...
}.track(padRef)
// Use the ref to access the child component
padRef { pad ->
ui.button {
onClick { pad.clear() }
+"Clear signature"
}
if (pad.isEmpty()) {
ui.red.label { +"No signature yet" }
}
}
}
} The padRef { ... } block only renders when the ref is attached — it's safe by design.
Composing components
Components compose naturally through the HTML DSL:
class App(ctx: NoProps) : PureComponent(ctx) {
override fun VDom.render() {
div(classes = "app") {
NavBar()
ui.container {
Sidebar()
MainContent()
}
Footer()
}
}
} Passing callbacks
Components communicate upward through callback props:
class TodoItem(ctx: Ctx<Props>) : Component<TodoItem.Props>(ctx) {
data class Props(
val text: String,
val done: Boolean,
val onToggle: () -> Unit,
val onDelete: () -> Unit,
)
override fun VDom.render() {
ui.item {
ui.checkbox {
input(type = InputType.checkBox) {
checked = props.done
onChange { props.onToggle() }
}
label { +props.text }
}
ui.red.icon.button {
onClick { props.onDelete() }
icon.trash()
}
}
}
} The shouldRedraw optimization
By default, components redraw whenever props change. Override shouldRedraw to skip unnecessary renders:
override fun shouldRedraw(nextProps: Props): Boolean {
return nextProps != props // Only redraw if props actually changed
} Since Props is a data class, != does a structural comparison — this is efficient and correct.
Accessing app services
Components can access app-level services through the attribute system:
override fun VDom.render() {
// These are available in any component
val router = router // Navigation
val modals = modals // Show modals
val toasts = toasts // Show notifications
val popups = popups // Show popups and context menus
val responsive = responsiveCtrl() // Screen size info
} These are injected via Kraft's attribute system — no manual wiring needed. See Overlays for modals, toasts, and popups. See Utilities for responsive design.
Keys for list rendering
When rendering a list of items, set a key on each element so Preact can efficiently track which
items changed, moved, or were removed:
ui.list {
items.forEach { item ->
noui.item {
key = item.id // stable, unique identifier
+item.name
}
}
} Without keys, Preact re-creates every element on each render. With keys, it only updates what actually changed. Use a stable identifier (database ID, unique string) — not the list index.
// Also works on components
users.forEach { user ->
UserCard(user) // implicit key from props
// Or set explicitly on the wrapper
div {
key = user.id
UserCard(user)
}
} Raw HTML with unsafe
The unsafe block injects raw HTML strings directly into the DOM, bypassing the type-safe DSL:
div {
unsafe {
+"""<em>This is raw HTML</em>"""
}
} // SAFE: sanitize untrusted content before injecting
import io.peekandpoke.kraft.addons.marked.markdown2html
div {
unsafe {
// markdown2html uses DOMPurify internally
+markdown2html(untrustedMarkdown)
}
} Key the parent of unsafe
Preact cannot diff raw HTML — it treats the entire unsafe block as an opaque blob. On each
re-render, the raw DOM is destroyed and recreated from scratch. To prevent this, set a key on the
parent element. If the key doesn't change, Preact preserves the DOM subtree:
div {
key = "article-${article.id}"
unsafe {
+article.htmlContent
}
} Without the key, every component re-render rebuilds the raw HTML DOM — which is wasteful for large content and causes visible flicker. With a stable key, the DOM is only rebuilt when the key changes.