Skip to content

Forms and controlled inputs ​

Vue v-model is sugar. React makes you write both ends of the binding - verbose at first, then predictable.

Controlled input ​

tsx
import { useState } from 'react'

function NameForm() {
  const [name, setName] = useState('')

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        console.log('submit', name)
      }}
    >
      <label htmlFor="name">Name</label>
      <input
        id="name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button type="submit">Save</button>
    </form>
  )
}

Comments for the code above:

  • value + onChange ≈ v-model
  • Always preventDefault on submit if you're handling it in JS
  • htmlFor wires the label (because for is reserved)

Checkbox / select ​

tsx
const [agree, setAgree] = useState(false)
const [color, setColor] = useState('red')

<input
  type="checkbox"
  checked={agree}
  onChange={(e) => setAgree(e.target.checked)}
/>

<select value={color} onChange={(e) => setColor(e.target.value)}>
  <option value="red">Red</option>
  <option value="blue">Blue</option>
</select>

As you can see - checked for checkboxes, value for text/select. Same controlled idea.

One form object ​

tsx
type FormState = { email: string; age: string }

const [form, setForm] = useState<FormState>({ email: '', age: '' })

function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  const { name, value } = e.target
  setForm((prev) => ({ ...prev, [name]: value }))
}

<input name="email" value={form.email} onChange={onChange} />
<input name="age" value={form.age} onChange={onChange} />

Comments for the code above:

  • name attributes drive the key update - handy for bigger forms
  • Age kept as string from the input; parse on submit if you need a number

Uncontrolled (refs) - when I use them ​

Sometimes you don't need every keystroke in state - file inputs, quick-and-dirty forms, integrating non-React widgets:

tsx
const inputRef = useRef<HTMLInputElement>(null)

function onSubmit(e: React.FormEvent) {
  e.preventDefault()
  console.log(inputRef.current?.value)
}

<input ref={inputRef} defaultValue="hello" />

defaultValue instead of value - React won't control it after mount.

For serious forms (validation, nested fields), people reach for React Hook Form / Formik. Out of scope here - but the controlled model above is what those libs still sit on top of.

v-model mental swap ​

VueReact
v-model="x"value={x} onChange={e => setX(e.target.value)}
v-model:openopen={open} onOpenChange={setOpen} (custom components)
modifiers .lazy / .numberDIY in the handler

Next: refs and the DOM - when you need an escape hatch.

Best,

-- ł.