React & Frontend

Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática

14 min de leitura

Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática

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

<h2>O Que São Componentes Controlados e Não Controlados?</h2>

<p>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 <strong>componente controlado</strong> é 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 <strong>componente não controlado</strong> 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.</p>

<p>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 é &quot;melhor&quot;, mas qual é mais apropriada para cada situação específica.</p>

<h2>Componentes Controlados: Dominando Seu Formulário</h2>

<p>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.</p>

<h3>Estrutura Básica</h3>

<pre><code class="language-jsx">import React, { useState } from &#039;react&#039;;

function FormularioControlado() {

const [email, setEmail] = useState(&#039;&#039;);

const [senha, setSenha] = useState(&#039;&#039;);

const handleEmailChange = (event) =&gt; {

setEmail(event.target.value);

};

const handleSenhaChange = (event) =&gt; {

setSenha(event.target.value);

};

const handleSubmit = (event) =&gt; {

event.preventDefault();

console.log(&#039;Email:&#039;, email, &#039;Senha:&#039;, senha);

// Enviar dados para servidor

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;label&gt;

Email:

&lt;input

type=&quot;email&quot;

value={email}

onChange={handleEmailChange}

/&gt;

&lt;/label&gt;

&lt;label&gt;

Senha:

&lt;input

type=&quot;password&quot;

value={senha}

onChange={handleSenhaChange}

/&gt;

&lt;/label&gt;

&lt;button type=&quot;submit&quot;&gt;Enviar&lt;/button&gt;

&lt;/form&gt;

);

}

export default FormularioControlado;</code></pre>

<p>Note que cada input possui um atributo <code>value</code> vinculado ao estado e um <code>onChange</code> 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.</p>

<h3>Validação em Tempo Real</h3>

<p>Um dos grandes benefícios dos componentes controlados é implementar feedback imediato ao usuário:</p>

<pre><code class="language-jsx">import React, { useState } from &#039;react&#039;;

function FormularioComValidacao() {

const [username, setUsername] = useState(&#039;&#039;);

const [erro, setErro] = useState(&#039;&#039;);

const handleChange = (event) =&gt; {

const valor = event.target.value;

setUsername(valor);

// Validação em tempo real

if (valor.length &lt; 3) {

setErro(&#039;Usuário deve ter no mínimo 3 caracteres&#039;);

} else if (!/^[a-zA-Z0-9_]+$/.test(valor)) {

setErro(&#039;Usuário pode conter apenas letras, números e underscore&#039;);

} else {

setErro(&#039;&#039;);

}

};

return (

&lt;div&gt;

&lt;input

type=&quot;text&quot;

value={username}

onChange={handleChange}

placeholder=&quot;Digite seu usuário&quot;

/&gt;

{erro &amp;&amp; &lt;p style={{ color: &#039;red&#039; }}&gt;{erro}&lt;/p&gt;}

&lt;p&gt;Caracteres digitados: {username.length}&lt;/p&gt;

&lt;/div&gt;

);

}

export default FormularioComValidacao;</code></pre>

<p>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.</p>

<h2>Componentes Não Controlados: Simplicidade com Refs</h2>

<p>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.</p>

<h3>Estrutura Básica com useRef</h3>

<pre><code class="language-jsx">import React, { useRef } from &#039;react&#039;;

function FormularioNaoControlado() {

const emailRef = useRef(null);

const senhaRef = useRef(null);

const handleSubmit = (event) =&gt; {

event.preventDefault();

const email = emailRef.current.value;

const senha = senhaRef.current.value;

console.log(&#039;Email:&#039;, email, &#039;Senha:&#039;, senha);

// Enviar dados para servidor

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;label&gt;

Email:

&lt;input type=&quot;email&quot; ref={emailRef} /&gt;

&lt;/label&gt;

&lt;label&gt;

Senha:

&lt;input type=&quot;password&quot; ref={senhaRef} /&gt;

&lt;/label&gt;

&lt;button type=&quot;submit&quot;&gt;Enviar&lt;/button&gt;

&lt;/form&gt;

);

}

export default FormularioNaoControlado;</code></pre>

<p>Não há <code>value</code> nem <code>onChange</code>. 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.</p>

<h3>Resetar Formulário com Refs</h3>

<p>Uma vantagem prática dos componentes não controlados é resetar formulários de forma simples:</p>

<pre><code class="language-jsx">import React, { useRef } from &#039;react&#039;;

function FormularioComReset() {

const formRef = useRef(null);

const handleSubmit = (event) =&gt; {

event.preventDefault();

console.log(&#039;Formulário enviado&#039;);

// Limpar formulário

formRef.current.reset();

};

return (

&lt;form ref={formRef} onSubmit={handleSubmit}&gt;

&lt;input type=&quot;text&quot; placeholder=&quot;Nome&quot; /&gt;

&lt;input type=&quot;email&quot; placeholder=&quot;Email&quot; /&gt;

&lt;textarea placeholder=&quot;Mensagem&quot;&gt;&lt;/textarea&gt;

&lt;button type=&quot;submit&quot;&gt;Enviar&lt;/button&gt;

&lt;/form&gt;

);

}

export default FormularioComReset;</code></pre>

<p>O <code>reset()</code> nativo do formulário HTML limpa todos os campos automaticamente — sem você ter que respetar múltiplas variáveis de estado.</p>

<h2>Comparação Prática: Quando Usar Cada Abordagem</h2>

<p>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.</p>

<h3>Use Componentes Controlados Para</h3>

<p><strong>Validação em tempo real</strong>: Quando você precisa avisar o usuário sobre erros enquanto ele digita.</p>

<p><strong>Desabilitar campo dinamicamente</strong>: Botão de submit desabilitado até que o formulário seja válido.</p>

<p><strong>Integração com lógica complexa</strong>: Quando o valor de um campo afeta outros campos (ex: seleção de estado carrega cidades).</p>

<p><strong>Histórico e desfazer</strong>: Quando você precisa manter histórico de mudanças para implementar undo/redo.</p>

<p><strong>Exemplo prático de integração entre campos:</strong></p>

<pre><code class="language-jsx">import React, { useState } from &#039;react&#039;;

function FormularioIntegrado() {

const [pais, setPais] = useState(&#039;&#039;);

const [estado, setEstado] = useState(&#039;&#039;);

const estados = {

&#039;brasil&#039;: [&#039;São Paulo&#039;, &#039;Rio de Janeiro&#039;, &#039;Minas Gerais&#039;],

&#039;argentina&#039;: [&#039;Buenos Aires&#039;, &#039;Córdoba&#039;, &#039;Rosario&#039;],

};

const handlePaisChange = (event) =&gt; {

const novoPais = event.target.value;

setPais(novoPais);

setEstado(&#039;&#039;); // Reseta estado quando país muda

};

return (

&lt;div&gt;

&lt;select value={pais} onChange={handlePaisChange}&gt;

&lt;option value=&quot;&quot;&gt;Selecione um país&lt;/option&gt;

&lt;option value=&quot;brasil&quot;&gt;Brasil&lt;/option&gt;

&lt;option value=&quot;argentina&quot;&gt;Argentina&lt;/option&gt;

&lt;/select&gt;

{pais &amp;&amp; (

&lt;select value={estado} onChange={(e) =&gt; setEstado(e.target.value)}&gt;

&lt;option value=&quot;&quot;&gt;Selecione um estado&lt;/option&gt;

{estados[pais]?.map((est) =&gt; (

&lt;option key={est} value={est}&gt;

{est}

&lt;/option&gt;

))}

&lt;/select&gt;

)}

&lt;p&gt;

{pais &amp;&amp; estado

? Você selecionou ${estado}, ${pais}

: &#039;Selecione país e estado&#039;}

&lt;/p&gt;

&lt;/div&gt;

);

}

export default FormularioIntegrado;</code></pre>

<p>Aqui, um campo controlado afeta diretamente o outro. Isso é praticamente impossível com componentes não controlados sem lógica adicional.</p>

<h3>Use Componentes Não Controlados Para</h3>

<p><strong>Integração com código legacy</strong>: Quando você está migrando gradualmente de jQuery ou código vanilla JavaScript.</p>

<p><strong>Formulários muito simples</strong>: Inputs básicos onde você coleta dados apenas no submit.</p>

<p><strong>Upload de arquivos</strong>: O <code>&lt;input type=&quot;file&quot;&gt;</code> é inerentemente não controlado em React.</p>

<p><strong>Performance com formulários gigantes</strong>: Formulários com centenas de campos onde cada atualização de estado causaria re-renders.</p>

<p><strong>Exemplo com upload de arquivo:</strong></p>

<pre><code class="language-jsx">import React, { useRef } from &#039;react&#039;;

function UploadArquivo() {

const arquivoRef = useRef(null);

const handleSubmit = (event) =&gt; {

event.preventDefault();

const arquivo = arquivoRef.current.files[0];

if (arquivo) {

console.log(&#039;Arquivo selecionado:&#039;, arquivo.name);

// Fazer upload

}

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;input type=&quot;file&quot; ref={arquivoRef} /&gt;

&lt;button type=&quot;submit&quot;&gt;Upload&lt;/button&gt;

&lt;/form&gt;

);

}

export default UploadArquivo;</code></pre>

<p>Não faz sentido tentar controlar <code>&lt;input type=&quot;file&quot;&gt;</code> — o React não pode setar o valor por questões de segurança. Refs são a solução natural.</p>

<h2>Abordagem Híbrida: O Melhor dos Dois Mundos</h2>

<p>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.</p>

<pre><code class="language-jsx">import React, { useState, useRef } from &#039;react&#039;;

function FormularioHibrido() {

// Controlado: email com validação

const [email, setEmail] = useState(&#039;&#039;);

const [erroEmail, setErroEmail] = useState(&#039;&#039;);

// Não controlado: arquivo

const arquivoRef = useRef(null);

// Não controlado: campo simples

const nomeRef = useRef(null);

const handleEmailChange = (event) =&gt; {

const valor = event.target.value;

setEmail(valor);

if (!valor.includes(&#039;@&#039;)) {

setErroEmail(&#039;Email inválido&#039;);

} else {

setErroEmail(&#039;&#039;);

}

};

const handleSubmit = (event) =&gt; {

event.preventDefault();

const dados = {

email: email,

nome: nomeRef.current.value,

arquivo: arquivoRef.current.files[0],

};

console.log(&#039;Enviando:&#039;, dados);

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;div&gt;

&lt;label&gt;

Nome (não controlado):

&lt;input type=&quot;text&quot; ref={nomeRef} /&gt;

&lt;/label&gt;

&lt;/div&gt;

&lt;div&gt;

&lt;label&gt;

Email (controlado):

&lt;input

type=&quot;email&quot;

value={email}

onChange={handleEmailChange}

/&gt;

&lt;/label&gt;

{erroEmail &amp;&amp; &lt;p style={{ color: &#039;red&#039; }}&gt;{erroEmail}&lt;/p&gt;}

&lt;/div&gt;

&lt;div&gt;

&lt;label&gt;

Arquivo (não controlado):

&lt;input type=&quot;file&quot; ref={arquivoRef} /&gt;

&lt;/label&gt;

&lt;/div&gt;

&lt;button type=&quot;submit&quot; disabled={erroEmail !== &#039;&#039;}&gt;

Enviar

&lt;/button&gt;

&lt;/form&gt;

);

}

export default FormularioHibrido;</code></pre>

<p>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.</p>

<h2>Conclusão</h2>

<p>Aprendemos que <strong>componentes controlados</strong> 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. <strong>Componentes não controlados</strong> 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 <strong>abordagem híbrida</strong>, combinando ambas conforme a necessidade de cada campo. A chave é reconhecer que essa não é uma questão de &quot;um é melhor que o outro&quot;, mas de escolher a ferramenta certa para o trabalho específico.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable" target="_blank" rel="noopener noreferrer">React Documentation: Forms - Controlled Components</a></li>

<li><a href="https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components" target="_blank" rel="noopener noreferrer">React Documentation: Uncontrolled Components</a></li>

<li><a href="https://developer.mozilla.org/en-US/docs/Learn/Forms" target="_blank" rel="noopener noreferrer">MDN Web Docs: HTML Forms</a></li>

<li><a href="https://formik.org/" target="_blank" rel="noopener noreferrer">Formik Documentation: Working with Forms</a></li>

<li><a href="https://react.dev/reference/react/useRef" target="_blank" rel="noopener noreferrer">React Hooks Documentation: useRef</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

O que Todo Dev Deve Saber sobre Hooks para Formulários: Abstraindo Validação e Estado de Campos
O que Todo Dev Deve Saber sobre Hooks para Formulários: Abstraindo Validação e Estado de Campos

Entendendo o Problema: Estado e Validação em Formulários Quando começamos a t...

Como Usar Playwright com React: E2E, Visual Regression e Component Testing em Produção
Como Usar Playwright com React: E2E, Visual Regression e Component Testing em Produção

Entendendo Playwright e sua Integração com React Playwright é uma framework d...

Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI
Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI

Entendendo o Cache e sua Importância no React Query O cache é o coração do Re...