Objavljeno

Pozdrav React, Pozdrav useEffect (Inšallah)

Autori

Kako zamijeniti useEffect u Reactu u većini slučajeva

U ovom članku pokazat ću vam kako koristiti React za zamjenu useEffect u većini slučajeva.

Gledao sam "Zbogom, useEffect" od Davida Khoursida, i to mi je 🤯 razvalilo mozak na 😀 dobar način. Slažem se da se useEffect toliko koristi da čini naš kod prljavim i teškim za održavanje. Dugo sam koristio useEffect, i kriv sam što sam ga zloupotrebljavao. Siguran sam da React ima funkcije koje će učiniti moj kod čistijim i lakšim za održavanje.

Što je useEffect?

useEffect je hook koji nam omogućuje izvođenje nuspojava u funkcijskim komponentama. Spaja componentDidMount, componentDidUpdate i componentWillUnmount u jedan API. To je uvjerljiv hook koji će nam omogućiti da radimo mnogo toga. Ali je i vrlo opasan hook koji može uzrokovati puno grešaka.

Zašto je useEffect opasan?

Pogledajmo sljedeći primjer:

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

To je jednostavan brojač koji se povećava svake sekunde. Koristi useEffect da postavi interval. Također koristi useEffect da očisti interval kada se komponenta razmontira. Gornji komad koda je čest slučaj upotrebe za useEffect. To je jednostavan primjer, ali je i užasan primjer.

Problem s ovim primjerom je taj što se interval postavlja svaki put kada se komponenta ponovo renderira. Ako se komponenta ponovo renderira iz bilo kojeg razloga, interval će se ponovno postaviti. Interval će se pozivati dvaput u sekundi. To nije problem s ovim jednostavnim primjerom, ali može biti veliki problem kada je interval složeniji. Također može uzrokovati curenje memorije.

Kako to popraviti?

Postoji mnogo načina da se popravi ovaj problem. Jedan način je da se koristi useRef za pohranu intervala.

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

Gornji kod je puno bolji od prethodnog primjera. Ne postavlja interval svaki put kada se komponenta ponovo renderira. Ali ipak treba poboljšanja. I dalje je malo kompliciran. I dalje koristi useEffect, što je vrlo opasan hook.

useEffect nije za efekte

Kako znamo o useEffect, on kombinira componentDidMount, componentDidUpdate i componentWillUnmount u jedan API. Dajmo neke primjere:

useEffect(() => {
  // componentDidMount?
}, [])
useEffect(() => {
  // componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
  return () => {
    // componentWillUnmount?
  }
}, [])

To je lako razumjeti. useEffect se koristi za izvođenje nuspojava kada se komponenta montira, ažurira i razmontira. Ali ne koristi se samo za izvođenje nuspojava. Također se koristi za izvođenje nuspojava kada se komponenta ponovo renderira. Nije dobra ideja izvoditi nuspojave kada se komponenta ponovo renderira. To može uzrokovati puno grešaka. Bolje je koristiti druge hookove za izvođenje nuspojava kada se komponenta ponovo renderira.

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>Broj promjena: {count}</div>
    </div>
  )
}

useEffect nije postavitelj stanja

import React, { useState, useEffect } from 'react'

const Example = () => {
  const [count, setCount] = useState(0)

  // Slično componentDidMount i componentDidUpdate:
  useEffect(() => {
    // Ažurirajte naslov dokumenta pomoću API-ja preglednika
    document.title = `Kliknuli ste ${count} puta`
  }) // <-- ovo je problem, 😱 nedostaje niz ovisnosti

  return (
    <div>
      <p>Kliknuli ste {count} puta</p>
      <button onClick={() => setCount(count + 1)}>Klikni me</button>
    </div>
  )
}

Preporučujem čitanje ove dokumentacije: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Imperativno vs Deklarativno

Imperativno: Kada se nešto dogodi, izvršite ovaj efekt.

Deklarativno: Kada se nešto dogodi, uzrokovat će promjenu stanja i ovisno (niz ovisnosti) o tome koji su dijelovi stanja promijenjeni, ovaj efekt bi trebao biti izvršen, ali samo ako je ispunjen neki uvjet. I React ga može ponovo izvršiti bez razloga zbog istodobnog renderiranja.

Koncept vs Implementacija

Koncept:

useEffect(() => {
  doSomething()

  return () => cleanup()
}, [whenThisChanges])

Implementacija:

useEffect(() => {
  if (foo && bar && (baz || quo)) {
    doSomething()
  } else {
    doSomethingElse()
  }

  // uh, zaboravio sam čišćenje
}, [foo, bar, baz, quo])

Implementacija u stvarnom svijetu:

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

Strašno je pisati ovaj kod. Štoviše, to će biti normalno u našoj bazi koda i pomiješano. 😱🤮

Kuda idu efekti?

React 18 izvršava efekte dva puta prilikom montiranja (u strogom načinu rada). Montiraj/efekt (╯°□°)╯︵ ┻━┻ -> Razmontiraj (simulirano)/čišćenje ┬─┬ /( º _ º /) -> Ponovo montiraj/efekt (╯°□°)╯︵ ┻━┻

Treba li ga smjestiti izvan komponente? Zadani useEffect? Uh... neugodno. Hmm... 🤔 Nismo ga mogli staviti u renderiranje jer bi trebao biti bez nuspojava jer je renderiranje samo kao desna strana matematičke jednadžbe. Trebao bi biti samo rezultat izračuna.

Za što je useEffect?

Sinkronizacija

useEffect(() => {
  const sub = createThing(input).subscribe((value) => {
    // učinite nešto s vrijednošću
  })

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

Efekti akcije vs Efekti aktivnosti

 Ispajanje i zaboravi            Sinkronizirano
 (Efekti akcije)        (Efekti aktivnosti)

        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-------------------------------------------------------------------------------->
                                       Razmontiraj      Ponovo montiraj

Kuda idu efekti akcije?

Rukovatelji događaja. Sorta.

<form
  onSubmit={(event) => {
    // 💥 nuspojava!
    submitData(event)
  }}
>
  {/* ... */}
</form>

Postoje izvrsne informacije u Beta React.js. Preporučujem vam da ga pročitate. Pogotovo "Mogu li rukovatelji događaja imati nuspojave?" dio.

Apsolutno! Rukovatelji događaja su najbolje mjesto za nuspojave.

Još jedan izvrstan resurs koji želim spomenuti je Gdje možete uzrokovati nuspojave

U Reactu, nuspojave obično pripadaju unutar rukovatelja događaja.

Ako ste iscrpili sve ostale mogućnosti i ne možete pronaći odgovarajućeg rukovatelja događaja za svoju nuspojavu, još uvijek ga možete priložiti svom vraćenom JSX-u pomoću poziva useEffect u svojoj komponenti. To govori Reactu da ga izvrši kasnije, nakon renderiranja, kada su dopuštene nuspojave. Međutim, ovaj pristup bi trebao biti vaše posljednje utočište.

"Efekti se događaju izvan renderiranja" - David Khoursid.

(state) => UI
(state, event) => nextState // 🤔 Efekti?

UI je funkcija stanja. Kako se renderiraju sva trenutna stanja, ona će proizvesti trenutni UI. Isto tako, kada se dogodi događaj, stvorit će novo stanje. I kada se stanje promijeni, izgradit će novi UI. Ovaj paradigam je jezgra React-a.

Kada se događaju efekti?

Middleware? 🕵️ Povratne pozive? 🤙 Sage? 🧙‍♂️ Reakcije? 🧪 Sudopera? 🚰 Monads(?) 🧙‍♂️ Kada god? 🤷‍♂️

Prijelazi stanja. Uvijek.

(state, event) => nextState
          |
          V
(state, event) => (nextState, effect) // Ovdje

![Ilustracija ponovnog renderiranja](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")

Kuda idu efekti akcije? Rukovatelji događaja. Prijelazi stanja.

Koje se događaju u isto vrijeme kao rukovatelji događaja.

Možda nam ne trebaju efekti

Mogli bismo koristiti useEffect jer ne znamo da već postoji ugrađeni API iz React-a koji može riješiti ovaj problem.

Evo izvrsnog resursa za čitanje o ovoj temi: Možda vam ne treba efekt

Ne trebamo useEffect za transformaciju podataka.

useEffect ➡️ useMemo (iako nam useMemo u većini slučajeva ne treba)

const Cart = () => {
  const [items, setItems] = useState([])
  const [total, setTotal] = useState(0)

  useEffect(() => {
    setTotal(items.reduce((total, item) => total + item.price, 0))
  }, [items])

  // ...
}

Pročitajte i ponovno pažljivo razmislite o tome 🧐.

const Cart = () => {
  const [items, setItems] = useState([])
  const total = useMemo(() => {
    return items.reduce((total, item) => total + item.price, 0)
  }, [items])

  // ...
}

Umjesto da se koristi useEffect za izračunavanje zbroja, možemo koristiti useMemo za memoriranje zbroja. Čak i ako varijabla nije skup izračun, ne moramo koristiti useMemo za njeno memoriranje jer zapravo mijenjamo performanse za memoriju.

Kada god vidimo setState u useEffect, to je znak upozorenja da ga možemo pojednostaviti.

Efekti s vanjskim spremištima? useSyncExternalStore

useEffect ➡️ useSyncExternalStore

❌ Krivi način:

const Store = () => {
  const [isConnected, setIsConnected] = useState(true)

  useEffect(() => {
    const sub = storeApi.subscribe(({ status }) => {
      setIsConnected(status === 'connected')
    })

    return () => {
      sub.unsubscribe()
    }
  }, [])

  // ...
}

✅ Najbolji način:

const Store = () => {
  const isConnected = useSyncExternalStore(
    // 👇 pretplati se
    storeApi.subscribe,
    // 👇 dohvati snimku
    () => storeApi.getStatus() === 'connected',
    // 👇 dohvati snimku s poslužitelja
    true
  )

  // ...
}

Ne trebamo useEffect za komuniciranje s roditeljima.

useEffect ➡️ rukovatelj događaja

❌ Krivi način:

const ChildProduct = ({ onOpen, onClose }) => {
  const [isOpen, setIsOpen] = useState(false)

  useEffect(() => {
    if (isOpen) {
      onOpen()
    } else {
      onClose()
    }
  }, [isOpen])

  return (
    <div>
      <button
        onClick={() => {
          setIsOpen(!isOpen)
        }}
      >
        Prebaci brzi pregled
      </button>
    </div>
  )
}

📈 Bolji način:

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={}
      >
        Prebaci brzi pregled
      </button>
    </div>
  )
}

✅ Najbolji način je da se stvori prilagođeni hook:

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}
      >
        Prebaci brzi pregled
      </button>
    </div>
  )
}

Ne trebamo useEft za inicijaliziranje globalnih singletona.

useEffect ➡️ samoPozoviGa

❌ Krivi način:

const Store = () => {
  useEffect(() => {
    storeApi.authenticate() // 👈 Ovo će se pokrenuti dvaput!
  }, [])

  // ...
}

🔨 Popravimo to:

const Store = () => {
  const didAuthenticateRef = useRef()

  useEffect(() => {
    if (didAuthenticateRef.current) return

    storeApi.authenticate()

    didAuthenticateRef.current = true
  }, [])

  // ...
}

➿ Drugi način:

let didAuthenticate = false

const Store = () => {
  useEffect(() => {
    if (didAuthenticate) return

    storeApi.authenticate()

    didAuthenticate = true
  }, [])

  // ...
}

🤔 Što ako:

storeApi.authenticate()

const Store = () => {
  // ...
}

🍷 SSR, zar ne?

if (typeof window !== 'undefined') {
  storeApi.authenticate()
}
const Store = () => {
  // ...
}

🧪 Testiranje?

const renderApp = () => {
  if (typeof window !== 'undefined') {
    storeApi.authenticate()
  }

  appRoot.render(<Store />)
}

Ne moramo sve smjestiti unutar komponente.

Ne trebamo useEffect za dohvaćanje podataka.

useEffect ➡️ renderirajKaoŠtoDohvaćaš (SSR) ili useSWR (CSR)

❌ Krivi način:

const Store = () => {
  const [items, setItems] = useState([])

  useEffect(() => {
    let isCanceled = false

    getItems().then((data) => {
      if (isCanceled) return

      setItems(data)
    })

    return () => {
      isCanceled = true
    }
  })

  // ...
}

💽 Način Remix-a:

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) s async/await u Server Component načinu rada:

// app/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/...')
  // Vraćena vrijednost se *ne* serijalizira
  // Možete vratiti Date, Map, Set, itd.

  // Preporuka: obradite pogreške
  if (!res.ok) {
    // Ovo će aktivirati najbliži `error.js` Error Boundary
    throw new Error('Nije moguće dohvatiti podatke')
  }

  return res.json()
}

export default async function Page() {
  const data = await getData()

  return <main></main>
}

⏭️💁 Next.js (appDir) s useSWR u Client Component načinu rada:

// app/page.tsx
import useSWR from 'swr'

export default function Page() {
  const { data, error } = useSWR('/api/data', fetcher)

  if (error) return <div>Došlo je do pogreške prilikom učitavanja</div>
  if (!data) return <div>Učitavanje...</div>

  return <div>Zdravo {data}!</div>
}

⏭️🧹 Next.js (pagesDir) u SSR načinu rada:

// 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>Zdravo {data}!</div>
}

⏭️💁 Next.js (pagesDir) u CSR načinu rada:

// pages/index.tsx
import useSWR from 'swr'

export default function Page() {
  const { data, error } = useSWR('/api/data', fetcher)

  if (error) return <div>Došlo je do pogreške prilikom učitavanja</div>
  if (!data) return <div>Učitavanje...</div>

  return <div>Zdravo {data}!</div>
}

🍃 React Query (SSR način rada:

import { getItems } from './storeApi'
import { useQuery } from 'react-query'

const Store = () => {
  const queryClient = useQueryClient()

  return (
    <button
      onClick={() => {
        queryClient.prefetchQuery('items', getItems)
      }}
    >
      Pogledaj stavke
    </button>
  )
}

const Items = () => {
  const { data, isLoading, isError } = useQuery('items', getItems)

  // ...
}

⁉️ Zaista ⁉️ Što bismo trebali koristiti? useEffect? useQuery? useSWR?

ili... samo use() 🤔

use() je nova React funkcija koja prihvaća obećanje konceptualno slično await. use() obrađuje obećanje vraćeno funkcijom na način koji je kompatibilan s komponentama, hookovima i Suspense. Saznajte više o use() u RFC-u za React.

function Note({ id }) {
  // Ovo dohvaća bilješku asinhrono, ali autoru komponente izgleda kao
  // sinkrona operacija.
  const note = use(fetchNote(id))
  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
    </div>
  )
}

Problemi s dohvaćanjem u useEffect

🏃‍♂️ Natjecanje

🔙 Nema trenutnog gumba za povratak

🔍 Nema SSR-a ili početnog HTML sadržaja

🌊 Potjera za slapovima

  • Reddit, Dan Abramov

Zaključak

Od dohvaćanja podataka do borbe s imperativnim API-jima, nuspojave su jedan od najvećih izvora frustracije u razvoju web aplikacija. I budimo iskreni, stavljanje svega u useEffect hookove samo malo pomaže. Srećom, postoji znanost (pa, matematika) za nuspojave, formalizirana u strojnom stanju i stanju, koja nam može pomoći da vizualno modeliramo i razumijemo kako orkestrirati efekte, bez obzira na to koliko su kompleksni, deklarativno.

Resursi