UseEffect, o hook mais mal utilizado
Porque o UseEffect é o hook mais mal utilizado
Inspiração
Seguindo o Dan Abramov no twitter acabo sabendo sempre que a "nova" (que já está sendo desenvolvida a anos) documentação do react é atualizada com um novo conteúdo, em uma dessas ocasiões resolvi ler o artigo You might not need an effect que fala sobre situações em que desenvolvedores acreditam erroneamente que um useEffect é a solução para um problema e qual a solução que deveriam adotar no lugar
Mal entendido
Não vou aqui listar todas as situações até porque não faria sentido simplesmente reproduzir um artigo oficial que qualquer um pode entrar e ler. Mas um conceito que me chamou atenção e que parece ir de encontro com a maior parte dos mal usos do hook é o de que useEffect é um hook para mudanças de estado que devem acontecer fora do siclo de render
React é um framework bastante versátil, que provê primitivos que te permitem encontrar sua própria solução para um problema. Mas embora ele te permita usar os primitivos de inumeras formas cada um foi criado com um uso em mente, e ao tentar usa-lo de uma forma diferentes a planejada você vai inevitavelmente estar se expondo a problemas que a ferramenta nunca se propôs a evitar
Funcionamento
Se dividirmos um componente em script e template (antes do retorno e depois do retorno) teremos o seguinte funcionamento do ciclo de render após a mudança de um estado:
script -> render -> Effects
Apesar de serem escritos dentro da parte do script os effects são executados apenas após o render, não apenas isso mas quando algum estado é modificado um novo render é forçado.
Mas ao mesmo tempo mudanças de estado acontecem em lote, então mesmo que vários estados sejam modificados apenas um novo render é forçado, mas isso apenas caso sejam modificados no mesmo siclo de render
Então caso você use esteja usando um effect para algo como sincronizar estados, está forçando ao menos 1 re-render a mais que o necessário.
script -> render -> Effects(estado modificado forçando outro ciclo de render) -> script -> render
Essa conta pode subir exponecialmente caso tenha outros effects com dependências de estado
Concerto
Um exemplo de uso do effect para sincronizar estados é por exemplo limpar uma lista caso um filtro seja modificado
const [filter, setFilter] = useState(0)
const [list, setList] = useState([])
useEffect(() => {
setList([])
}, [filter])
Para evitar que esse effect force renders desnecessariamente basta trazer o setter para dentro do ciclo de render
const [filter, setFilter] = useState(0)
const [list, setList] = useState([])
const [comparativeFilter, setComparativeFilter] = useState(filter)
if (filter !== comparativeFilter) {
setList([])
setComparativeFilter(filter)
}
A mesma lógica se aplica a limitar a quantidade de vezes que um bloco de lógica é executado
Ao invés de fazer
const {setTotalDeComponentesRenderizados} = useGlobalStore()
useEffect(() => {
setTotalDeComponentesRenderizados((prev) => prev + 1)
}, [])
Podemos simplesmente
const {setTotalDeComponentesRenderizados} = useGlobalStore()
const firstRender = useRef(true)
if (firstRender.current) {
setTotalDeComponentesRenderizados((prev) => prev + 1)
firstRender.current = false
}
Outro clássico uso errôneo do effect é para dar foco a inputs usando refs.
A prop de ref em um elemento assim como um effect sem dependências recebe um callback que é executado quando o elemento é montado e executa o retorno desse callback quando desmontado, então quando você passa uma ref para a prop ref
<input
ref={ref}
Basicamento o que está acontecendo é
<input
ref={(node) => {
ref.current = node
}}
Então se você quer interagir com um elemento quando o componente é montado ao invés de um effect
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return (
<input
ref={inputRef}
Basta usar uma callbackRef
const focus = useCallback((node) => {
node.focus()
}, [])
return (
<input
ref={focus}
Uso correto
O useEffect tem 2 principais usos:
- Ações que não interferem no DOM nem mudam estado (dependendo de estado ou não)
- Ações assíncronas que mudam estado ou que precisam de cleanup
No seguinte exemplo:
const [title, setTitle] = useState("Novo Título")
useEffect(() => {
window.title = title
}, [title])
Não existe motivo para algo como a mudança de título estar bloqueando o ciclo de render se não vai mudar nada no render em si nem forçar outro render, então o ideal é que seja escrito num effect, já que são executados de forma assíncrona fora do ciclo de render
Cleanup
Muitas pessoas não sabem mas um dos principais recursos do useEffect é que vc pode retornar um callback e ele será executado quando o componente for desmontado
Funções assíncronas correm o risco de serem resolvidas após o componente ter sido desmontado, por isso é importante que o resutado dessas funções sejam impedidos de tentar mudar estados que já não existem
const [posts, setPosts] = useEffect({})
useEffect(() => {
let componentIsMounted = true
const getPosts = async () => {
const res = await fetch(url)
const data = await res.json()
if (componentIsMounted) {
setPosts(data)
}
getPosts()
return () => {
componentIsMounted = false
}}, [])
O mesmo recurso de cleanup deve ser utilizado para funções cuja execução não é limitada pelo escopo do componente, como eventListners, timeOuts e Intervals
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}
updateState();
window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
Conclusão
Eu ainda pretendo fazer um post só falando disso mas os maiores motivos de toda essa confusão são:
- O fato de que por o React usar o vdom caso seja necessário interagir com o DOM é preciso escapar do react
- Os hooks num geral servem para escapar da reatividade, não optar por ela
No fim o conselho que fica é: Não usem React
Sério gente, hoje em dia existem vários frameworks por aí que:
- Os componentes são executados apenas 1x, optando pela reatividade ao invés de fugir dela
- São reativos de verdade mudando apenas a parte exata do dom que um estado é referenciado
- Não dependem de closures pra gerenciar estado
- Usam o DOM de verdade, permitindo o uso facilmente o uso de métodos do browser
Minha recomendação é o Svelte, que eu considero ser o único framework front-end realmente divertido de se usar
Além de resolver todos os pontos que falei acima ainda:
- Tem uma sintaxe de declaração e gerênciamento de estado super simples (se quer que um valor seja reativo use let, se que que o valor mude basta atribuir um novo valor a variável)
- Traz o css pra dentro do componente de forma escopada
- Facilita o uso de variáveis css baseadas em estado
- É compilado em javascript puro então não envia dependências em produção, é leve e rápido
- Tem o melhor meta framework (minha opnião)
- Dá suporte first class para animações e transições tanto com css como com javascript
- Consegue usar promises direto no template sem a necessidade de envelopar todo o componente num suspense
- Te permite optar por 2-way-data-bind podendo referenciar até mesmo Elementos do DOM e Componentes com variáveis