- Publicado em
Olá, React, olá useEffect (espero que seja assim)
- Autores
- Nome
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
useEffect
na maioria dos casos
React: Substituindo Neste artigo, mostrarei como usar React para substituir useEffect
na maioria dos casos.
Tenho visto o vídeo "Goodbye, useEffect" por David Khoursid, e ele 🤯 me deixou boquiaberto de um jeito 😀 bom. Concordo que useEffect
tem sido usado tanto que deixa nosso código sujo e difícil de manter. Eu uso useEffect
há muito tempo, e sou culpado de usá-lo de forma errada. Tenho certeza que React tem recursos que deixarão meu código mais limpo e fácil de manter.
useEffect
?
O que é useEffect
é um hook que nos permite executar efeitos colaterais em componentes de função. Ele combina componentDidMount
, componentDidUpdate
e componentWillUnmount
em uma única API. É um hook poderoso que nos permite fazer muitas coisas. Mas também é um hook muito perigoso que pode causar muitos bugs.
useEffect
é perigoso?
Por que Vamos dar uma olhada no seguinte exemplo:
import React, { useEffect } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return <div>{count}</div>
}
É um contador simples que aumenta a cada segundo. Ele usa useEffect
para definir um intervalo. Ele também usa useEffect
para limpar o intervalo quando o componente é desmontado. O trecho de código acima é um caso de uso comum para useEffect
. É um exemplo simples, mas também um exemplo terrível.
O problema com este exemplo é que o intervalo é definido a cada vez que o componente é renderizado novamente. Se o componente for renderizado novamente por qualquer motivo, o intervalo será definido novamente. O intervalo será chamado duas vezes por segundo. Não é um problema com este exemplo simples, mas pode ser um grande problema quando o intervalo é mais complexo. Também pode causar vazamentos de memória.
Como consertar?
Existem muitas maneiras de corrigir este problema. Uma delas é usar useRef
para armazenar o intervalo.
import React, { useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const intervalRef = useRef()
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
return <div>{count}</div>
}
O código acima é muito melhor que o exemplo anterior. Ele não define o intervalo a cada vez que o componente é renderizado novamente. Mas ainda precisa de melhorias. Ainda é um pouco complicado. E ainda usa useEffect
, que é um hook muito perigoso.
useEffect
não é para efeitos
Como sabemos sobre useEffect
, ele combina componentDidMount
, componentDidUpdate
e componentWillUnmount
em uma única API. Vamos dar alguns exemplos disso:
useEffect(() => {
// componentDidMount?
}, [])
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, [])
É fácil de entender. useEffect
é usado para executar efeitos colaterais quando o componente é montado, atualizado e desmontado. Mas ele não é usado apenas para executar efeitos colaterais. Ele também é usado para executar efeitos colaterais quando o componente é renderizado novamente. Não é uma boa ideia executar efeitos colaterais quando o componente é renderizado novamente. Isso pode causar muitos bugs. É melhor usar outros hooks para executar efeitos colaterais quando o componente é renderizado novamente.
import React, { useState, useEffect } from 'react'
const Example = () => {
const [value, setValue] = useState('')
const [count, setCount] = useState(-1)
useEffect(() => {
setCount(count + 1)
})
const onChange = ({ target }) => setValue(target.value)
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>Número de alterações: {count}</div>
</div>
)
}
useEffect
não é um definidor de estado
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// Similar a componentDidMount e componentDidUpdate:
useEffect(() => {
// Atualiza o título do documento usando a API do navegador
document.title = `Você clicou ${count} vezes`
}) // <-- este é o problema, 😱 está faltando o array de dependências
return (
<div>
<p>Você clicou {count} vezes</p>
<button onClick={() => setCount(count + 1)}>Clique em mim</button>
</div>
)
}
Recomendo ler esta documentação: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Imperativo vs Declarativo
Imperativo: Quando algo acontece, execute este efeito.
Declarativo: Quando algo acontece, isso fará com que o estado mude e dependendo (array de dependências) de quais partes do estado mudaram, este efeito deve ser executado, mas apenas se alguma condição for verdadeira. E React pode executá-lo novamente por nenhum motivo renderização concorrente.
Conceito vs Implementação
Conceito:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Implementação:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// ops, esqueci a limpeza
}, [foo, bar, baz, quo])
Implementação do mundo real:
useEffect(() => {
if (isOpen && component && containerElRef.current) {
if (React.isValidElement(component)) {
ionContext.addOverlay(overlayId, component, containerElRef.current!);
} else {
const element = createElement(component as React.ComponentClass, componentProps);
ionContext.addOverlay(overlayId, element, containerElRef.current!);
}
}
}, [component, containerElRef.current, isOpen, componentProps]);
useEffect(() => {
if (removingValue && !hasValue && cssDisplayFlex) {
setCssDisplayFlex(false)
}
setRemovingValue(false)
}, [removingValue, hasValue, cssDisplayFlex])
É assustador escrever este código. Além disso, será normal em nossa base de código e bagunçado. 😱🤮
Para onde vão os efeitos?
React 18 executa efeitos duas vezes na montagem (no modo estrito). Montar/efeito (╯°□°)╯︵ ┻━┻ -> Desmontar (simulado)/limpeza ┬─┬ /( º _ º /) -> Remontar/efeito (╯°□°)╯︵ ┻━┻
Deve ser colocado fora do componente? O useEffect
padrão? Uh... estranho. Hmm... 🤔 Não poderíamos colocá-lo no render, pois não deve haver efeitos colaterais lá, porque render é como o lado direito de uma equação matemática. Deve ser apenas o resultado do cálculo.
useEffect
?
Para que serve Sincronização
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// faça algo com o valor
})
return sub.unsubscribe
}, [input])
useEffect(() => {
const handler = (event) => {
setPointer({ x: event.clientX, y: event.clientY })
}
elRef.current.addEventListener('pointermove', handler)
return () => {
elRef.current.removeEventListener('pointermove', handler)
}
}, [])
Efeitos de ação vs Efeitos de atividade
Disparar e esquecer Sincronizado
(Efeitos de ação) (Efeitos de atividade)
0 ---------------------- ----------------- - - -
o o | A | o o | A | A
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o V | V o o V | V |
o-------------------------------------------------------------------------------->
Desmontar Remontar
Para onde vão os efeitos de ação?
Manipuladores de eventos. Mais ou menos.
<form
onSubmit={(event) => {
// 💥 efeito colateral!
submitData(event)
}}
>
{/* ... */}
</form>
Há uma excelente informação no Beta React.js. Recomendo ler. Especialmente a parte "Manipuladores de eventos podem ter efeitos colaterais?".
Absolutamente! Os manipuladores de eventos são o melhor lugar para efeitos colaterais.
Outro ótimo recurso que quero mencionar é Onde você pode causar efeitos colaterais
No React, efeitos colaterais geralmente pertencem a manipuladores de eventos.
Se você esgotou todas as outras opções e não consegue encontrar o manipulador de eventos certo para seu efeito colateral, ainda pode anexá-lo ao JSX retornado com uma chamada
useEffect
em seu componente. Isso informa ao React para executá-lo depois, após a renderização, quando efeitos colaterais forem permitidos. No entanto, esta abordagem deve ser seu último recurso.
"Efeitos acontecem fora da renderização" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 Efeitos?
UI é uma função do estado. Como todos os estados atuais são renderizados, ele produzirá a UI atual. Da mesma forma, quando um evento ocorre, ele cria um novo estado. E quando o estado muda, ele constrói uma nova UI. Este paradigma é o núcleo do React.
Quando os efeitos acontecem?
Transições de estado. Sempre.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Aqui
![Ilustração de renderização novamente](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
Para onde vão os efeitos de ação? Manipuladores de eventos. Transições de estado.
Que acontecem de ser executados ao mesmo tempo que manipuladores de eventos.
Podemos não precisar de um efeito
Poderíamos usar useEffect
porque não sabemos se já existe uma API integrada do React que pode resolver esse problema.
Aqui está um ótimo recurso para ler sobre esse tópico: Você pode não precisar de um efeito
useEffect
para transformar dados.
Não precisamos de useEffect
➡️ useMemo
(embora não precisemos de useMemo
na maioria dos casos)
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
Leia e pense sobre isso novamente com cuidado 🧐.
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
Em vez de usar useEffect
para calcular o total, podemos usar useMemo
para memorizar o total. Mesmo que a variável não seja um cálculo caro, não precisamos usar useMemo
para memorizá-la, porque estamos basicamente trocando desempenho por memória.
Sempre que vemos setState
em useEffect
, é um sinal de alerta de que podemos simplificá-lo.
useSyncExternalStore
Efeitos com lojas externas?useEffect
➡️ useSyncExternalStore
❌ Errado:
const Store = () => {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})
return () => {
sub.unsubscribe()
}
}, [])
// ...
}
✅ Melhor:
const Store = () => {
const isConnected = useSyncExternalStore(
// 👇 se inscrever
storeApi.subscribe,
// 👇 obter instantâneo
() => storeApi.getStatus() === 'connected',
// 👇 obter instantâneo do servidor
true
)
// ...
}
useEffect
para comunicar com os pais.
Não precisamos de useEffect
➡️ eventHandler
❌ Errado:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])
return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Alternar visualização rápida
</button>
</div>
)
}
📈 Melhor:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return (
<div>
<button
onClick={}
>
Alternar visualização rápida
</button>
</div>
)
}
✅ O melhor é criar um hook personalizado:
const useToggle({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return [isOpen, handleToggle]
}
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, handleToggle] = useToggle({ onOpen, onClose })
return (
<div>
<button
onClick={handleToggle}
>
Alternar visualização rápida
</button>
</div>
)
}
useEft
para inicializar singletons globais.
Não precisamos de useEffect
➡️ justCallIt
❌ Errado:
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 Isso será executado duas vezes!
}, [])
// ...
}
🔨 Vamos consertar:
const Store = () => {
const didAuthenticateRef = useRef()
useEffect(() => {
if (didAuthenticateRef.current) return
storeApi.authenticate()
didAuthenticateRef.current = true
}, [])
// ...
}
➿ Outra forma:
let didAuthenticate = false
const Store = () => {
useEffect(() => {
if (didAuthenticate) return
storeApi.authenticate()
didAuthenticate = true
}, [])
// ...
}
🤔 E se:
storeApi.authenticate()
const Store = () => {
// ...
}
🍷 SSR, huh?
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
🧪 Testando?
const renderApp = () => {
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
appRoot.render(<Store />)
}
Não precisamos necessariamente colocar tudo dentro de um componente.
useEffect
para buscar dados.
Não precisamos de useEffect
➡️ renderAsYouFetch
(SSR) ou useSWR
(CSR)
❌ Errado:
const Store = () => {
const [items, setItems] = useState([])
useEffect(() => {
let isCanceled = false
getItems().then((data) => {
if (isCanceled) return
setItems(data)
})
return () => {
isCanceled = true
}
})
// ...
}
💽 Maneira do Remix:
import { useLoaderData } from '@renix-run/react'
import { json } from '@remix-run/node'
import { getItems } from './storeApi'
export const loader = async () => {
const items = await getItems()
return json(items)
}
const Store = () => {
const items = useLoaderData()
// ...
}
export default Store
⏭️🧹 Next.js (appDir) com async/await em Server Component:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// O valor de retorno *não* é serializado
// Você pode retornar Date, Map, Set, etc.
// Recomendação: tratar erros
if (!res.ok) {
// Isso ativará o `error.js` Error Boundary mais próximo
throw new Error('Falha ao buscar dados')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
⏭️💁 Next.js (appDir) com useSWR
em Client Component:
// app/page.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>falha ao carregar</div>
if (!data) return <div>carregando...</div>
return <div>olá {data}!</div>
}
⏭️🧹 Next.js (pagesDir) em SSR:
// pages/index.tsx
import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch('https://api.example.com/...')
const data = await res.json()
return {
props: {
data,
},
}
}
export default function Page({ data }) {
return <div>olá {data}!</div>
}
⏭️💁 Next.js (pagesDir) em CSR:
// pages/index.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>falha ao carregar</div>
if (!data) return <div>carregando...</div>
return <div>olá {data}!</div>
}
🍃 React Query (SSR):
import { getItems } from './storeApi'
import { useQuery } from 'react-query'
const Store = () => {
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
Ver itens
</button>
)
}
const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)
// ...
}
⁉️ Realmente ⁉️ O que devemos usar? useEffect? useQuery? useSWR?
ou... apenas use() 🤔
use() é uma nova função React que aceita uma promessa conceitualmente semelhante ao await. use() trata a promessa retornada por uma função de uma forma que é compatível com componentes, hooks e Suspense. Saiba mais sobre use() na RFC do React.
function Note({ id }) {
// Isso busca uma nota assincronamente, mas para o autor do componente, parece
// uma operação síncrona.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
useEffect
Problemas de busca em 🏃♂️ Condições de corrida
🔙 Sem botão de voltar instantâneo
🔍 Sem SSR ou conteúdo HTML inicial
🌊 Perseguindo cachoeira
- Reddit, Dan Abramov
Conclusão
De buscar dados a lutar com APIs imperativas, efeitos colaterais são uma das maiores fontes de frustração no desenvolvimento de aplicativos web. E sejamos honestos, colocar tudo em hooks useEffect
só ajuda um pouco. Felizmente, há uma ciência (bem, matemática) para efeitos colaterais, formalizada em máquinas de estado e diagramas de estado, que podem nos ajudar a modelar e entender visualmente como orquestrar efeitos, não importa o quão complexos eles se tornem declarativamente.