O Que São Componentes Controlados e Não Controlados?
Antes de mais nada, precisamos entender que essa distinção existe principalmente no contexto do React, embora o conceito seja aplicável a outros frameworks. Um componente controlado é aquele cujo estado é gerenciado pelo React — ou seja, o valor do elemento de formulário vem do estado da aplicação e é atualizado através de event handlers. Um componente não controlado funciona de forma similar aos elementos HTML tradicionais, onde o DOM é a fonte da verdade, e você acessa o valor quando necessário, geralmente através de refs.
Essa diferença fundamental afeta como você projeta seus formulários, gerencia dados e implementa validações. Escolher a abordagem correta desde o início evita refatorações desnecessárias e problemas de sincronização de dados. Não é uma questão de qual é "melhor", mas qual é mais apropriada para cada situação específica.
Componentes Controlados: Dominando Seu Formulário
Um componente controlado coloca o React no centro do controle. Todo valor do input é sincronizado com o estado, e qualquer mudança passa por um handler que atualiza esse estado. Dessa forma, React conhece sempre qual é o valor atual.
Estrutura Básica
import React, { useState } from 'react';
function FormularioControlado() {
const [email, setEmail] = useState('');
const [senha, setSenha] = useState('');
const handleEmailChange = (event) => {
setEmail(event.target.value);
};
const handleSenhaChange = (event) => {
setSenha(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Email:', email, 'Senha:', senha);
// Enviar dados para servidor
};
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
value={email}
onChange={handleEmailChange}
/>
</label>
<label>
Senha:
<input
type="password"
value={senha}
onChange={handleSenhaChange}
/>
</label>
<button type="submit">Enviar</button>
</form>
);
}
export default FormularioControlado;
Note que cada input possui um atributo value vinculado ao estado e um onChange que atualiza esse estado. React é responsável por manter esses inputs sempre sincronizados com a aplicação. Você pode validar, desabilitar ou modificar campos em tempo real porque sempre tem acesso ao valor atual.
Validação em Tempo Real
Um dos grandes benefícios dos componentes controlados é implementar feedback imediato ao usuário:
import React, { useState } from 'react';
function FormularioComValidacao() {
const [username, setUsername] = useState('');
const [erro, setErro] = useState('');
const handleChange = (event) => {
const valor = event.target.value;
setUsername(valor);
// Validação em tempo real
if (valor.length < 3) {
setErro('Usuário deve ter no mínimo 3 caracteres');
} else if (!/^[a-zA-Z0-9_]+$/.test(valor)) {
setErro('Usuário pode conter apenas letras, números e underscore');
} else {
setErro('');
}
};
return (
<div>
<input
type="text"
value={username}
onChange={handleChange}
placeholder="Digite seu usuário"
/>
{erro && <p style={{ color: 'red' }}>{erro}</p>}
<p>Caracteres digitados: {username.length}</p>
</div>
);
}
export default FormularioComValidacao;
Aqui você vê claramente o poder: a validação ocorre a cada mudança, e você pode renderizar mensagens de erro ou desabilitar o botão de submit dinamicamente, tudo porque React mantém controle total sobre o estado.
Componentes Não Controlados: Simplicidade com Refs
Componentes não controlados deixam o DOM ser a fonte da verdade. Você não vincula um valor ao estado do React — em vez disso, acessa o valor diretamente do DOM quando necessário, usualmente com refs. Essa abordagem é mais próxima de como HTML tradicional funciona.
Estrutura Básica com useRef
import React, { useRef } from 'react';
function FormularioNaoControlado() {
const emailRef = useRef(null);
const senhaRef = useRef(null);
const handleSubmit = (event) => {
event.preventDefault();
const email = emailRef.current.value;
const senha = senhaRef.current.value;
console.log('Email:', email, 'Senha:', senha);
// Enviar dados para servidor
};
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input type="email" ref={emailRef} />
</label>
<label>
Senha:
<input type="password" ref={senhaRef} />
</label>
<button type="submit">Enviar</button>
</form>
);
}
export default FormularioNaoControlado;
Não há value nem onChange. Os inputs existem independentemente do estado React. Você acessa o valor apenas quando realmente precisa dele — no exemplo acima, no momento do submit. Isso é simples e direto para formulários básicos.
Resetar Formulário com Refs
Uma vantagem prática dos componentes não controlados é resetar formulários de forma simples:
import React, { useRef } from 'react';
function FormularioComReset() {
const formRef = useRef(null);
const handleSubmit = (event) => {
event.preventDefault();
console.log('Formulário enviado');
// Limpar formulário
formRef.current.reset();
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<input type="text" placeholder="Nome" />
<input type="email" placeholder="Email" />
<textarea placeholder="Mensagem"></textarea>
<button type="submit">Enviar</button>
</form>
);
}
export default FormularioComReset;
O reset() nativo do formulário HTML limpa todos os campos automaticamente — sem você ter que respetar múltiplas variáveis de estado.
Comparação Prática: Quando Usar Cada Abordagem
A escolha entre controlado e não controlado depende dos requisitos específicos do seu formulário. Não existe uma resposta universal, mas há padrões claros que ajudam nessa decisão.
Use Componentes Controlados Para
Validação em tempo real: Quando você precisa avisar o usuário sobre erros enquanto ele digita.
Desabilitar campo dinamicamente: Botão de submit desabilitado até que o formulário seja válido.
Integração com lógica complexa: Quando o valor de um campo afeta outros campos (ex: seleção de estado carrega cidades).
Histórico e desfazer: Quando você precisa manter histórico de mudanças para implementar undo/redo.
Exemplo prático de integração entre campos:
import React, { useState } from 'react';
function FormularioIntegrado() {
const [pais, setPais] = useState('');
const [estado, setEstado] = useState('');
const estados = {
'brasil': ['São Paulo', 'Rio de Janeiro', 'Minas Gerais'],
'argentina': ['Buenos Aires', 'Córdoba', 'Rosario'],
};
const handlePaisChange = (event) => {
const novoPais = event.target.value;
setPais(novoPais);
setEstado(''); // Reseta estado quando país muda
};
return (
<div>
<select value={pais} onChange={handlePaisChange}>
<option value="">Selecione um país</option>
<option value="brasil">Brasil</option>
<option value="argentina">Argentina</option>
</select>
{pais && (
<select value={estado} onChange={(e) => setEstado(e.target.value)}>
<option value="">Selecione um estado</option>
{estados[pais]?.map((est) => (
<option key={est} value={est}>
{est}
</option>
))}
</select>
)}
<p>
{pais && estado
? `Você selecionou ${estado}, ${pais}`
: 'Selecione país e estado'}
</p>
</div>
);
}
export default FormularioIntegrado;
Aqui, um campo controlado afeta diretamente o outro. Isso é praticamente impossível com componentes não controlados sem lógica adicional.
Use Componentes Não Controlados Para
Integração com código legacy: Quando você está migrando gradualmente de jQuery ou código vanilla JavaScript.
Formulários muito simples: Inputs básicos onde você coleta dados apenas no submit.
Upload de arquivos: O <input type="file"> é inerentemente não controlado em React.
Performance com formulários gigantes: Formulários com centenas de campos onde cada atualização de estado causaria re-renders.
Exemplo com upload de arquivo:
import React, { useRef } from 'react';
function UploadArquivo() {
const arquivoRef = useRef(null);
const handleSubmit = (event) => {
event.preventDefault();
const arquivo = arquivoRef.current.files[0];
if (arquivo) {
console.log('Arquivo selecionado:', arquivo.name);
// Fazer upload
}
};
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={arquivoRef} />
<button type="submit">Upload</button>
</form>
);
}
export default UploadArquivo;
Não faz sentido tentar controlar <input type="file"> — o React não pode setar o valor por questões de segurança. Refs são a solução natural.
Abordagem Híbrida: O Melhor dos Dois Mundos
Na prática, projetos reais frequentemente usam uma mistura de ambas. Você pode ter um formulário com campos controlados para validação complexa e campos não controlados para dados simples.
import React, { useState, useRef } from 'react';
function FormularioHibrido() {
// Controlado: email com validação
const [email, setEmail] = useState('');
const [erroEmail, setErroEmail] = useState('');
// Não controlado: arquivo
const arquivoRef = useRef(null);
// Não controlado: campo simples
const nomeRef = useRef(null);
const handleEmailChange = (event) => {
const valor = event.target.value;
setEmail(valor);
if (!valor.includes('@')) {
setErroEmail('Email inválido');
} else {
setErroEmail('');
}
};
const handleSubmit = (event) => {
event.preventDefault();
const dados = {
email: email,
nome: nomeRef.current.value,
arquivo: arquivoRef.current.files[0],
};
console.log('Enviando:', dados);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Nome (não controlado):
<input type="text" ref={nomeRef} />
</label>
</div>
<div>
<label>
Email (controlado):
<input
type="email"
value={email}
onChange={handleEmailChange}
/>
</label>
{erroEmail && <p style={{ color: 'red' }}>{erroEmail}</p>}
</div>
<div>
<label>
Arquivo (não controlado):
<input type="file" ref={arquivoRef} />
</label>
</div>
<button type="submit" disabled={erroEmail !== ''}>
Enviar
</button>
</form>
);
}
export default FormularioHibrido;
Essa abordagem pragmática reconhece que diferentes partes de um formulário têm diferentes necessidades. Email precisa de validação em tempo real (controlado), nome é simples (não controlado), e arquivo não pode ser controlado mesmo que quiséssemos.
Conclusão
Aprendemos que componentes controlados colocam React no controle total do estado do formulário, permitindo validação em tempo real, lógica interdependente entre campos e controle fino sobre o que o usuário vê. São a escolha padrão para formulários complexos e interativos. Componentes não controlados delegam o controle ao DOM, acessando valores apenas quando necessário através de refs — são simples, diretos e adequados para formulários básicos, uploads e situações onde performance é crítica. A maioria dos projetos reais usa uma abordagem híbrida, combinando ambas conforme a necessidade de cada campo. A chave é reconhecer que essa não é uma questão de "um é melhor que o outro", mas de escolher a ferramenta certa para o trabalho específico.