paint-brush
Como usar React para substituir useEffectpor@imamdev
3,449 leituras
3,449 leituras

Como usar React para substituir useEffect

por Imamuzzaki Abu Salam16m2022/12/31
Read on Terminal Reader

Muito longo; Para ler

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. Neste artigo, mostrarei como usar React para substituir useEffect na maioria dos casos.
featured image - Como usar React para substituir useEffect
Imamuzzaki Abu Salam HackerNoon profile picture

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.

O que é useEffect?

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.

Por que useEffect é perigoso?

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.

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 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 vs Declarativo

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 vs Implementação

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. 😱🤮

Para onde vão os efeitos?

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.

Para que serve o useEffect?

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) } }, [])

Efeitos de ação x efeitos de atividade

 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

Para onde vão os efeitos de ação?

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.

Quando os efeitos acontecem?

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 

Rerenderizar imagem de ilustração

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.

Podemos não precisar de efeitos

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

Não precisamos de useEffect para transformar dados.

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.

Efeitos com lojas externas? useSyncExternalStore

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 ) // ... }

Não precisamos de useEffect para nos comunicarmos com os pais.

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> ) }

Não precisamos de useEft para inicializar singletons globais.

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.

Não precisamos de useEffect para buscar dados.

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> ) }

Obtenção de problemas em useEffect

🏃‍♂️ Condições de corrida

🔙 Sem botão de retorno instantâneo

🔍 Nenhum SSR ou conteúdo HTML inicial

🌊 Perseguindo a cachoeira

  • Reddit, Dan Abramov

Conclusão

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.

Recursos