Component Anatomy
A Verbose component is a TypeScript class that extends BaseComponent. This guide walks through each part of a component and how they connect.
Overview
import { Component, Prop, State, Watch, Emit, Slot, OnCommand } from '@verbose/decorators'
import { Command, createCommand, BaseComponent } from '@verbose/decorators'
import { resource } from '@verbose/core'
@Component({ tag: 'user-card' })
class UserCard extends BaseComponent {
// ── Props ──────────────────────────────────────────────────────────────
@Prop() userId!: number
@Prop() elevated = false
@Prop() onSave?: (user: User) => void
@Prop() refresh?: Command
// ── Local state ─────────────────────────────────────────────────────────
@State() editing = false
@State() name = ''
// ── Async data ──────────────────────────────────────────────────────────
user = resource(async () => {
const res = await fetch(`/api/users/${this.props.userId}`)
return res.json() as Promise<User>
})
// ── Watchers ─────────────────────────────────────────────────────────────
@Watch('editing')
onEditingChange(next: boolean) {
if (next) this.name = this.user.data.value?.name ?? ''
}
// ── Inbound commands ─────────────────────────────────────────────────────
@OnCommand('refresh')
handleRefresh() {
this.user.refetch()
}
// ── Emitted events ───────────────────────────────────────────────────────
@Emit('onSave')
save() {
return { ...this.user.data.value, name: this.name }
}
// ── Slots ────────────────────────────────────────────────────────────────
@Slot('actions') actions!: Child[]
@Slot() default!: Child[]
// ── Lifecycle ────────────────────────────────────────────────────────────
onMount() {
console.log('UserCard mounted, userId:', this.props.userId)
}
onUnmount() {
console.log('UserCard unmounted')
}
// ── Render ───────────────────────────────────────────────────────────────
render() {
const { data, pending, error } = this.user
if (pending.value) return <div class="skeleton" />
if (error.value) return <div class="error">{error.value.message}</div>
return (
<div class={`card ${this.props.elevated ? 'elevated' : ''}`}>
<header>
{this.editing ? (
<input value={this.name} onInput={e => { this.name = (e.target as HTMLInputElement).value }} />
) : (
<h2>{data.value?.name}</h2>
)}
</header>
<main>{this.default}</main>
<footer>
{this.editing ? (
<button onClick={() => this.save()}>Save</button>
) : (
<button onClick={() => { this.editing = true }}>Edit</button>
)}
{this.actions}
</footer>
</div>
)
}
}Each part explained
1. @Component
Registers the class as a component and defines metadata.
@Component({ tag: 'user-card' })
class UserCard extends BaseComponent { }tag— custom element tag name (optional)- Every component class extends
BaseComponent
2. Props — @Prop()
Props are values received from the parent. The decorated property value is the local default — the parent always takes priority.
@Prop() userId!: number // required (parent always provides)
@Prop() elevated = false // optional with default
@Prop() onSave?: (u: User) => void // callback prop
@Prop() refresh?: Command // imperative command propAlways read props via this.props.propName inside render() to ensure reactivity:
render() {
return <div class={this.props.elevated ? 'elevated' : ''} />
}3. Local state — @State()
Internal component state. Each @State property is a signal; any assignment schedules a re-render.
@State() editing = false
@State() name = ''Direct read and write:
this.editing // reads current value
this.editing = true // updates and schedules re-renderTo access the underlying signal (e.g. to pass to composables):
import { getSignal } from '@verbose/decorators'
const editingSignal = getSignal<boolean>(this, 'editing')4. Async data — resource
Manages data fetching with reactive pending, error, and data state. Re-executes whenever any signal read inside the fetcher changes.
user = resource(async () => {
const res = await fetch(`/api/users/${this.props.userId}`)
return res.json()
})In the template:
if (this.user.pending.value) return <Spinner />
if (this.user.error.value) return <Error />
return <h2>{this.user.data.value?.name}</h2>Manual actions:
this.user.refetch() // re-trigger the fetcher
this.user.cancel() // abort the in-flight request
this.user.mutate({ name: 'x' }) // optimistic update5. Watchers — @Watch
Runs a method whenever one or more @State / @Prop properties change.
@Watch('editing')
onEditingChange(next: boolean, prev: boolean) {
if (next) this.name = this.user.data.value?.name ?? ''
}Multiple properties at once:
@Watch('firstName', 'lastName')
onNameChange([newFirst, newLast]: [string, string]) {
this.fullName = `${newFirst} ${newLast}`
}6. Inbound commands — @OnCommand
Subscribes to a Command prop, executing the method when the parent calls command.trigger(). Automatically unsubscribes on unmount.
@Prop() refresh?: Command
@OnCommand('refresh')
handleRefresh() {
this.user.refetch()
}In the parent:
const refreshCmd = createCommand()
<UserCard refresh={refreshCmd} />
refreshCmd.trigger() // calls handleRefresh7. Emitted events — @Emit
Binds the method and calls the named prop callback with the return value. Ensures correct this binding.
@Prop() onSave?: (user: User) => void
@Emit('onSave')
save() {
return { ...this.user.data.value, name: this.name }
// return value is passed to this.props.onSave(...)
}In the parent:
<UserCard onSave={(user) => console.log('saved:', user)} />8. Slots — @Slot
Allows distributing content into the component. An unnamed slot receives direct children with no slot attribute.
@Slot('actions') actions!: Child[] // matched by <div slot="actions">
@Slot() default!: Child[] // children without slot=""In the parent:
<UserCard userId={1}>
<p>This goes into the default slot</p>
<div slot="actions">
<button>Delete</button>
</div>
</UserCard>9. Lifecycle
Override lifecycle methods directly on the class.
| Method | When it runs |
|---|---|
onBeforeMount() | Before first render |
onMount() | After first DOM insertion |
onBeforeUpdate(prev) | Before re-render triggered by prop change |
onAfterUpdate(prev) | After DOM update |
onUnmount() | When removed from DOM |
onError(err) | On uncaught error inside the component |
onMount() {
this._timerId = setInterval(() => this.tick(), 1000)
}
onUnmount() {
clearInterval(this._timerId)
}10. Render
The only required method. Must return a VNode or null. Re-executes reactively whenever any signal read inside it changes.
render() {
// Signal reads here create reactive dependencies
const { data, pending } = this.user
return (
<div>
{pending.value ? <Spinner /> : <h2>{data.value?.name}</h2>}
</div>
)
}TIP
Prefer reading @State and @Prop values directly inside render() rather than storing them in variables outside — this ensures the reactive tracking works correctly.
Data flow
Parent
│
@Prop / Command
│
▼
┌─────────────┐
│ Component │
│ │
│ @State ◄───┼── @Watch
│ resource │
│ @Slot ◄────┼── children from parent
│ │
└──────┬──────┘
│
render() ──► DOM
│
@Emit / callback props
│
▼
ParentComposing with external utilities
import { createRef, useElementSize } from '@verbose/composables'
import { tween } from '@verbose/motion'
@Component()
class AnimatedPanel extends BaseComponent {
ref = createRef<HTMLDivElement>()
size = useElementSize(this.ref)
opacity = tween(0, 1, { duration: 400 })
onMount() {
this.opacity.target.set(1)
}
render() {
return (
<div
ref={this.ref}
style={{ opacity: String(this.opacity.value.value) }}
>
<p>Width: {this.size.width.value}px</p>
</div>
)
}
}