The Handle object provides the component's interface to the framework.
Schedules a component update and returns a promise that resolves with an AbortSignal after
the update completes.
function Counter(handle: Handle) {
let count = 0
return () => (
<button
mix={[
on('click', () => {
count++
handle.update()
}),
]}
>
Count: {count}
</button>
)
}Waiting for the update:
function Player(handle: Handle) {
let isPlaying = false
let stopButton: HTMLButtonElement
return () => (
<button
disabled={isPlaying}
mix={[
on('click', async () => {
isPlaying = true
await handle.update()
stopButton.focus()
}),
]}
>
Play
</button>
)
}Schedules a task to run after the next update. The task receives an AbortSignal that's aborted when:
- The component re-renders (new render cycle starts)
- The component is removed from the tree
Use queueTask in event handlers when work needs to happen after DOM changes:
function Form(handle: Handle) {
let showDetails = false
let detailsSection: HTMLElement
return () => (
<form>
<input
type="checkbox"
checked={showDetails}
mix={[
on('change', (event) => {
showDetails = event.currentTarget.checked
handle.update()
if (showDetails) {
// Queue DOM operation after the new section renders
handle.queueTask(() => {
detailsSection.scrollIntoView({ behavior: 'smooth' })
})
}
}),
]}
/>
{showDetails && (
<section mix={[ref((node) => (detailsSection = node))]}>Details content</section>
)}
</form>
)
}Use queueTask for work that needs to be reactive to prop changes:
When you need to perform async work (like data fetching) that should respond to prop changes, use queueTask in the render function. The signal will be aborted if props change or the component is removed, ensuring only the latest work completes.
Don't create states as values to "react to" on the next render with queueTask:
// ❌ Avoid: Creating state just to react to it in queueTask
function BadExample(handle: Handle) {
let shouldLoad = false // Unnecessary state
return () => (
<div>
<button
mix={[
on('click', () => {
shouldLoad = true // Setting state just to trigger queueTask
handle.update()
handle.queueTask(() => {
if (shouldLoad) {
// Do work
}
})
}),
]}
>
Load
</button>
</div>
)
}
// ✅ Prefer: Do the work directly in the event handler or queueTask
function GoodExample(handle: Handle) {
return () => (
<div>
<button
mix={[
on('click', () => {
handle.queueTask(() => {
// Do work directly - no intermediate state needed
})
}),
]}
>
Load
</button>
</div>
)
}When showing loading state before async work, await handle.update() and use the returned signal:
function AsyncExample(handle: Handle) {
let data: string[] = []
let loading = false
async function load() {
loading = true
let signal = await handle.update()
let response = await fetch('/api/data', { signal })
if (signal.aborted) return
data = await response.json()
loading = false
handle.update()
}
return () => <button mix={[on('click', load)]}>{loading ? 'Loading...' : 'Load data'}</button>
}An AbortSignal that's aborted when the component is disconnected. Useful for cleanup operations.
function Clock(handle: Handle) {
let interval = setInterval(() => {
if (handle.signal.aborted) {
clearInterval(interval)
return
}
handle.update()
}, 1000)
return () => <span>{new Date().toString()}</span>
}Or using event listeners:
function Clock(handle: Handle) {
let interval = setInterval(handle.update, 1000)
handle.signal.addEventListener('abort', () => clearInterval(interval))
return () => <span>{new Date().toString()}</span>
}Listen to an EventTarget with automatic cleanup when the component disconnects. Ideal for global event targets like document and window.
function KeyboardTracker(handle: Handle) {
let keys: string[] = []
addEventListeners(document, handle.signal, {
keydown(event) {
keys.push(event.key)
handle.update()
},
})
return () => <div>Keys: {keys.join(', ')}</div>
}The root frame for the current runtime tree. This is useful when nested components need to reload the entire page/frame tree instead of only their nearest frame.
When server rendering with renderToStream(), pass the frameSrc option to populate handle.frames.top.src during SSR. For nested frame renders, also pass topFrameSrc to keep the top-frame URL fixed while handle.frame.src changes per frame.
function RefreshAllButton(handle: Handle) {
return () => (
<button
mix={[
on('click', async () => {
await handle.frames.top.reload()
}),
]}
>
Refresh everything
</button>
)
}Look up a named frame in the current runtime tree. This is useful when one frame action should refresh adjacent frame content.
Return value:
FrameHandlewhen a frame with thatnameis currently mountedundefinedwhen no such frame is mounted
function CartRow(handle: Handle) {
return () => (
<button
mix={[
on('click', async () => {
await handle.frames.get('cart-summary')?.reload()
await handle.frame.reload()
}),
]}
>
Update Cart
</button>
)
}If multiple mounted frames share the same name, the most recently mounted frame is returned.
Stable identifier per component instance. Useful for HTML APIs like htmlFor, aria-owns, etc.
function LabeledInput(handle: Handle) {
return () => (
<div>
<label htmlFor={handle.id}>Name</label>
<input id={handle.id} type="text" />
</div>
)
}Context API for ancestor/descendant communication. See Context for full documentation.
function App(handle: Handle<{ theme: string }>) {
handle.context.set({ theme: 'dark' })
return () => (
<div>
<Header />
<Content />
</div>
)
}
function Header(handle: Handle) {
let { theme } = handle.context.get(App)
return () => (
<header mix={[css({ backgroundColor: theme === 'dark' ? '#000' : '#fff' })]}>Header</header>
)
}Important: handle.context.set() does not cause any updates—it simply stores a value. If you need the component tree to update when context changes, call handle.update() after setting the context.