How to: two-way bind a value
You want a native control’s value to stay in sync with a variable without wiring your own change handler by hand.
React has no equivalent sugar. React’s controlled-component idiom (value +
onValueChange, both explicit) is already the two-way binding — there is no
[(value)]/v-model shorthand to reach for, and none is planned.
This is Vue’s v-model sugar. Switch, TextInput, and Slider all support
it. Switch/TextInput come from @symbiote-native/vue; Slider ships from
the separate @symbiote-native/slider
wrapper package (@symbiote-native/slider/vue), not the adapter’s own barrel:
<script setup lang="ts">import { Switch, TextInput } from '@symbiote-native/vue';import { Slider } from '@symbiote-native/slider/vue';</script>
<template> <Switch v-model="enabled" /> <TextInput v-model="name" /> <Slider v-model="volume" :minimum-value="0" :maximum-value="1" /></template>Bare v-model="x" compiles to prop modelValue + emit update:modelValue;
named v-model:value="x" compiles to prop value + emit update:value. Each
component resolves whichever arrived and emits both, so :value/@value- change and v-model are interchangeable, never a choice you have to commit
to upfront.
This is Angular’s banana-in-a-box [(value)] two-way binding. The same three
components expose a matching value input + valueChange output, so
Angular’s [(value)] sugar works unmodified — Slider again from the
separate @symbiote-native/slider/angular package:
import { Switch, TextInput } from '@symbiote-native/angular';import { Slider } from '@symbiote-native/slider/angular';
// template: `// <Switch [(value)]="enabled" />// <TextInput [(value)]="name" />// <Slider [(value)]="volume" [minimumValue]="0" [maximumValue]="1" />// `This is not a special case wired per-component — it falls out of the RN prop
name (value) already matching Angular’s @Output() naming convention
(value + valueChange), which is exactly what [(x)] expects.
This is Svelte 5’s own $bindable() mechanism — a component opts a prop
into bind: by declaring it let { value = $bindable() } = $props().
Switch, TextInput, and Slider all declare value this way, so
bind:value works with no adapter-side plumbing: Svelte’s compiler
generates the two-way sync at the call site. Switch/TextInput come
from @symbiote-native/svelte; Slider ships from the separate
@symbiote-native/slider wrapper package
(@symbiote-native/slider/svelte), not the adapter’s own barrel:
<script lang="ts"> import { Switch, TextInput } from '@symbiote-native/svelte'; import { Slider } from '@symbiote-native/slider/svelte';
let enabled = $state(false); let name = $state(''); let volume = $state(0.5);</script>
<Switch bind:value={enabled} /><TextInput bind:value={name} /><Slider bind:value={volume} minimumValue={0} maximumValue={1} />The explicit value + onValueChange pair — Svelte’s own equivalent of
React’s controlled-component idiom — still works exactly as before and is
still the right choice when you need to see or reject a change before it
lands (the “rejecting parent” case in Why this isn’t
fragile below). One caveat specific to
$bindable(): supplying your own onValueChange on the same instance as
bind:value silently takes over — the component can’t tell whether an
instance is bound from the inside, so it only echoes the native-reported
value back into your bound variable when onValueChange is absent. Pass
one or the other, not both, on the same control.
Why this isn’t fragile
Section titled “Why this isn’t fragile”The controlled-write correction (native reports a change, you don’t update
value, SymbioteNative commands native back to the JS value) is shared logic in
@symbiote-native/components, not reimplemented per adapter or per binding style —
v-model/[(value)]/$bindable() are all sugar on top of the same contract
described in the Events guide and
the Vue API reference.