Neste artigo, mostrarei como usar React para substituir useEffect na maioria dos casos.
Eu tenho assistido "Goodbye, useEffect" de David Khoursid , e isso 🤯 me impressiona de uma forma 😀 boa. Concordo que useEffect tem sido tão usado que torna nosso código sujo e difícil de manter. Eu uso useEffect há muito tempo e sou culpado por usá-lo mal. Tenho certeza de que o React possui recursos que tornarão meu código mais limpo e fácil de manter.
useEffect é um gancho que nos permite executar efeitos colaterais em componentes de função. Ele combina componentDidMount, componentDidUpdate e componentWillUnmount em uma única API. É um gancho convincente que nos permitirá fazer muitas coisas. Mas também é um gancho muito perigoso que pode causar muitos bugs.
Vejamos o 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 generalizado para useEffect.
É um exemplo direto, mas também é um exemplo terrível.
O problema com este exemplo é que o intervalo é definido toda vez que o componente é renderizado novamente. Se o componente renderizar 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 corrigi-lo?
Há muitas maneiras de corrigir esse problema. Uma maneira é 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 do que o exemplo anterior. Ele não define o intervalo toda vez que o componente é renderizado novamente. Mas ainda precisa de melhorias. Ainda é um pouco complicado. E ainda usa useEffect, que é um gancho muito perigoso.
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 não é usado apenas para realizar efeitos colaterais. Também é usado para executar efeitos colaterais quando o componente é renderizado novamente. Não é uma boa ideia executar efeitos colaterais quando o componente for renderizado novamente. Pode causar muitos bugs. É melhor usar outros ganchos para executar efeitos colaterais quando o componente for renderizado novamente.
useEffect não é um gancho de ciclo de vida.
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>Number of changes: {count}</div> </div> ) }
useEffect não é um configurador de estado
import React, { useState, useEffect } from 'react' const Example = () => { const [count, setCount] = useState(0) // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times` }) // <-- this is the problem, 😱 it's missing the dependency array return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ) }
Recomendo a leitura desta documentação:https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Imperativo : Quando algo acontecer, execute este efeito.
Declarativo : Quando algo acontecer, fará com que o estado mude e dependendo (array de dependência) de quais partes do estado mudaram, esse efeito deve ser executado, mas somente se alguma condição for verdadeira. E o React pode executá-lo novamente sem motivo de renderização simultânea.
Conceito :
useEffect(() => { doSomething() return () => cleanup() }, [whenThisChanges])
Implementação :
useEffect(() => { if (foo && bar && (baz || quo)) { doSomething() } else { doSomethingElse() } // oops, I forgot the cleanup }, [foo, bar, baz, quo])
Implementação no 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 confuso. 😱🤮
React 18 executa efeitos duas vezes na montaria (no modo estrito). Montar/efeito (╯°□°)╯︵ ┻━┻ -> Desmontar (simulado)/limpeza ┬─┬ /( º _ º /) -> Remontar/efeito (╯°□°)╯︵ ┻━┻
Deve ser colocado fora do componente? O padrão useEffect? Uh... estranho. Hmm... 🤔 Não poderíamos colocá-lo em renderização, pois não deveria haver efeitos colaterais, porque a renderização é como a mão direita de uma equação matemática. Deve ser apenas o resultado do cálculo.
Sincronização
useEffect(() => { const sub = createThing(input).subscribe((value) => { // do something with value }) 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) } }, [])
Fire-and-forget Synchronized (Action effects) (Activity effects) 0 ---------------------- ----------------- - - - oo | A | oo | A | A oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo V | V oo V | V | o--------------------------------------------------------------------------------> Unmount Remount
Manipuladores de eventos. Sorta.
<form onSubmit={(event) => { // 💥 side-effect! submitData(event) }} > {/* ... */} </form>
Há informações excelentes em Beta React.js. Recomendo a leitura. Especialmente o "Os manipuladores de eventos podem ter efeitos colaterais?" parte .
Absolutamente! <u>Manipuladores de eventos são o melhor lugar para efeitos colaterais.</u>
Outro grande recurso que quero mencionar é Onde você pode causar efeitos colaterais
No React, <u>os efeitos colaterais geralmente pertencem aos manipuladores de eventos.</u>
Se você esgotou todas as outras opções e não consegue encontrar o manipulador de eventos correto para seu efeito colateral, você ainda pode anexá-lo ao seu JSX retornado com uma chamada <u>useEffect</u> em seu componente. Isso diz ao React para executá-lo mais tarde, após a renderização, quando os efeitos colaterais são permitidos. <u> No entanto, essa abordagem deve ser seu último recurso. </u>
"Os efeitos acontecem fora da renderização" - David Khoursid.
(state) => UI (state, event) => nextState // 🤔 Effects?
A interface do usuário é uma função do estado. Como todos os estados atuais são renderizados, ele produzirá a interface do usuário atual. Da mesma forma, quando um evento acontece, ele cria um novo estado. E quando o estado mudar, ele criará uma nova interface do usuário. Este paradigma é o núcleo do React.
Middleware? 🕵️ Chamadas de retorno? 🤙 Sagas? 🧙♂️ Reações? 🧪 Pias? 🚰 Mônadas(?) 🧙♂️ Sempre? 🤷♂️
Transições de estado. Sempre.
(state, event) => nextState | V (state, event) => (nextState, effect) // Here
Para onde vão os efeitos de ação? Manipuladores de eventos. Transições de estado.
Que são executados ao mesmo tempo que os manipuladores de eventos.
Poderíamos usar useEffect porque não sabemos se já existe uma API integrada do React que pode resolver esse problema.
Aqui está um excelente recurso para ler sobre este tópico: Você pode não precisar de um efeito
useEffect ➡️ useMemo (mesmo que 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 novamente com atenção 🧐.
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.
useEffect ➡️ useSyncExternalStore
❌ Caminho errado:
const Store = () => { const [isConnected, setIsConnected] = useState(true) useEffect(() => { const sub = storeApi.subscribe(({ status }) => { setIsConnected(status === 'connected') }) return () => { sub.unsubscribe() } }, []) // ... }
✅ Melhor forma:
const Store = () => { const isConnected = useSyncExternalStore( // 👇 subscribe storeApi.subscribe, // 👇 get snapshot () => storeApi.getStatus() === 'connected', // 👇 get server snapshot true ) // ... }
useEffect ➡️ eventHandler
❌ Caminho errado:
const ChildProduct = ({ onOpen, onClose }) => { const [isOpen, setIsOpen] = useState(false) useEffect(() => { if (isOpen) { onOpen() } else { onClose() } }, [isOpen]) return ( <div> <button onClick={() => { setIsOpen(!isOpen) }} > Toggle quick view </button> </div> ) }
📈 Melhor maneira:
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={} > Toggle quick view </button> </div> ) }
✅ A melhor maneira é criar um gancho 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} > Toggle quick view </button> </div> ) }
useEffect ➡️ justCallIt
❌ Caminho errado:
const Store = () => { useEffect(() => { storeApi.authenticate() // 👈 This will run twice! }, []) // ... }
🔨 Vamos corrigir:
const Store = () => { const didAuthenticateRef = useRef() useEffect(() => { if (didAuthenticateRef.current) return storeApi.authenticate() didAuthenticateRef.current = true }, []) // ... }
➿ Outra maneira:
let didAuthenticate = false const Store = () => { useEffect(() => { if (didAuthenticate) return storeApi.authenticate() didAuthenticate = true }, []) // ... }
🤔 E se:
storeApi.authenticate() const Store = () => { // ... }
🍷 SSR, né?
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 ➡️ renderAsYouFetch (SSR) ou useSWR (CSR)
❌ Caminho 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 no modo Server Component:
// app/page.tsx async function getData() { const res = await fetch('https://api.example.com/...') // The return value is *not* serialized // You can return Date, Map, Set, etc. // Recommendation: handle errors if (!res.ok) { // This will activate the closest `error.js` Error Boundary throw new Error('Failed to fetch data') } return res.json() } export default async function Page() { const data = await getData() return <main></main> }
⏭️💁 Next.js (appDir) com useSWR no modo Client Component:
// app/page.tsx import useSWR from 'swr' export default function Page() { const { data, error } = useSWR('/api/data', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data}!</div> }
⏭️🧹 Next.js (pagesDir) no modo 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>hello {data}!</div> }
⏭️💁 Next.js (pagesDir) no modo CSR:
// pages/index.tsx import useSWR from 'swr' export default function Page() { const { data, error } = useSWR('/api/data', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data}!</div> }
🍃 React Query (forma SSR:
import { getItems } from './storeApi' import { useQuery } from 'react-query' const Store = () => { const queryClient = useQueryClient() return ( <button onClick={() => { queryClient.prefetchQuery('items', getItems) }} > See items </button> ) } const Items = () => { const { data, isLoading, isError } = useQuery('items', getItems) // ... }
⁉️ Realmente ⁉️
O que devemos usar? usarEfeito? useQuery? usarSWR?
ou... apenas use() 🤔
use() é uma nova função React que aceita uma promessa conceitualmente semelhante a await. use() lida com a promessa retornada por uma função de maneira compatível com componentes, ganchos e Suspense. Saiba mais sobre use() no React RFC.
function Note({ id }) { // This fetches a note asynchronously, but to the component author, it looks // like a synchronous operation. const note = use(fetchNote(id)) return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> </div> ) }
🏃♂️ Condições de corrida
🔙 Sem botão de retorno instantâneo
🔍 Nenhum SSR ou conteúdo HTML inicial
🌊 Perseguindo a cachoeira
- Reddit, Dan Abramov
Da busca de dados à luta com APIs imperativas, os efeitos colaterais são uma das fontes mais significativas de frustração no desenvolvimento de aplicativos da web. E sejamos honestos, colocar tudo em usoEffect hooks só ajuda um pouco. Felizmente, existe uma ciência (bem, matemática) para efeitos colaterais, formalizada em máquinas de estado e gráficos de estado, que pode nos ajudar a modelar visualmente e entender como orquestrar efeitos, não importa o quão complexos eles se tornem declarativamente.