Einstein Components: o que são e como lidar com eles

Todo desenvolvedor já esbarrou num componente que parece inteligente demais. Ele calcula estado, chama API, decide layout, valida formulário, renderiza oito variações diferentes e ainda sabe quando mostrar modal. Esses são os famosos Einstein Components: componentes que sabem tanto que você precisa de um doutorado para alterar uma prop sem quebrar tudo.
Enquanto o padrão clássico do front recomenda “Componentes burros” (ou dumb componentes), aqueles que são simples, previsíveis e burros no melhor sentido possível, não sabem nada além da responsabilidade dele. O "Einstein Component" é aquele que sabe tanto sobre diversos assuntos diferentes que, a menor mudança, causa insegurança de lidar com N efeitos colaterais.
Então qual a forma prática de lidar com esses monstrinhos? Reescrever tudo do zero? Até dá, mas é receita clássica para: levar mais tempo do que deveria, reinventar problemas que já estavam resolvidos, e o pior, acabar esquecendo algum detalhe que o componente antigo já fazia direitinho desde 2019. E seguir empilhando funcionalidades nesse caos organizado? Aí você só adia a dor: cada nova linha vira uma aposta contra o próprio futuro.
Na literatura de design de software existe um consenso: começar pelo mínimo, organizando a bagunça antes de mover peças, e então aplicar pequenos refactors seguros e graduais.
Neste artigo, vou mostrar um passo a passo completo usando um componente fictício (mas dolorosamente realista). Se você quiser se aprofundar ainda mais no assunto, da uma olhada nessas obras:
Tidy First? - Kent Beck: Um livro curto e prático sobre como pequenas minirrefatorações, feitas do jeito certo melhoram o código sem tocar na lógica.
Refatoração - Martin Fowler: Esse ainda ta na minha fila, mas é uma das maiores referencias pra esse tema.
Agora, bora pro código!
O Einstein Component
Abaixo, segue um exemplo comum do dia a dia de um componente com uma tarefa relativamente simples: buscar dados de uma API e mostrar na tela.
const ProductPanel = ({ user, productId, mode, showSummary }) => {
const [loading, setLoading] = useState(true);
const [product, setProduct] = useState(null);
const [toast, setToast] = useState(null);
useEffect(() => {
fetch(`/api/products/${productId}`)
.then((res) => res.json())
.then((data) => {
if (mode === "full") {
setProduct({ ...data, price: Number(data.price) });
} else {
setProduct({ name: data.name });
}
})
.finally(() => setLoading(false));
}, [productId, mode]);
const handleBuy = async () => {
if (!user) {
setToast("Faça login!");
return;
}
// Finja que tem 158 linhas de lógica aqui xD
};
if (loading) return <Loader />;
return (
<div>
{showSummary && <Summary user={user} product={product} />}
{product && (
<button className={mode === "full" ? "big" : "small"} onClick={handleBuy}>
Comprar
</button>
)}
{toast && <Toast message={toast} />}
</div>
);
};
Mesmo para uma tarefa simples como essa, o componente acaba lidando com muita coisa ao mesmo tempo: requisição da API, controle de estado de carregamento, lógica de interação com o botão (handleBuy), determinação dinâmica de classes com base em mode e renderização condicional de UI dependendo do retorno da API.
Uma parte dessa complexidade é essencial: ela vem diretamente do problema que estamos tentando resolver. O problema está na complexidade acidental: aquela que surge das escolhas que fazemos ao estruturar o código. Nesse caso, a decisão de concentrar todas essas responsabilidades dentro de um único componente transforma algo simples em um pequeno aglomerado de comportamentos misturados.
Refatorando o Einstein Component
Passo 0 - Testes
Antes de pensar em refatorar qualquer coisa, garanta que existem testes automatizados protegendo o comportamento atual do componente. Isso evita que você tenha que fazer rollback em produção numa sexta-feira às 18h99, enquanto explica para os stakeholders por que a sua “grande melhoria” acabou virando um bug.
O segredo aqui é sempre testar o comportamento, nunca a implementação. Ou seja: valide que, dado um certo retorno da API, o componente renderiza o que deveria, em vez de testar se uma função interna específica foi chamada. Esse tipo de teste te dá liberdade para mover código, dividir responsabilidades e reorganizar estruturas sem medo de quebrar funcionalidades invisíveis.
Passo 1 - Deixar explícito o que está implícito
Primeiro, vamos identificar pequenas mudanças que não alterem comportamento, mas melhorem legibilidade.
// Antes
.then((data) => {
if (mode === "full") {
setProduct({ ...data, price: Number(data.price) });
} else {
setProduct({ name: data.name });
}
})
// Depois
.then((data) => {
const normalizedProduct =
mode === "full"
? { ...data, price: Number(data.price) }
: { name: data.name };
setProduct(normalizedProduct);
})
Nomear normalizedProduct torna explicitamente visível que ali há uma responsabilidade, facilitando tanto a leitura do código atual, como o próximo passo da reorganização do código.
Passo 2 - Extrair responsabilidades
Agora é hora de identificar trechos que realizam mais de uma ação e separá-los, para isolar responsabilidades e reduzir efeitos colaterais.
No caso do nosso componente, o useEffect é responsável por buscar dados, normalizá-los e controlar o estado visual de loading, tudo ao mesmo tempo.
Antes
useEffect(() => {
fetch(`/api/products/${productId}`)
.then((res) => res.json())
.then((data) => {
const normalizedProduct =
mode === "full"
? { ...data, price: Number(data.price) }
: { name: data.name };
setProduct(normalizedProduct);
})
.finally(() => setLoading(false));
}, [productId, mode]);
Depois
Separe a função que se comunica com a API para uma camada própria para isso.
const fetchProduct = async (id, mode) => {
const data = await fetch(`/api/products/${id}`).then((r) => r.json());
const normalizedProduct =
mode === "full"
? { ...data, price: Number(data.price) }
: { name: data.name };
return normalizedProduct;
};
Importe o fetchProduct e o consuma no useEffect.
import { fetchProduct } from "..."
useEffect(() => {
fetchProduct(productId, mode)
.then((normalized) => {
setProduct(normalized);
})
.finally(() => {
setLoading(false);
});
}, [productId, mode]);
Dessa forma, a lógica de decidir como recuperar os dados e transformá-los, já não pertence mais ao nosso componente, e sim a uma camada apenas responsável por isso. E o useEffect apenas coordena a lógica de visualização.
Além de que, agora é extremamente fácil de criar testes automatizados para fetchProduct, basta mockar o retorno da API e descrever os cenários de normalização dos dados. Também podemos reaproveitá-lo em outras partes da nossa aplicação.
Passo 3 - Separar lógica e declaração visual
O próximo objetivo é garantir que o componente se preocupe apenas com apresentação. Para isso, toda lógica que não é puramente visual deve ser extraída para módulos separados.
Antes
Hoje, o componente contém tanto o useEffect quanto toda a lógica de compra. Vamos isolá-los para melhorar a separação de responsabilidades.
useEffect(() => {
fetch(`/api/products/${productId}`)
.then((res) => res.json())
.then((data) => {
if (mode === "full") {
setProduct({ ...data, price: Number(data.price) });
} else {
setProduct({ name: data.name });
}
})
.finally(() => setLoading(false));
}, [productId, mode]);
const handleBuy = async () => {
if (!user) {
setToast("Faça login!");
return;
}
// Finja que tem 158 linhas de lógica aqui xD
};
Depois
Vamos criar hooks customizáveis:
const useBuyAction = (user) => {
const [toast, setToast] = useState(null);
const handleBuy = async (product) => {
if (!user) {
setToast("Faça login!");
return;
}
// Finja que tem 158 linhas de lógica aqui xD
};
return { toast, handleBuy };
};
const useProduct = (id, mode) => {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
let active = true;
setLoading(true);
fetchProduct(id, mode).then((data) => {
if (active) setProduct(data);
setLoading(false);
});
return () => { active = false; };
}, [id, mode]);
return { product, loading };
};
E então utiliza-los em nosso componente:
const ProductPanel = ({ productId, mode, showSummary, user }) => {
const { product, loading } = useProduct(productId, mode);
const { toast, handleBuy } = useBuyAction(user);
};
Dessa forma, ProductPanel apenas se preocupa em orquestrar os dados, mas não sabe nada sobre a implementação deles.
Passo 4 - Separar lógicas visuais
Se o componente ainda contém decisões visuais que pertencem a contextos diferentes, significa que é possível extrair novos componentes e reduzir ainda mais o acoplamento.
Antes
Além de lidar com Summary e Toast, o componente ainda decide qual classe aplicar no botão com base em mode:
return (
<div>
{showSummary && <Summary user={user} product={product} />}
{product && (
<button className={mode === "full" ? "big" : "small"} onClick={handleBuy}>
Comprar
</button>
)}
{toast && <Toast message={toast} />}
</div>
);
Depois
Podemos então separar essa decisão para um componente menor, apenas responsável por isso:
const BuyButton = ({ mode, onClick }) => (
<button className={mode === "full" ? "big" : "small"} onClick={onClick}>
Comprar
</button>
);
E então utilizá-lo no nosso componente principal.
return (
<div>
{showSummary && <Summary user={user} product={product} />}
{product && <BuyButton mode={mode} onClick={handleBuy} />)}
{toast && <Toast message={toast} />}
</div>
);
Resultado final
Agora, nosso componente apenas junta as peças. A carga cognitiva é muito menor: basta um rápido olhar para entender de onde cada informação vem e para onde vai.
const ProductPanel = ({ productId, mode, showSummary, user }) => {
const { product, loading } = useProduct(productId, mode);
const { toast, handleBuy } = useBuyAction(user);
if (loading) return <Loader />;
return (
<>
{showSummary && <Summary product={product} user={user} />}
{product && <BuyButton mode={mode} onClick={handleBuy} />)}
{toast && <Toast message={toast} />}
</>
);
};
Cada extração, cada nomeação mais clara, cada responsabilidade removida do componente principal contribui para algo muito maior: previsibilidade.
O código final não é apenas mais limpo. Ele é mais fácil de testar, de expandir, de revisar. E isso é o que separa um sistema saudável de uma bomba-relógio.
Da próxima vez que você encontrar um componente que parece saber demais, lembre-se: comece pequeno, proteja comportamentos com testes, extraia responsabilidades com cuidado e avance em passos seguros.
Seu eu do futuro (e sua sexta-feira às 18h99) agradecem.
Qualquer tolo escreve um código que um computador possa entender.
Bons programadores escrevem códigos que os seres humanos podem entender.
- Martin Fowler