In the controlled-input pattern, an input's displayed value is dictated entirely by a useState value, and its onChange handler reads the new value from event.target.value and passes it to the setter, keeping the state and the input's displayed content perfectly synchronized on every keystroke. This gives the component full, immediate access to the current input value at any point, useful for showing live character counts, enabling/disabling a submit button based on the current input, or performing real-time validation, none of which are as straightforward with an uncontrolled input relying only on the DOM's own internal value.
1Understanding useState in Forms
In the controlled-input pattern, an input's displayed value is dictated entirely by a useState value, and its onChange handler reads the new value from event.target.value and passes it to the setter, keeping the state and the input's displayed content perfectly synchronized on every keystroke. This gives the component full, immediate access to the current input value at any point, useful for showing live character counts, enabling/disabling a submit button based on the current input, or performing real-time validation, none of which are as straightforward with an uncontrolled input relying only on the DOM's own internal value.
A controlled text input's initial useState value should be an empty string, '', not undefined — starting undefined and switching to a string later triggers a React warning about a component changing from an uncontrolled to a controlled input.
import { useState } from 'react';
function CommentBox() {
const [comment, setComment] = useState('');
return (
<>
<textarea value={comment} onChange={(e) => setComment(e.target.value)} />
<p>{comment.length}/280 characters</p>
</>
);
}2Practical Example
Here is a real-world application of useState in Forms showing how it is used in production React code.
import { useState } from 'react';
function SubmitForm() {
const [name, setName] = useState('');
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button disabled={name.trim() === ''}>Submit</button>
</>
);
}3Best Practices
Follow these guidelines when working with useState in Forms:
1. Initialize a controlled input's state with an appropriate empty default, like '' for text or false for a checkbox, never undefined, to avoid a React warning about switching between controlled and uncontrolled
2. Read the new value from event.target.value, or event.target.checked for checkboxes, inside onChange, passing it directly to the state setter
3. Use the current state value directly for real-time UI feedback, like a live character count or a submit button that's disabled until required fields are filled
Tip: A controlled text input's initial useState value should be an empty string, '', not undefined — starting undefined and switching to a string later triggers a React warning about a component changing from an uncontrolled to a controlled input.
import { useState } from 'react';
function CommentBox() {
const [comment, setComment] = useState('');
return (
<>
<textarea value={comment} onChange={(e) => setComment(e.target.value)} />
<p>{comment.length}/280 characters</p>
</>
);
}