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.

react.devuseState

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:

  • useState holds the input value.
  • value={state} binds state to the input (state → view).
  • An onChange handler 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

React
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.

Angular
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():

Angular
// Childvalue = model("");
Angular
<!-- 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's value + onChange prop 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

ReactAngular
value={x} + onChange={e => setX(e.target.value)}[(ngModel)]="x"
value + onChange props on a custom componentmodel() + [(value)]
defaultValue (uncontrolled)[ngModel] (one-way)