Handle user and view events
Use event props for actions reported by a view. Use bound signals for controls whose value should stay synchronized with app state.
Bind a field or handle its changes
import { Text, TextField, VStack, signal } from 'kuito';
export function Search() {
const query = signal('');
return (
<VStack gap={8}>
<TextField label="Search" value={query} onSubmit={() => console.log(query.value)} />
<Text>{query.value}</Text>
</VStack>
);
}Passing a writable signal to a Bound prop enables two-way updates. Passing a plain value or read-only signal displays it without writing back. TextField uses value, Toggle uses checked, Sheet uses open, and refreshable views use refreshing. Changes set by the app do not generate a second user-change event.
Choose the event
| Event prop | Use it for |
|---|---|
onPress |
A completed action |
onPressStart, onPressEnd |
Press tracking and feedback |
onLongPress |
A long-press action |
onChange |
The new text or toggle value |
onSubmit |
Submission from a text field |
onFocus, onBlur |
Keyboard focus changes |
onLoad, onError |
Media load outcomes |
onScroll |
Scroll event data when a JS callback is needed |
onAction |
An accessibility action |
Native module views define their own event payloads. Consult the component's prop reference instead of assuming every callback receives a DOM-shaped event.
Listen from a ref
A view's ref is an event target: addEventListener,
removeEventListener and dispatchEvent work as in the DOM, with the
capture, once and signal options. Listeners hear events by their native
name, which is Kuito's own (press, not the DOM's click):
| Event prop | Event name |
|---|---|
onPress, onPressCapture |
press |
onPressStart, onPressEnd |
pressin, pressout |
onLongPress |
longpress |
onChange, onSubmit |
change, submit |
onFocus, onBlur |
focus, blur |
onLoadStart, onLoad, onError |
loadstart, load, error |
onScroll, onScrollEnd |
scroll, scrollend |
onAction |
accessibilityaction |
This form holds back every press inside it while a save runs. Its listener hears the press in the capture phase, before the button does:
import { Box, Button, onMount, ref, signal, type KuitoNativeEvent } from 'kuito';
const saving = signal(false);
export function Form(props: { onSave: () => void }) {
const form = ref();
const guard = (event: KuitoNativeEvent) => {
if (saving.value) event.stopPropagation();
};
onMount(() => form.current?.addEventListener('press', guard, { capture: true }));
return (
<Box ref={form}>
<Button onPress={props.onSave}>Save</Button>
</Box>
);
}A view's listeners go with it: removing the view removes them. Remove one
sooner with removeEventListener, passing the same function and capture,
or with an AbortSignal given as signal.
How an event travels
A press travels the view tree as a DOM event does. It goes down from the root to the pressed view (capture), reaches that view (target), then goes back up (bubble). It bubbles and is cancelable. Every other native event reaches its view only: it does not bubble, and capture listeners do not hear it.
At each view the prop runs before the listeners: onPressCapture in the
capture phase, onPress at the target and while bubbling. A Box with
onPressCapture and onPress around a Button with onPress and a press
listener runs, in order: the Box's onPressCapture, the Button's onPress,
the Button's listener, the Box's onPress.
| Member | Does |
|---|---|
stopPropagation() |
Stops the event once the current view has run its prop and listeners |
stopImmediatePropagation() |
Stops it now: the current view's remaining listeners do not run |
preventDefault() |
On a cancelable event, sets defaultPrevented; dispatchEvent returns false |
target, currentTarget |
The view the event is for, and the view running the listener |
eventPhase |
1 capture, 2 target, 3 bubble |
composedPath() |
The views from the target up to the root |
preventDefault() only marks the event in Kuito: what the platform already
did, such as a button's highlight or a haptic, still happens. To make a view
ignore touches, set disabled.
Dispatch a custom event
dispatchEvent sends an event from a view through the same phases. Use a
KuitoCustomEvent to carry data, and bubbles: true for ancestors to hear
it. A listener receives the object that was dispatched, so instanceof
recovers its detail:
import { Box, KuitoCustomEvent, Pressable, Text, onMount, ref } from 'kuito';
function ReloadButton() {
const row = ref();
const ask = () =>
row.current?.dispatchEvent(
new KuitoCustomEvent('refresh', { detail: { force: true }, bubbles: true }),
);
return (
<Pressable ref={row} onPress={ask}>
<Text>Reload</Text>
</Pressable>
);
}
export function Feed() {
const feed = ref();
onMount(() =>
feed.current?.addEventListener('refresh', (event) => {
if (event instanceof KuitoCustomEvent) console.log('refresh', event.detail);
}),
);
return (
<Box ref={feed}>
<ReloadButton />
</Box>
);
}dispatchEvent returns false when a listener called preventDefault() on a
cancelable event. Prefer props and callbacks for ordinary parent-child
communication; use a custom event when the views in between should not have
to pass it along.
| Call | Fails with |
|---|---|
dispatchEvent on a removed view |
TypeError: Unknown event target |
addEventListener with a listener that is not a function |
TypeError: Listener must be a function |
Event lifetime
Removed views do not receive late events. Do not use onBlur as an unmount cleanup hook; use onCleanup. Errors in action handlers must be handled there rather than relying on Catch. For per-frame visual updates prefer motion sources over sending every event through JS.