React: Formulários e componentes controlados
Última atualização: 2026-08-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()
// I'll go when I submit it. DOM Data from Zhongna
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
function RegisterForm() {
const [form, setForm] = React.useState({
username: '',
email: '',
password: '',
role: 'developer'
})
// Each input change React Everyone knows
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Registration Data:', form) // Retrieve the complete form data
}
return (
<form onSubmit={handleSubmit}>
<input name="username" value={form.username} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" value={form.password} onChange={handleChange} />
<select name="role" value={form.role} onChange={handleChange}>
<option value="developer">Developer</option>
<option value="designer">Designer</option>
</select>
<button type="submit">Register</button>
</form>
)
}
Benefí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
function TextInputs() {
const [name, setName] = React.useState('')
const [bio, setBio] = React.useState('')
const [age, setAge] = React.useState(18)
return (
<div>
{/* Single-line text */}
<label>Name:</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Please enter your name"
/>
{/* Numbers */}
<label>Age:</label>
<input
type="number"
value={age}
onChange={e => setAge(Number(e.target.value))} // Convert to a number!
min={0} max={150}
/>
{/* Multi-line text */}
<label>Personal Profile:</label>
<textarea
value={bio}
onChange={e => setBio(e.target.value)}
rows={4}
placeholder="Introduce Yourself..."
/>
</div>
)
}
Observaçã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
function SelectInputs() {
const [city, setCity] = React.useState('')
const [hobbies, setHobbies] = React.useState([])
return (
<div>
{/* Single-selection drop-down menu */}
<label>City:</label>
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="">-- Please select --</option>
<option value="beijing">Beijing</option>
<option value="shanghai">Shanghai</option>
<option value="shenzhen">Shenzhen</option>
</select>
{/* Multiple-choice drop-down list */}
<label>Hobbies:</label>
<select
multiple
value={hobbies}
onChange={e => {
const selected = Array.from(e.target.options)
.filter(o => o.selected)
.map(o => o.value)
setHobbies(selected)
}}
>
<option value="coding">Programming</option>
<option value="reading">Read</option>
<option value="gaming">Games</option>
</select>
</div>
)
}
(3) Caixas de seleção e botões de opção
function CheckboxAndRadio() {
const [agree, setAgree] = React.useState(false)
const [gender, setGender] = React.useState('')
const [interests, setInterests] = React.useState([])
// Multiple Choice:Toggle Inclusion/Exclusion
function toggleInterest(value) {
setInterests(prev =>
prev.includes(value)
? prev.filter(i => i !== value)
: [...prev, value]
)
}
return (
<div>
{/* Single checkbox */}
<label>
<input
type="checkbox"
checked={agree}
onChange={e => setAgree(e.target.checked)} // Please note that checked No value
/>
Agree to the User Agreement
</label>
{agree && <p style={{ color: '#52c41a' }}>✅ Agreed</p>}
{/* Radio Button Group:The same one name */}
<p>Gender:</p>
{['Male', 'Female', 'Other'].map(g => (
<label key={g} style={{ marginRight: '12px' }}>
<input
type="radio"
name="gender"
value={g}
checked={gender === g}
onChange={e => setGender(e.target.value)}
/>
{g}
</label>
))}
{/* Multiple-Selection Checkbox Group */}
<p>Areas of Interest:</p>
{['Front End', 'Backend', 'AI', 'Design'].map(item => (
<label key={item} style={{ marginRight: '12px' }}>
<input
type="checkbox"
checked={interests.includes(item)}
onChange={() => toggleInterest(item)}
/>
{item}
</label>
))}
</div>
)
}
▶ Exemplo 1: Tabela de referência rápida para tipos de elementos de formulário
// ============================================
// Example:Controlled Syntax for All Common Form Elements
// ============================================
function FormElementReference() {
const [text, setText] = React.useState('')
const [number, setNumber] = React.useState(0)
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [bio, setBio] = React.useState('')
const [city, setCity] = React.useState('')
const [agree, setAgree] = React.useState(false)
const [gender, setGender] = React.useState('')
const [file, setFile] = React.useState(null)
return (
<div>
{/* Text Input */}
<input type="text" value={text} onChange={e => setText(e.target.value)} />
{/* Numeric Input */}
<input type="number" value={number} onChange={e => setNumber(Number(e.target.value))} />
{/* Enter your email address */}
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
{/* Password Entry */}
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
{/* Multi-line text */}
<textarea value={bio} onChange={e => setBio(e.target.value)} />
{/* Drop-down menu */}
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="">Please select</option>
<option value="beijing">Beijing</option>
</select>
{/* Single checkbox */}
<input type="checkbox" checked={agree} onChange={e => setAgree(e.target.checked)} />
{/* Radio button */}
<input type="radio" name="gender" value="male" checked={gender === 'male'}
onChange={e => setGender(e.target.value)} />
{/* File Upload(Uncontrolled) */}
<input type="file" onChange={e => setFile(e.target.files[0])} />
{/* Note:File input must be in uncontrolled mode. */}
</div>
)
}
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:
// ============================================
// Example:Custom useForm Hook
// Features:Unified Management of Form Status、Verification、Submit
// ============================================
function useForm(initialValues, validateFn) {
const [values, setValues] = React.useState(initialValues)
const [errors, setErrors] = React.useState({})
const [touched, setTouched] = React.useState({})
// Handling Input Changes
function handleChange(e) {
const { name, value, type, checked } = e.target
const newValue = type === 'checkbox' ? checked : value
const newValues = { ...values, [name]: newValue }
setValues(newValues)
// Real-Time Verification
if (validateFn) {
const newErrors = validateFn(newValues)
setErrors(newErrors)
}
}
// Handling Loss of Focus
function handleBlur(e) {
const { name } = e.target
setTouched({ ...touched, [name]: true })
}
// Process Submission
function handleSubmit(callback) {
return (e) => {
e.preventDefault()
// Mark all fields as checked
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true
return acc
}, {})
setTouched(allTouched)
// Verification
if (validateFn) {
const newErrors = validateFn(values)
setErrors(newErrors)
if (Object.keys(newErrors).length > 0) return
}
callback(values)
}
}
// Reset Form
function reset() {
setValues(initialValues)
setErrors({})
setTouched({})
}
return { values, errors, touched, handleChange, handleBlur, handleSubmit, reset }
}
// ---- Usage useForm ----
function LoginForm() {
function validate(values) {
const errors = {}
if (!values.email) errors.email = 'Please enter your email address'
else if (!values.email.includes('@')) errors.email = 'The email address format is incorrect.'
if (!values.password) errors.password = 'Please enter your password'
else if (values.password.length < 6) errors.password = 'Password must be at least 6 chars'
return errors
}
const form = useForm({ email: '', password: '' }, validate)
return (
<form onSubmit={form.handleSubmit(data => console.log('Submit:', data))}
style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>Log In</h2>
<input
name="email"
value={form.values.email}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Email"
style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.email && form.errors.email ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
/>
{form.touched.email && form.errors.email && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.email}</p>
)}
<input
name="password"
type="password"
value={form.values.password}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Password"
style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.password && form.errors.password ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
/>
{form.touched.password && form.errors.password && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.password}</p>
)}
<button type="submit" style={{
width: '100%', padding: '10px', backgroundColor: '#1890ff',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Log In
</button>
<button type="button" onClick={form.reset}
style={{ width: '100%', padding: '8px', marginTop: '8px', cursor: 'pointer' }}>
Reset
</button>
</form>
)
}
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'
}
▶ Exemplo 2: Vinculação de dados de formulário — Preenchimento automático de códigos de área ao selecionar um país
function PhoneForm() {
const [form, setForm] = useState({ country: 'US', phone: '' })
const areaCodes = { US: '+1', CN: '+86', JP: '+81', UK: '+44', BR: '+55' }
function handleCountryChange(e) {
const country = e.target.value
setForm(prev => ({
...prev,
country,
phone: prev.phone.replace(/^\+\d+\s*/, '') + ' ',
}))
}
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Phone Contact</h3>
<select name="country" value={form.country} onChange={handleCountryChange}
style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
{Object.keys(areaCodes).map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
<div style={{ display: 'flex', gap: 8 }}>
<input value={areaCodes[form.country]} readOnly style={{ width: 60, padding: 8, borderRadius: 4 }} />
<input name="phone" value={form.phone} onChange={handleChange}
placeholder="Phone number" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
</div>
<p style={{ color: '#666', fontSize: 13, marginTop: 8 }}>
Full: {areaCodes[form.country]} {form.phone.trim()}
</p>
</div>
)
}
▶ Exemplo 3: Formulário dinâmico — Adicionar/remover membros da equipe
function TeamForm() {
const [members, setMembers] = useState([{ name: '', email: '', role: 'developer' }])
function addMember() {
setMembers(prev => [...prev, { name: '', email: '', role: 'developer' }])
}
function removeMember(index) {
setMembers(prev => prev.filter((_, i) => i !== index))
}
function updateMember(index, field, value) {
setMembers(prev => prev.map((m, i) => i === index ? { ...m, [field]: value } : m))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Team:', members)
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 500, margin: '0 auto' }}>
<h3>Team Members</h3>
{members.map((m, i) => (
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
<input value={m.name} onChange={e => updateMember(i, 'name', e.target.value)}
placeholder="Name" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
<input value={m.email} onChange={e => updateMember(i, 'email', e.target.value)}
placeholder="Email" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
<select value={m.role} onChange={e => updateMember(i, 'role', e.target.value)}
style={{ padding: 6, borderRadius: 4 }}>
<option value="developer">Dev</option>
<option value="designer">Design</option>
<option value="manager">PM</option>
</select>
{members.length > 1 && (
<button type="button" onClick={() => removeMember(i)}
style={{ padding: '6px 10px', color: '#ff4d4f', border: 'none', cursor: 'pointer' }}>
X
</button>
)}
</div>
))}
<button type="button" onClick={addMember}
style={{ padding: '6px 16px', marginRight: 8, cursor: 'pointer' }}>
+ Add Member
</button>
<button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Submit</button>
</form>
)
}
▶ Exemplo 4: Assistente de formulário em várias etapas
function MultiStepForm() {
const [step, setStep] = useState(1)
const [form, setForm] = useState({ name: '', email: '', password: '', city: '', agree: false })
const [errors, setErrors] = useState({})
function updateField(e) {
const { name, value, type, checked } = e.target
setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
}
function validate() {
const errs = {}
if (step === 1) {
if (!form.name.trim()) errs.name = 'Name required'
if (!form.email.includes('@')) errs.email = 'Valid email required'
} else if (step === 2) {
if (form.password.length < 6) errs.password = 'At least 6 characters'
} else if (step === 3) {
if (!form.city) errs.city = 'Select a city'
if (!form.agree) errs.agree = 'Must agree to terms'
}
setErrors(errs)
return Object.keys(errs).length === 0
}
function next() { if (validate()) setStep(s => Math.min(s + 1, 3)) }
function back() { setStep(s => Math.max(s - 1, 1)) }
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Step {step} of 3</h3>
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
{[1, 2, 3].map(s => (
<div key={s} style={{ flex: 1, height: 4, borderRadius: 2,
backgroundColor: s <= step ? '#1890ff' : '#f0f0f0' }} />
))}
</div>
{step === 1 && (
<>
<input name="name" value={form.name} onChange={updateField} placeholder="Name"
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.name ? '1px solid #ff4d4f' : undefined }} />
{errors.name && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.name}</p>}
<input name="email" value={form.email} onChange={updateField} placeholder="Email"
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.email ? '1px solid #ff4d4f' : undefined }} />
{errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.email}</p>}
</>
)}
{step === 2 && (
<>
<input name="password" type="password" value={form.password} onChange={updateField}
placeholder="Password" style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }} />
{errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.password}</p>}
</>
)}
{step === 3 && (
<>
<select name="city" value={form.city} onChange={updateField}
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }}>
<option value="">Select city</option>
<option value="beijing">Beijing</option>
<option value="shanghai">Shanghai</option>
</select>
{errors.city && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.city}</p>}
<label style={{ display: 'block', marginTop: 8 }}>
<input type="checkbox" name="agree" checked={form.agree} onChange={updateField} />
I agree to the terms
</label>
{errors.agree && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.agree}</p>}
</>
)}
<div style={{ marginTop: 16 }}>
{step > 1 && <button type="button" onClick={back} style={{ marginRight: 8, cursor: 'pointer' }}>Back</button>}
{step < 3 ? (
<button type="button" onClick={next} style={{ cursor: 'pointer' }}>Next</button>
) : (
<button type="button" onClick={() => { if (validate()) alert('Done!') }} style={{ cursor: 'pointer' }}>Submit</button>
)}
</div>
</div>
)
}
▶ 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>
)
}
❓ Perguntas Frequentes
P: Por que não consigo digitar normalmente no campo de entrada de um componente controlado? R: Um erro comum é esquecer de vincular o evento
onChangeou definir ovaluecom um valor fixo. Por exemplo,<input value={name} />não possui um eventoonChange; quando o usuário digita, o React imediatamente redefinevaluepara o valor anterior dename, de modo que o campo de entrada “não permite que você digite”. Solução: certifique-se de queonChangeatualize corretamente o estado.
P: Para
input[type="checkbox"], devo usarcheckedouvalue? R: Para caixas de seleção, usecheckedattribute (boolean), notvalue. To retrieve the new value in the correspondingonChangeevent, usee.target.checked. Similarly, for radio buttons, usechecked.
P: Quando a validação do formulário deve ser realizada? R: Cada uma das três opções de momento tem uma finalidade diferente: ① Validação à medida que o usuário digita (onChange) — fornece feedback em tempo real, mas é acionada com frequência; ② Validação quando o campo perde o foco (onBlur) — minimiza a interrupção, exibindo erros somente depois que o usuário terminar de preencher o formulário; ③ Validação no momento do envio (onSubmit) — serve como verificação final. Recomendamos usar uma combinação de onBlur e onSubmit, com onChange adicionado a campos críticos, como o nome de usuário, para feedback em tempo real.
P: Qual é a abordagem geral para lidar com
handleChangequando um formulário tem muitos campos? R: Usee.target.nameem 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 mesmohandleChange. As caixas de seleção exigem um tratamento especial:const newValue = type === 'checkbox' ? checked : value.
P: Como decidir entre componentes controlados e não controlados? R: Uma regra simples: se for necessária validação em tempo real, formatação ou exibição condicional → use um componente controlado (por exemplo, avisos sobre a força da senha em tempo real, formatação automática de valores). Se você estiver preocupado apenas com o valor final ou com a leitura do valor somente após o envio → use um componente não controlado (por exemplo, uploads de arquivos, formulários de contato simples).
<input type="file">Você só pode usar componentes não controlados porque seuvalueé somente de leitura.useFormOs 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.