React: Formulários e componentes controlados
Última atualização: 2026-10-26
Os formulários funcionam como uma ponte entre os aplicativos web e os usuários. No React, os formulários não se resumem mais apenas a “preenchê-los e enviá-los” — em vez disso, “o React reconhece cada caractere que você digita”. Esse padrão é chamado de componente controlado.
1. O que você vai aprender
- Componentes controlados x componentes não controlados
- Abordagem controlada para os elementos
input,textarea,select,checkboxeradio - Processo de envio de formulários
- Validação básica de formulários
- Ganchos de formulário personalizados
2. A evolução de um formulário de inscrição
(1) Desafio: Dificuldade em acompanhar dados provenientes de formulários não controlados
Alice criou um formulário de cadastro de usuário que recupera dados usando métodos nativos:
<!-- HTML Form:Read data all at once upon submission -->
<form id="register-form">
<input name="username" id="username" />
<input type="email" name="email" id="email" />
<input type="password" name="password" id="password" />
<select name="role" id="role">
<option value="developer">Developer</option>
<option value="designer">Designer</option>
</select>
<button type="submit">Register</button>
</form>
<script>
document.getElementById('register-form').addEventListener('submit', function(e) {
e.preventDefault()
// Ao enviar, leia os valores do formulário
const username = document.getElementById('username').value
const email = document.getElementById('email').value
const password = document.getElementById('password').value
const role = document.getElementById('role').value
// ... Manually verify each field
})
</script>
Pergunta:
- Falta de transparência: O que está sendo inserido? O valor mediano? O React não sabe.
- Dificuldade com a validação em tempo real: é preciso monitorar os eventos de entrada e implementar manualmente a lógica de validação.
- Problema com a vinculação de dados de formulário: Recursos como “preenchimento automático do código de área após a seleção de um país” precisam ser programados manualmente
(2) Soluções para componentes controlados pelo React
▶ ExperimenteBenefícios: O React monitora cada campo de entrada em tempo real, oferecendo validação e sincronização de dados em tempo real, e recupera os dados completos imediatamente após o envio.
3. Componentes controlados x componentes não controlados
graph TB
subgraph "Controlled Components(Recommendations)"
A1[User Input] --> B1[onChange Event]
B1 --> C1[Update State]
C1 --> D1[React Render Again]
D1 --> E1[value = State]
end
subgraph "Uncontrolled Components"
A2[User Input] --> B2[Update Directly DOM]
B2 --> C2[Use when submitting ref Reading]
end
style C1 fill:#52c41a,color:#fff
style B2 fill:#faad14,color:#000
| Dimensão | Componentes controlados | Componentes não controlados |
|---|---|---|
| Fonte de dados | Estado do React (única fonte de verdade) | O próprio DOM |
| Método de acesso | Leitura direta do Estado | Requer uma referência ou document.getElementById |
| Validação em tempo real | ✅ Suporte nativo | ❌ Requer monitoramento adicional |
| Sincronização instantânea | ✅ Sincronização de dados em tempo real | ❌ É necessária sincronização manual |
| Casos de uso | A maioria dos formulários | Envio de arquivos, formulários simples de uso único |
Regra: Use componentes controlados em 90% dos casos. Apenas o arquivo
<input type="file">deve usar um componente não controlado.
4. Sintaxe controlada para diversos elementos de formulário
| Elemento do formulário | Propriedade vinculada | Valor de onChange | Exemplo |
|---|---|---|---|
<input type="text"> |
value |
e.target.value |
<input value={name} onChange={e => setName(e.target.value)} /> |
<input type="number"> |
value |
Number(e.target.value) |
Deve ser convertido manualmente para valores numéricos |
<input type="checkbox"> |
checked |
e.target.checked |
Booleano, valor “não” |
<input type="radio"> |
checked |
e.target.value |
Agrupados por nome; a opção “marcado” indica se estão selecionados |
<textarea> |
value |
e.target.value |
O React usa o valor, não o texto do elemento filho |
<select> |
value |
e.target.value |
Seleções múltiplas multiple + matriz |
<input type="file"> |
❌ Sem controle | e.target.files[0] |
Só pode ser usado sem controle |
(1) Entrada de texto
▶ ExperimenteObservação: No React, use a propriedade
<textarea>em vez do conteúdo de texto do elemento filho. O valor dee.target.valuepara<input type="number">é uma string; certifique-se de convertê-lo em um número.
(2) Caixa de seleção
▶ Experimente(3) Caixas de seleção e botões de opção
▶ ExperimenteSaída:
Formulário com campos de entrada e tratamento de envio. Componente React com tipagem TypeScript usando interface para as props
▶ Exemplo 1: Tabela de referência rápida para tipos de elementos de formulário
▶ ExperimenteSaída:
Referência: text (value/onChange), number (conversão Number()), email, password, textarea (value), select (value), checkbox (checked), radio (checked), file (não controlado, files[0])
5. Validação de formulários
| Momento da verificação | Evento desencadeador | Vantagens | Desvantagens | Cenários recomendados |
|---|---|---|---|---|
| Durante a digitação | onChange | Feedback em tempo real; os usuários podem corrigir erros imediatamente | Acionado com frequência; um erro é sinalizado antes que o usuário termine de digitar | Força da senha, validação do nome de usuário |
| Quando fora de foco | onBlur | Minimizar distrações; validar somente após a conclusão | Os usuários veem os erros somente depois de saírem do campo | A maioria dos campos de formulário |
| Ao enviar | onSubmit | Revisão final e validação unificada | O usuário só percebe um erro depois de preencher o formulário | Validação de fallback final |
(1) Verificação em tempo real
function ValidatedForm() {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState({})
const [touched, setTouched] = React.useState({}) // Record which fields have been accessed
function validate(fieldName, value) {
const newErrors = { ...errors }
if (fieldName === 'email') {
if (!value) newErrors.email = 'The email address cannot be left blank.'
else if (!value.includes('@')) newErrors.email = 'The email address format is incorrect.'
else delete newErrors.email
}
if (fieldName === 'password') {
if (!value) newErrors.password = 'The password cannot be empty.'
else if (value.length < 6) newErrors.password = 'Password must be at least 6 chars'
else if (!/[A-Z]/.test(value)) newErrors.password = 'Passwords must contain uppercase letters'
else delete newErrors.password
}
setErrors(newErrors)
}
function handleBlur(e) {
const { name, value } = e.target
setTouched({ ...touched, [name]: true })
validate(name, value)
}
function handleSubmit(e) {
e.preventDefault()
// Validate all fields before submitting
const allErrors = {}
if (!email || !email.includes('@')) allErrors.email = 'Please enter a valid email address'
if (!password || password.length < 6) allErrors.password = 'Password must be at least 6 chars'
if (Object.keys(allErrors).length > 0) {
setErrors(allErrors)
setTouched({ email: true, password: true })
return
}
alert('Submission Successful!')
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
<div style={{ marginBottom: '16px' }}>
<label>Email:</label>
<input
name="email"
type="email"
value={email}
onChange={e => { setEmail(e.target.value); validate('email', e.target.value) }}
onBlur={handleBlur}
style={inputStyle(touched.email && errors.email)}
/>
{touched.email && errors.email && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.email}</p>
)}
</div>
<div style={{ marginBottom: '16px' }}>
<label>Password:</label>
<input
name="password"
type="password"
value={password}
onChange={e => { setPassword(e.target.value); validate('password', e.target.value) }}
onBlur={handleBlur}
style={inputStyle(touched.password && errors.password)}
/>
{touched.password && errors.password && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.password}</p>
)}
</div>
{/* Password Strength Indicator */}
{password && (
<div style={{ marginBottom: '16px' }}>
<p style={{ fontSize: '12px', color: '#666' }}>
Password Strength:
<span style={{ color: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a' }}>
{password.length < 6 ? 'Weak' : password.length < 10 ? 'Medium' : 'Strong'}
</span>
</p>
<div style={{ height: '4px', backgroundColor: '#f0f0f0', borderRadius: '2px' }}>
<div style={{
height: '100%',
width: `${Math.min(100, (password.length / 12) * 100)}%`,
backgroundColor: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a',
borderRadius: '2px',
transition: 'width 0.3s'
}} />
</div>
</div>
)}
<button type="submit" disabled={Object.keys(errors).length > 0}
style={{
width: '100%', padding: '10px',
backgroundColor: Object.keys(errors).length > 0 ? '#d9d9d9' : '#1890ff',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Register
</button>
</form>
)
}
function inputStyle(hasError) {
return {
width: '100%', padding: '8px',
border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
borderRadius: '4px',
outline: 'none'
}
}
(2) Personalizar o gancho useForm
Extraia a lógica do formulário para um hook personalizado, a fim de facilitar sua reutilização:
▶ Expandir exemplo executável (91 linhas)
6. Exemplo completo: Formulário de cadastro do usuário
// ============================================
// Complete Example:User Registration Form
// Features:Controlled Components + Real-Time Verification + Form Data Synchronization
// ============================================
function RegistrationForm() {
const [form, setForm] = React.useState({
username: '',
email: '',
password: '',
confirmPassword: '',
country: '',
gender: '',
acceptTerms: false
})
const [errors, setErrors] = React.useState({})
function validate() {
const newErrors = {}
if (!form.username || form.username.length < 2)
newErrors.username = 'Username must be at least 2 characters'
if (!form.email || !form.email.includes('@'))
newErrors.email = 'Please enter a valid email address'
if (!form.password || form.password.length < 6)
newErrors.password = 'Password must be at least 6 chars'
if (form.password !== form.confirmPassword)
newErrors.confirmPassword = 'The two passwords do not match'
if (!form.country)
newErrors.country = 'Please select a country'
if (!form.acceptTerms)
newErrors.acceptTerms = 'Please agree to the Terms of Service'
return newErrors
}
function handleChange(e) {
const { name, value, type, checked } = e.target
const newValue = type === 'checkbox' ? checked : value
const newForm = { ...form, [name]: newValue }
setForm(newForm)
setErrors(validate())
// Interconnection:Automatically adjust password rule prompts after selecting a country
}
function handleSubmit(e) {
e.preventDefault()
const finalErrors = validate()
setErrors(finalErrors)
if (Object.keys(finalErrors).length === 0) {
alert(`Registration Successful!Welcome ${form.username}`)
}
}
function inputStyle(fieldName) {
return {
width: '100%', padding: '8px', marginBottom: '4px',
border: `1px solid ${errors[fieldName] ? '#ff4d4f' : '#d9d9d9'}`,
borderRadius: '4px'
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>📝 User Registration</h2>
<input name="username" value={form.username} onChange={handleChange}
placeholder="Username" style={inputStyle('username')} />
{errors.username && <p style={errorStyle}>{errors.username}</p>}
<input name="email" type="email" value={form.email} onChange={handleChange}
placeholder="Email" style={inputStyle('email')} />
{errors.email && <p style={errorStyle}>{errors.email}</p>}
<input name="password" type="password" value={form.password} onChange={handleChange}
placeholder="Password (at least 6 chars)" style={inputStyle('password')} />
{errors.password && <p style={errorStyle}>{errors.password}</p>}
<input name="confirmPassword" type="password" value={form.confirmPassword}
onChange={handleChange} placeholder="Confirm Password" style={inputStyle('confirmPassword')} />
{errors.confirmPassword && <p style={errorStyle}>{errors.confirmPassword}</p>}
<select name="country" value={form.country} onChange={handleChange}
style={inputStyle('country')}>
<option value="">-- Select a Country --</option>
<option value="CN">China</option>
<option value="US">United States</option>
<option value="JP">Japan</option>
<option value="BR">Brazil</option>
<option value="SA">Saudi Arabia</option>
</select>
{errors.country && <p style={errorStyle}>{errors.country}</p>}
<div style={{ margin: '12px 0' }}>
<p>Gender:</p>
{['Male', 'Female', 'Other'].map(g => (
<label key={g} style={{ marginRight: '16px' }}>
<input type="radio" name="gender" value={g}
checked={form.gender === g} onChange={handleChange} />
{g}
</label>
))}
</div>
<label style={{ display: 'block', margin: '12px 0' }}>
<input type="checkbox" name="acceptTerms" checked={form.acceptTerms}
onChange={handleChange} />
I have read and agree to <a href="#">User Agreement</a>
</label>
{errors.acceptTerms && <p style={errorStyle}>{errors.acceptTerms}</p>}
<button type="submit" style={{
width: '100%', padding: '10px',
backgroundColor: Object.keys(errors).length === 0 && form.username ? '#1890ff' : '#d9d9d9',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Register
</button>
</form>
)
}
const errorStyle = {
color: '#ff4d4f', fontSize: '12px',
margin: '0 0 8px 0'
}
Saída:
Exibe: "Please select". Tipos de entrada: text, email, number. Menu suspenso: Please select, Beijing
▶ Exemplo 2: Vinculação de dados de formulário — Preenchimento automático de códigos de área ao selecionar um país
▶ ExperimenteSaída:
Seletor de país (US/CN/JP/UK/BR) → o código de área é preenchido automaticamente (US: +1, CN: +86). Campo de telefone + código de área somente leitura. Exibe "Full: +1 5551234"
Saída:
Exibe: "Phone Contact". Estado: form (setter: setForm). Entrada: Phone number. Menu suspenso: {c}
▶ Exemplo 3: Formulário dinâmico — Adicionar/remover membros da equipe
▶ ExperimenteSaída:
Linhas dinâmicas de membros (Name + Email + menu suspenso de Role). "+ Add Member" adiciona uma linha, "X" remove uma linha (mantém pelo menos 1). Envio → console.log("Team:", members)
Saída:
... ('Team:', members)
▶ Exemplo 4: Assistente de formulário em várias etapas
▶ Expandir exemplo executável (77 linhas)
Saída:
Assistente de 3 etapas com barra de progresso. Etapa 1: Name+Email, etapa 2: Password (6+ caracteres), etapa 3: City+Terms. Avançar valida, Voltar retorna. Envio → "Done!"
Saída:
Exibe "file upload (uncontrolled)". Estado: preview, filename. Botão: upload. Usa useRef
▶ Exemplo 5: Componentes não gerenciados — Upload de arquivos e useRef
function FileUploadForm() {
const fileInputRef = useRef(null)
const [preview, setPreview] = useState(null)
const [fileName, setFileName] = useState('')
function handleSubmit(e) {
e.preventDefault()
const file = fileInputRef.current?.files[0]
if (!file) { alert('Please select a file'); return }
console.log('Uploading:', file.name, file.size, 'bytes')
alert(`File "${file.name}" (${(file.size / 1024).toFixed(1)} KB) selected for upload`)
}
function handleFileChange(e) {
const file = e.target.files[0]
if (!file) { setPreview(null); setFileName(''); return }
setFileName(file.name)
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => setPreview(e.target.result)
reader.readAsDataURL(file)
} else {
setPreview(null)
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>File Upload (Uncontrolled)</h3>
<div style={{ marginBottom: 12 }}>
<input type="file" ref={fileInputRef} onChange={handleFileChange}
accept="image/*,.pdf,.doc" style={{ marginBottom: 8 }} />
{fileName && <p style={{ fontSize: 13, color: '#666' }}>Selected: {fileName}</p>}
</div>
{preview && (
<div style={{ marginBottom: 12 }}>
<img src={preview} alt="Preview" style={{ maxWidth: '100%', maxHeight: 200, borderRadius: 4, border: '1px solid #eee' }} />
</div>
)}
<button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Upload</button>
</form>
)
}
Saída:
Entrada de arquivo → exibe a prévia da imagem, ou exibe o nome do arquivo. No upload, alert "File "photo.jpg" (245.3 KB) selected for upload"
❓ Perguntas Frequentes
input[type="checkbox"], devo usar checked ou value?checked attribute (boolean), not value. To retrieve the new value in the corresponding onChange event, use e.target.checked. Similarly, for radio buttons, use checked.handleChange quando um formulário tem muitos campos?e.target.name em combinação com o nome da propriedade calculada: const { name, value } = e.target; setForm(prev => ({...prev, [name]: value})). Dessa forma, todos os <input name="xxx"> podem usar o mesmo handleChange. As caixas de seleção exigem um tratamento especial: const newValue = type === 'checkbox' ? checked : value.<input type="file"> Você só pode usar componentes não controlados porque seu value é somente de leitura. useForm Os hooks podem encapsular código padrão para componentes controlados.📖 Resumo
- Componentes controlados: Os valores dos elementos do formulário são controlados pelo estado do React e atualizados por meio de
onChange - 90% dos formulários utilizam componentes controlados; apenas
<input type="file">deve utilizar um componente não controlado - Três momentos para a validação do formulário: onChange (em tempo real), onBlur (quando o foco é perdido) e onSubmit (no momento do envio)
- Reutilizar a lógica de processamento de formulários usando as propriedades genéricas
handleChange+name - Um hook personalizado
useFormpode encapsular a lógica do formulário para reutilização
📝 Exercícios
- Exercício básico (Dificuldade ⭐): Crie um componente
FeedbackFormque inclua nome, e-mail, avaliação (opções em menu suspenso: Positiva/Neutra/Negativa) e conteúdo (área de texto). Ao enviar, exiba todos os dados em um alerta. - Problema avançado (Dificuldade ⭐⭐): Crie um componente
PasswordStrengthMeterque exiba a força da senha (fraca/média/forte) em tempo real. Regras: ≥8 caracteres + letras maiúsculas e minúsculas + números = forte; ≥6 caracteres = média; caso contrário = fraca. - Desafio (Dificuldade: ⭐⭐⭐): Crie um componente
DynamicFormque permita aos usuários adicionar ou remover dinamicamente itens do formulário (por exemplo, “Adicionar vários membros da família”). Cada item do formulário deve incluir três campos: nome, idade e relação. Após o envio, o componente deve gerar um array.