Depois de alguns anos trabalhando com React, eu desenvolvi uma espécie de "checklist" pra decidir onde colocar a lógica. Funciona mais ou menos assim: esse componente precisa ser reutilizado em outros lugares? Se sim, ele provavelmente deveria ser dumb. Um botão, um card, um input — esses caras não deveriam saber de onde os dados vêm. Esse componente representa uma "página" ou uma "funcionalidade completa"? Aí sim, ele pode (e provavelmente vai) ter lógica. A lógica está ficando complexa demais? Se você olha pro componente e ele tem 15 useStates, 3 useEffects e você já não lembra mais o que cada coisa faz, é hora de extrair essa lógica pra um lugar separado.
E é aí que os hooks customizados entram. Uma coisa que mudou muito a forma como eu organizo código foi entender o papel dos hooks na arquitetura — eu até escrevi um artigo sobre isso se você quiser se aprofundar. A ideia é simples: ao invés de deixar toda a lógica dentro do componente, você extrai ela pra um hook. O componente fica responsável só pela renderização. Olha a diferença:
Antes (tudo junto):
function ListaProdutos() {
const [produtos, setProdutos] = useState([]);
const [loading, setLoading] = useState(true);
const [erro, setErro] = useState(null);
useEffect(() => {
fetch('/api/produtos')
.then(res => res.json())
.then(data => {
setProdutos(data);
setLoading(false);
})
.catch(err => {
setErro(err);
setLoading(false);
});
}, []);
if (loading) return <p>Carregando...</p>;
if (erro) return <p>Deu ruim: {erro.message}</p>;
return (
<ul>
{produtos.map(p => <ProdutoCard key={p.id} produto={p} />)}
</ul>
);
}
Depois (com hook customizado):
function useProdutos() {
const [produtos, setProdutos] = useState([]);
const [loading, setLoading] = useState(true);
const [erro, setErro] = useState(null);
useEffect(() => {
fetch('/api/produtos')
.then(res => res.json())
.then(data => {
setProdutos(data);
setLoading(false);
})
.catch(err => {
setErro(err);
setLoading(false);
});
}, []);
return { produtos, loading, erro };
}
function ListaProdutos() {
const { produtos, loading, erro } = useProdutos();
if (loading) return <p>Carregando...</p>;
if (erro) return <p>Deu ruim: {erro.message}</p>;
return (
<ul>
{produtos.map(p => <ProdutoCard key={p.id} produto={p} />)}
</ul>
);
}
Percebe como o componente ListaProdutos ficou mais limpo? Ele não sabe mais como os produtos são buscados. Ele só sabe que precisa de produtos, loading e erro — e o hook cuida do resto.
Sobre o "depende" que você mencionou — você tem razão, a resposta muitas vezes é essa. Mas deixa eu tentar destrinchar: depende do tamanho do projeto (em um projeto pequeno, não faz sentido criar uma arquitetura super elaborada); depende de quantas pessoas trabalham no código (se é só você, você consegue manter tudo na cabeça — se tem uma equipe, separar ajuda todo mundo); depende de quanto o projeto vai crescer (se você sabe que aquele MVP vai virar um produto grande, vale começar organizado); e depende da complexidade da lógica (se envolve só mostrar uma lista, talvez não precise de tanta separação — se envolve autenticação, cache, retry automático, aí sim vale extrair).
Uma forma de pensar que me ajuda: quando eu olho pra um componente e fico em dúvida, eu me pergunto "se eu precisasse testar esse componente, o que eu testaria?". Se a resposta é "eu testaria se ele renderiza corretamente dado certas props", então ele provavelmente deveria ser um dumb component. Se a resposta é "eu testaria se ele busca os dados certos e trata os erros direito", então a lógica deveria estar em outro lugar (um hook, um service) que pode ser testada separadamente.
Espero que tenha clareado! E não se preocupa em acertar de primeira — eu mesmo já refatorei código dezenas de vezes porque comecei com tudo junto e depois fui separando conforme o projeto crescia. Faz parte. O importante é ir desenvolvendo esse senso de "isso aqui tá ficando grande demais, preciso separar", e isso vem com a prática.