React Essentials — Components, JSX, Props, State & MoreEntry 10 of 11
Two-Way Binding (One-Way Binding + useState + onChange)
Keeping an input's displayed value and a state variable in sync in both directions.
Notes
React has no native two-way binding. It achieves two-way behavior by combining one-way binding with state, known as a controlled input:
useStateholds the input value.value={state}binds state to the input (state → view).- An
onChangehandler writes user input back to state (view → state).
Setting value without onChange makes the input read-only, and React will warn. If you only need an initial value and don't want to control the input, use defaultValue.
What / Why / When
- What
Controlled inputs.
- Why
State stays the single source of truth, which makes validation, formatting, and resetting easy.
- When
Forms, inline editing, search boxes.
- Use case
A player name that can be edited in place.
Code compare
import { useState } from "react";export function Player({ initialName }: { initialName: string }) { const [playerName, setPlayerName] = useState(initialName); const [isEditing, setIsEditing] = useState(false); return ( <li> {isEditing ? ( <input type="text" required value={playerName} // state → view onChange={(e) => setPlayerName(e.target.value)} // view → state /> ) : ( <span>{playerName}</span> )} <button onClick={() => setIsEditing((editing) => !editing)}> {isEditing ? "Save" : "Edit"} </button> </li> );}Angular provides native two-way binding with [(ngModel)], which binds the value and updates the variable on change. Import FormsModule in the standalone component's imports.
import { Component, input, linkedSignal, signal } from "@angular/core";import { FormsModule } from "@angular/forms";@Component({ selector: "app-player", imports: [FormsModule], template: ` <li> @if (isEditing()) { <input type="text" required [(ngModel)]="playerName" /> } @else { <span>{{ playerName() }}</span> } <button (click)="isEditing.update((editing) => !editing)"> {{ isEditing() ? "Save" : "Edit" }} </button> </li> `,})export class PlayerComponent { initialName = input.required<string>(); playerName = linkedSignal(() => this.initialName()); // writable, seeded from the input isEditing = signal(false);}For two-way binding between your own components, use model():
// Childvalue = model("");<!-- Parent --><app-name-field [(value)]="playerName" />Differences and gotchas
[(ngModel)]binds directly to a writable signal (v17.2+).model()is the true Angular counterpart of React'svalue+onChangeprop pair. It bundles an input and an output into one two-way binding.linkedSignal()(v19+) is a writable signal that resets when its source changes, useful for editable copies of inputs.
Quick reference
| React | Angular |
|---|---|
value={x} + onChange={e => setX(e.target.value)} | [(ngModel)]="x" |
value + onChange props on a custom component | model() + [(value)] |
defaultValue (uncontrolled) | [ngModel] (one-way) |