Skip to content
This page has been auto-translated and may contain errors.View in English

Formulários

Quando você digita em um input HTML simples, o navegador cuida do valor por conta própria: armazena o que você digitou e redesenha o cursor sem nenhum código seu. Conecte o value desse input a um pedaço de state e atualize esse state no onChange, e o input começa a funcionar como tudo mais em React: o state é a fonte da verdade sobre o que aparece na página, e o input reflete isso. Isso é um input controlado, aquele onde React decide o que é mostrado a cada momento.

Aqui está um pequeno formulário com um input de texto:

jsx
function NameForm() {
  const [text, setText] = useState('')

  function handleSubmit(event) {
    event.preventDefault()
    console.log(text)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button>Submit</button>
    </form>
  )
}

O value do input vem do state text, e cada digitação dispara onChange, que chama setText com o novo valor. O state e o input ficam em sincronia porque React re-renderiza o input com o que text contém no momento. Enviar um formulário recarrega a página por padrão, então handleSubmit chama event.preventDefault() primeiro, depois faz o que o aplicativo precisa, aqui registrando o texto em console, enviando para um servidor em outro lugar.

Inputs controlados e não controlados

O input acima é um input controlado, porque seu valor vive no state do React e React decide o que ele exibe. Dê a ele um value sem um onChange, e o input fica somente para leitura: React continua pintando o mesmo valor em cada renderização, e nada que você digita nunca chega ao state, então emparelhe os dois sempre que um campo for controlado. Um input também pode ser não controlado: o DOM acompanha seu próprio valor, e você o lê apenas quando precisa, através de uma ref para um campo único (uma forma de alcançar um elemento DOM diretamente, coberta completamente em Refs e o DOM) ou através do FormData do formulário para um formulário inteiro no momento do envio. Inputs controlados dão ao componente o valor atual a cada digitação, que é o que validação em tempo real, contagem de caracteres ou campos dos quais outras partes da UI dependem necessitam. Inputs não controlados pulam uma renderização por digitação e alcançam o valor apenas no momento do envio, que é menos código para um campo simples que nada mais precisa acompanhar.

Checkboxes

Checkboxes seguem o mesmo padrão, mas seu state vive em checked em vez de value:

jsx
function Newsletter() {
  const [subscribed, setSubscribed] = useState(false)

  return (
    <label>
      <input
        type="checkbox"
        checked={subscribed}
        onChange={e => setSubscribed(e.target.checked)}
      />
      Subscribe to updates
    </label>
  )
}

e.target.checked é um booleano, e subscribed determina se a caixa está marcada: uma prop, um change handler, a mesma estrutura do input de texto.

Select

Um <select> também recebe um value, definido no próprio select em vez de no option selecionado:

jsx
function ColorPicker() {
  const [color, setColor] = useState('red')

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

Defina value no select, e React escolhe o option correspondente para você. Não há necessidade de adicionar um atributo selected a nenhum dos elementos option.

Textarea

Em HTML, uma textarea carrega seu texto entre as tags de abertura e fechamento. React a trata como um input como qualquer outro, então o texto chega através de uma prop value e onChange mantém tudo em sincronia:

jsx
function Feedback() {
  const [message, setMessage] = useState('')

  return (
    <textarea
      value={message}
      onChange={e => setMessage(e.target.value)}
      rows={4}
    />
  )
}

rows define a altura inicial da caixa. defaultValue é o equivalente não controlado de value aqui: define o texto inicial uma vez e deixa o DOM rastreá-lo a partir daí.

Radio buttons

Vários inputs compartilham um name, que diz ao navegador que pertencem juntos. Apenas um pode ser escolhido. Em React o grupo compartilha um pedaço de state, e o checked de cada input se compara contra ele:

jsx
function ShippingSpeed() {
  const [speed, setSpeed] = useState('standard')

  return (
    <fieldset>
      <legend>Shipping speed</legend>
      {['standard', 'express', 'overnight'].map(option => (
        <label key={option}>
          <input
            type="radio"
            name="speed"
            value={option}
            checked={speed === option}
            onChange={e => setSpeed(e.target.value)}
          />
          {option}
        </label>
      ))}
    </fieldset>
  )
}

fieldset e legend rotulam o grupo para leitores de tela, e cada label recebe uma key porque as opções vêm de um array (listas e keys).

checked={speed === option} é verdadeiro para exatamente uma opção, então um valor de state cobre todo o grupo, enquanto um checkbox carrega seu próprio booleano.

Enviar com uma action

Um <form> também pode receber uma função action. React a chama quando o formulário é enviado e passa um objeto FormData contendo os valores do formulário:

jsx
function Signup() {
  function handleSignup(formData) {
    const values = Object.fromEntries(formData)
    console.log(values.email, values.password)
  }

  return (
    <form action={handleSignup}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button>Sign up</button>
    </form>
  )
}

Não há evento aqui, então não há nada para chamar preventDefault(). React para o recarregamento padrão da página do navegador para você.

Object.fromEntries(formData) transforma FormData em um objeto simples em um passo. As chaves vêm do atributo name de cada input, é por isso que todo input no formulário precisa de um. Onde vários inputs compartilham um nome e podem ser enviados juntos, como um grupo de checkboxes, apenas o último valor sobrevive, e formData.getAll lê todos eles.

Os inputs acima são não controlados: o DOM mantém cada valor até o envio. Isso importa para o comportamento que surpreende pessoas vindo de onSubmit: React reseta o formulário uma vez que a action é resolvida, limpando campos não controlados como estes. Um campo controlado re-renderiza do state e mantém seu valor.

Cada caminho é adequado para um tipo diferente de formulário. Uma action se adequa a um formulário cujo trabalho é reunir valores e entregá-los, geralmente a um servidor, onde a espera é real e o resultado tem que voltar para a UI. Os valores chegam reunidos e nomeados. onSubmit com inputs controlados se adequa a um formulário que tem que reagir enquanto o usuário digita, para validação em tempo real, contagem de caracteres ou um campo que muda o que o resto do formulário mostra.

Version note

Form actions chegaram em React 19. Em React 18 e anteriores, <form action={handleSignup}> não dispara erro. React remove o atributo, porque uma função não é um valor de atributo válido, e avisa sobre isso no desenvolvimento. O formulário então envia para a URL atual, então a página recarrega e a função nunca é executada. Envio nessas versões passa por onSubmit com event.preventDefault().

Formulários reais raramente param em um campo. Um formulário de inscrição pedindo um nome, um email e uma senha poderia acompanhar três chamadas useState separadas, mas um objeto de state e um change handler único cobrem qualquer número de campos sem se repetir:

jsx
function SignupForm() {
  const [values, setValues] = useState({ name: '', email: '', password: '' })

  function handleChange(event) {
    const { name, value } = event.target
    setValues(prev => ({ ...prev, [name]: value }))
  }

  return (
    <form>
      <input name="name" value={values.name} onChange={handleChange} />
      <input name="email" value={values.email} onChange={handleChange} />
      <input type="password" name="password" value={values.password} onChange={handleChange} />
    </form>
  )
}

O atributo name de cada input corresponde a uma chave em values, então handleChangeevent.target.name para saber qual chave atualizar e [name]: value escreve de volta sob essa mesma chave. Adicione outro campo ao formulário, dê ao seu input um name correspondente, e o handler existente já o cobre.

Validação pode rodar em dois pontos diferentes, e eles servem propósitos diferentes. Validar na mudança, verificando o novo valor dentro de handleChange conforme ele chega, dá feedback imediato, útil para algo como um medidor de força de senha. Também significa que um erro pode aparecer para um campo que o usuário ainda não terminou de digitar. Validar no envio, verificando todo o objeto values dentro do handler de envio antes de qualquer coisa acontecer com ele, espera até que o usuário termine, que tende a ser o padrão melhor para verificações de campo obrigatório e formato. Muitos formulários misturam os dois: uma verificação leve na mudança, uma passada completa no envio.

Um envio que fala com um servidor leva tempo, e um botão que continua clicável durante essa espera convida a um segundo clique e um envio duplicado. Acompanhe um sinalizador submitting no state, defina-o antes do início da requisição e desabilite o botão enquanto for verdadeiro:

jsx
function SignupForm() {
  const [values, setValues] = useState({ name: '', email: '', password: '' })
  const [submitting, setSubmitting] = useState(false)

  async function handleSubmit(event) {
    event.preventDefault()
    setSubmitting(true)
    await saveSignup(values)
    setSubmitting(false)
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* inputs go here */}
      <button disabled={submitting}>{submitting ? 'Submitting...' : 'Sign up'}</button>
    </form>
  )
}

disabled={submitting} desabilita o botão e bloqueia cliques enquanto a requisição está em voo, então devolve o controle no momento em que se resolve. Um formulário que apenas reúne valores e os envia pode entregar todo o trabalho a uma action, onde useActionState envolve a action e te dá esse sinalizador pending sem ficar passando um.

Cada digitação em um input controlado faz uma volta completa: o DOM dispara um evento, seu handler chama setText, React re-renderiza o componente, e a nova prop value volta para o mesmo input. Parece instantâneo, mas o valor exibido do input está sendo definido por React em cada renderização. O elemento DOM não tem memória própria aqui, ele exibe o que o state diz agora.

Essa volta completa também é o custo. Um formulário com muitos campos controlados re-renderiza o componente inteiro a cada digitação em qualquer um deles. Normalmente isso é barato o suficiente para ignorar. Quando não é, ou quando o valor de um campo não precisa afetar qualquer outra coisa até envio, um input não controlado com uma ref é a escolha mais simples:

jsx
function NameForm() {
  const inputRef = useRef(null)

  function handleSubmit(event) {
    event.preventDefault()
    console.log(inputRef.current.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="" />
      <button>Submit</button>
    </form>
  )
}

Sem state, sem re-renderização por digitação. defaultValue faz o mesmo trabalho aqui que faz em uma textarea. Recorra a isso quando um campo é totalmente isolado: nada se renderiza diferente enquanto o usuário digita nele.

useActionState envolve uma action e devolve um sinalizador pending ao lado do que quer que a action retornou, então um sinalizador submitting e uma mensagem de erro deixam de ser pedaços separados de state que você liga sozinho. Server Actions e as integrações do framework construídas sobre eles levam isso mais longe, executando a action em si no servidor, passando aonde este manual vai.

JunoState conduz o input O padrão a manter é pequeno: o value do input vem do state, e onChange atualiza esse state. Uma vez que isso clica para um input de texto, funciona do mesmo jeito para selects e textareas, e para checkboxes e radio buttons com checked carregando o state. Envio é seu próprio passo: um submit de formulário executa uma função que você escreve, e essa função decide o que acontece com os valores.
JunoState conduz o input Inputs controlados são o padrão: value (ou checked para checkboxes) ligado a state, onChange atualizando, onSubmit com event.preventDefault() lidando com o envio. Para um formulário com múltiplos campos, um objeto de state e um handleChange chaveado por name substituem uma pilha de chamadas useState separadas, e um sinalizador submitting mantém o botão honesto enquanto uma requisição está em voo. Uma função action no formulário é a outra rota: React lhe passa um objeto FormData e cuida do evento, e useActionState assume esse sinalizador submitting uma vez que um servidor está envolvido.
JunoState conduz o input Inputs controlados são React possuindo o valor do DOM em cada renderização, o que é o que te dá um valor para ler, validar ou derivar a qualquer ponto. Saiba quando essa volta completa não está ganhando seu valor e uma ref não controlada vai fazer, e recorra a uma action de formulário quando o trabalho do formulário é reunir valores e entregá-los, com useActionState carregando o estado pending uma vez que um servidor está envolvido.

Próximo: Levantando state, onde dois componentes precisam compartilhar o mesmo pedaço de state.