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 estado e atualize esse estado no onChange, e o input começa a funcionar como tudo mais em React: o estado contém a verdade sobre o que está 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 formulário pequeno 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 estado text, e cada digitação dispara onChange, que chama setText com o novo valor. O estado e o input permanecem sincronizados porque React re-renderiza o input com qualquer text que esteja atualmente guardado. Enviar um formulário recarrega a página por padrão, então handleSubmit chama event.preventDefault() primeiro, depois faz o que a aplicação precisa, registrando o texto aqui, enviando-o para um servidor em outro lugar.

Inputs controlados e não controlados

O input acima é um input controlado, porque seu valor vive no estado React e React decide o que ele exibe. Dê a ele um value sem um onChange, e o input fica somente leitura: React continua pintando o mesmo valor a cada render, e nada que você digita jamais chega ao estado, então combine os dois sempre que um campo for controlado. Um input também pode ser não controlado: o DOM mantém rastreamento do seu próprio valor, e você o lê apenas quando precisa, através de uma ref para um único campo (uma forma de acessar um elemento DOM diretamente, coberta completamente em Refs e o DOM) ou através do FormData do formulário para um formulário inteiro na hora do envio. Inputs controlados dão ao componente o valor atual a cada digitação, o que é o que validação ao vivo, contadores de caracteres, ou campos dos quais outras partes da UI dependem precisam. Inputs não controlados pulam uma renderização por digitação e procuram pelo valor apenas na hora do envio, o que é menos código para um campo simples que mais nada precisa observar.

Checkboxes

Checkboxes seguem o mesmo padrão, mas seu estado 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, mesma forma que o input de texto.

Select

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

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 a 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 a mantém sincronizada:

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 daí em diante.

Botões de radio

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 estado, e cada checked do input 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 chaves).

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

Enviando 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 em que chamar preventDefault(). React para o carregamento padrão da página do navegador para você.

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

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 vindas 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 estado e mantém seu valor.

Cada caminho serve a um tipo diferente de formulário. Uma action cabe em um formulário cujo trabalho é coletar valores e passá-los adiante, geralmente para um servidor, onde a espera é real e o resultado tem que voltar para a UI. Os valores chegam coletados e nomeados. onSubmit com inputs controlados cabe em um formulário que tem que reagir enquanto o usuário digita, para validação ao vivo, uma contagem de caracteres, ou um campo que muda o que o resto do formulário mostra.

Nota de versão

Form actions chegaram no React 19. No React 18 e anteriores, <form action={handleSignup}> não lança erro. React descarta o atributo, porque uma função não é um valor de atributo válido, e avisa sobre isso em desenvolvimento. O formulário então é enviado para a URL atual, então a página recarrega e a função nunca é executada. O 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 rastrear três chamadas useState separadas, mas um objeto de estado e um handler de mudança única 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 combina com uma chave em values, então handleChangeevent.target.name para saber qual chave atualizar e [name]: value a escreve de volta sob aquela mesma chave. Adicione outro campo ao formulário, dê a seu input um name correspondente, e o handler existente já o cobre.

A validação pode rodar em dois pontos diferentes, e servem a propósitos diferentes. Validar na mudança, verificando o novo valor dentro de handleChange conforme ele chega, dá feedback de 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 o objeto values inteiro dentro do handler de envio antes de qualquer coisa acontecer com ele, aguarda até o usuário terminar, o que tende a ser o padrão melhor para campos obrigatórios e verificações de formato. Muitos formulários misturam os dois: uma verificação leve na mudança, uma passagem completa no envio.

Um envio que conversa com um servidor leva tempo, e um botão que permanece clicável durante essa espera convida um segundo clique e um envio duplicado. Rastreie uma flag submitting no estado, defina-a antes do request começar, e desative o botão enquanto for verdadeira:

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} desativa o botão e bloqueia cliques enquanto o request está em andamento, então devolvendo o controle no momento em que é resolvido. Um formulário que apenas coleta valores e os envia pode passar o trabalho inteiro para uma action, onde useActionState envolve a action e dá a você aquela flag pendente sem configurar uma.

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 chega de volta no mesmo input. Parece instantâneo, mas o valor exibido do input está sendo definido por React a cada render. O elemento DOM não tem memória própria aqui, ele exibe o que o estado 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. Geralmente isso é barato o suficiente para ignorar. Quando não é, ou quando o valor de um campo não precisa afetar qualquer coisa até o 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 estado, sem re-render por digitação. defaultValue faz o mesmo trabalho aqui que faz em uma textarea. Recorra a isso quando um campo é completamente isolado: nada renderiza diferentemente enquanto o usuário está digitando nele.

useActionState envolve uma action e devolve uma flag pendente ao lado do que quer que a action retornou, então uma flag submitting e uma mensagem de erro param de ser pedaços separados de estado que você configura. Server Actions e as integrações de framework construídas sobre eles vão mais longe, executando a própria action no servidor, para além de onde este handbook vai.

JunoState drives the input O padrão para manter é pequeno: o value do input vem do estado, e onChange atualiza esse estado. Uma vez que isso faz sentido para um input de texto, funciona da mesma forma para selects e textareas, e para checkboxes e botões de radio com checked carregando o estado.

Enviar é seu próprio passo: um envio de formulário executa uma função que você escreve, e aquela função decide o que acontece com os valores.

JunoState drives the input Inputs controlados são o padrão: value (ou checked para checkboxes) amarrado ao estado, onChange atualizando-o, onSubmit com event.preventDefault() lidando com o envio.

Para um formulário multi-campo, um objeto de estado único e um handleChange chaveado por name substituem uma pilha de chamadas useState separadas, e uma flag submitting mantém o botão honesto enquanto um request está em andamento.

Uma função action no formulário é a outra rota: React passa a ela um objeto FormData e cuida do evento, e useActionState assume aquela flag submitting uma vez que um servidor está envolvido.

JunoState drives the input Inputs controlados são React possuindo o valor do DOM a cada render, o que é o que lhe dá um valor para ler, validar, ou derivar a qualquer ponto.

Saiba quando aquela volta completa não está ganhando seu preço e uma ref não controlada fará, e recorra a uma action de formulário quando o trabalho de um formulário é coletar valores e passá-los adiante, com useActionState carregando o estado pendente uma vez que um servidor está envolvido.

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