Skip to content

Two-Way Binding

Two-way binding connects a state variable to a UI control so that changes in either direction are reflected in the other. Modify the variable, the input updates. Type in the input, the variable updates. The binding handles both directions under a single declaration.

Angular’s [(ngModel)] is the canonical example. The [()] syntax, sometimes called “banana in a box,” desugars into a property binding and an event binding:

<!-- Angular two-way binding -->
<input [(ngModel)]="name" />
<!-- Desugars to -->
<input [value]="name" (ngModelChange)="name = $event" />

Vue’s v-model works the same way. On text inputs it binds value and listens for input. On checkboxes it binds checked and listens for change.

<!-- Vue two-way binding -->
<input v-model="name" />
<!-- Custom component -->
<CustomInput v-model="search" />

Vue 3 lets components accept v-model by receiving modelValue as a prop and emitting update:modelValue.

CustomInput.vue
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>

React does not have a built-in two-way binding directive. You write the same behavior manually:

function NameInput() {
const [name, setName] = useState('')
return (
<input
value={name}
onChange={e => setName(e.target.value)}
/>
)
}

Some React patterns compress this into something that looks like two-way binding. The useSyncExternalStore hook, for instance, lets a component subscribe to external state and update it — the external store plus the component form a two-way channel. Form libraries like React Hook Form use refs and uncontrolled inputs to avoid the pattern entirely.

Forms and form-like UI controls are the natural fit. A text input, a checkbox, a slider — these are symmetrical by nature: the user sees the current value and can immediately change it. Two-way binding reduces the code you write for these cases by half compared to manual event handling.

Angular’s FormsModule and Vue’s v-model make form-heavy applications shorter to write. The trade-off is that the data flow is less explicit — you cannot look at the template and immediately tell which direction a change will travel because the binding does both.

Two-way bindings create circular update paths when combined with computed or derived values. If the bound variable goes through a transformation (uppercasing, filtering), the transformed value written back can trigger another transformation. Debugging these loops is harder than debugging unidirectional chains because each mutation looks like it came from the other bound party.


References