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



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
<!-- 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:

(2) Soluções para componentes controlados pelo React

▶ Experimente

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

100%
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

▶ Experimente

Observação: No React, use a propriedade <textarea> em vez do conteúdo de texto do elemento filho. O valor de e.target.value para <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

▶ Experimente

Saída:

TEXT
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

▶ Experimente

Saída:

TEXT
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

JSX
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'
  }
}
▶ Experimente

(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)
▶ Experimente

6. Exemplo completo: Formulário de cadastro do usuário

JSX
// ============================================
// 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'
}
▶ Experimente

Saída:

TEXT
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

▶ Experimente

Saída:

TEXT
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:

TEXT
Exibe: "Phone Contact". Estado: form (setter: setForm). Entrada: Phone number. Menu suspenso: {c}

▶ Exemplo 3: Formulário dinâmico — Adicionar/remover membros da equipe

▶ Experimente

Saída:

TEXT
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:

TEXT
... ('Team:', members)

▶ Exemplo 4: Assistente de formulário em várias etapas

▶ Expandir exemplo executável (77 linhas)
▶ Experimente

Saída:

TEXT
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:

TEXT
Exibe "file upload (uncontrolled)". Estado: preview, filename. Botão: upload. Usa useRef

▶ Exemplo 5: Componentes não gerenciados — Upload de arquivos e useRef

JSX
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>
  )
}
▶ Experimente

Saída:

TEXT
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

P Para input[type="checkbox"], devo usar checked ou value?
R Para caixas de seleção, use 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.
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 handleChange quando um formulário tem muitos campos?
R Use 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.
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 seu value é somente de leitura. useForm Os hooks podem encapsular código padrão para componentes controlados.

📖 Resumo


📝 Exercícios

  1. Exercício básico (Dificuldade ⭐): Crie um componente FeedbackForm que 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.
  2. Problema avançado (Dificuldade ⭐⭐): Crie um componente PasswordStrengthMeter que 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.
  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um componente DynamicForm que 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.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%