Nai-publish noong

Paalam React, Paalam useEffect (Sana)

Mga May-akda

Sa artikulong ito, ipapakita ko sa iyo kung paano gamitin ang React para palitan ang useEffect sa karamihan ng mga kaso.

Pinapanood ko ang "Goodbye, useEffect" ni David Khoursid, at 🤯 nakakatuwa talaga ito sa 😀 magandang paraan. Sang-ayon ako na ang useEffect ay ginagamit nang labis kaya nagiging marumi ang ating code at mahirap panatilihin. Matagal ko nang ginagamit ang useEffect, at may kasalanan ako sa hindi tamang paggamit nito. Sigurado ako na ang React ay may mga tampok na gagawing mas malinis at mas madaling mapanatili ang aking code.

Ano ang useEffect?

Ang useEffect ay isang hook na nagbibigay-daan sa amin na magsagawa ng mga side effect sa mga function component. Pinagsasama nito ang componentDidMount, componentDidUpdate, at componentWillUnmount sa isang solong API. Ito ay isang nakakaakit na hook na magbibigay-daan sa atin na gumawa ng maraming bagay. Ngunit ito rin ay isang napaka-mapanganib na hook na maaaring magdulot ng maraming bug.

Bakit mapanganib ang useEffect?

Tingnan natin ang sumusunod na halimbawa:

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

Ito ay isang simpleng counter na tumataas bawat segundo. Ginagamit nito ang useEffect upang magtakda ng isang interval. Ginagamit din nito ang useEffect upang tanggalin ang interval kapag ang component ay nawala. Ang code snippet sa itaas ay isang malawak na ginagamit na kaso para sa useEffect. Ito ay isang simpleng halimbawa, ngunit ito rin ay isang nakakatakot na halimbawa.

Ang problema sa halimbawang ito ay ang interval ay itinatakda sa tuwing ang component ay muling nag-render. Kung ang component ay muling nag-render dahil sa anumang dahilan, ang interval ay itatakda muli. Ang interval ay tatawagin ng dalawang beses bawat segundo. Hindi ito problema sa simpleng halimbawang ito, ngunit maaari itong maging isang malaking problema kapag ang interval ay mas kumplikado. Maaari rin itong magdulot ng memory leak.

Paano ito ayusin?

Maraming paraan upang ayusin ang problemang ito. Ang isang paraan ay ang paggamit ng useRef upang mag-imbak ng interval.

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

Ang code sa itaas ay mas mahusay kaysa sa nakaraang halimbawa. Hindi nito itinatakda ang interval sa tuwing ang component ay muling nag-render. Ngunit kailangan pa rin itong mapabuti. Ito ay medyo kumplikado pa rin. At ginagamit pa rin nito ang useEffect, na isang napaka-mapanganib na hook.

Ang useEffect ay hindi para sa mga effects

Tulad ng alam natin tungkol sa useEffect, pinagsasama nito ang componentDidMount, componentDidUpdate, at componentWillUnmount sa isang solong API. Magbigay tayo ng ilang halimbawa nito:

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

Madaling maunawaan. Ang useEffect ay ginagamit upang magsagawa ng mga side effect kapag ang component ay na-mount, na-update, at nawala. Ngunit hindi lamang ito ginagamit upang magsagawa ng mga side effect. Ginagamit din ito upang magsagawa ng mga side effect kapag ang component ay muling nag-render. Hindi magandang ideya na magsagawa ng mga side effect kapag ang component ay muling nag-render. Maaari itong magdulot ng maraming bug. Mas mainam na gumamit ng ibang mga hook upang magsagawa ng mga side effect kapag ang component ay muling nag-render.

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

Ang useEffect ay hindi isang state setter

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

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

  // Katulad ng componentDidMount at componentDidUpdate:
  useEffect(() => {
    // I-update ang document title gamit ang browser API
    document.title = `You clicked ${count} times`
  }) // <-- ito ang problema, 😱 nawawala ang dependency array

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

Inirerekomenda kong basahin ang dokumentasyong ito: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Imperative vs Declarative

Imperative: Kapag may nangyari, isagawa ang effect na ito.

Declarative: Kapag may nangyari, magdudulot ito ng pagbabago sa estado at depende (dependency array) sa kung aling bahagi ng estado ang nagbago, ang effect na ito ay dapat isagawa, ngunit kung may kundisyon lamang. At maaaring ulit itong isagawa ng React para sa walang dahilan concurrent rendering.

Konsepto vs Implementasyon

Konsepto:

useEffect(() => {
  doSomething()

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

Implementasyon:

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

  // oops, nakalimutan ko ang cleanup
}, [foo, bar, baz, quo])

Real-world implementation:

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

Nakakatakot isulat ang code na ito. Bukod dito, magiging normal ito sa ating codebase at magugulo. 😱🤮

Saan napupunta ang mga effects?

Ang React 18 ay nagpapatakbo ng mga effects ng dalawang beses sa pag-mount (sa strict mode). Mount/effect (╯°□°)╯︵ ┻━┻ -> Unmount (simulated)/cleanup ┬─┬ /( º _ º /) -> Remount/effect (╯°□°)╯︵ ┻━┻

Dapat ba itong ilagay sa labas ng component? Ang default na useEffect? Uh... awkward. Hmm... 🤔 Hindi natin ito mailagay sa render dahil dapat walang mga side-effects doon dahil ang render ay parang kanang kamay ng isang mathematical equation. Dapat itong resulta lamang ng pagkalkula.

Para saan ang useEffect?

Synchronization

useEffect(() => {
  const sub = createThing(input).subscribe((value) => {
    // gumawa ng isang bagay gamit ang 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)
  }
}, [])

Action effects vs Activity effects

 Fire-and-forget            Synchronized
 (Action effects)        (Activity effects)

        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-------------------------------------------------------------------------------->
                                       Unmount      Remount

Saan napupunta ang mga action effects?

Event handlers. Sorta.

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

May magandang impormasyon sa Beta React.js. Inirerekomenda kong basahin ito. Lalo na ang "Maaari bang magkaroon ng side effects ang mga event handler?" na bahagi.

Syempre! Ang mga event handler ay ang pinakamagandang lugar para sa mga side effects.

Ang isa pang magandang mapagkukunan na nais kong banggitin ay ang Kung saan mo maaaring magdulot ng mga side effects

Sa React, ang mga side effects ay karaniwang naroroon sa loob ng mga event handler.

Kung naubos mo na ang lahat ng iba pang opsyon at hindi mo mahanap ang tamang event handler para sa iyong side effect, maaari mo pa ring ikabit ito sa iyong naibalik na JSX gamit ang isang useEffect call sa iyong component. Sinabi nito sa React na isagawa ito mamaya, pagkatapos ng rendering, kung saan pinapayagan ang mga side effect. Gayunpaman, ang approach na ito ay dapat na ang iyong huling paraan.

"Ang mga effects ay nangyayari sa labas ng rendering" - David Khoursid.

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

Ang UI ay isang function ng estado. Habang na-render ang lahat ng kasalukuyang estado, magbubunga ito ng kasalukuyang UI. Gayundin, kapag may nangyaring event, magbubunga ito ng bagong estado. At kapag nagbago ang estado, magbubuo ito ng bagong UI. Ang paradigm na ito ang core ng React.

Kailan nangyayari ang mga effects?

Middleware? 🕵️ Callbacks? 🤙 Sagas? 🧙‍♂️ Reactions? 🧪 Sinks? 🚰 Monads(?) 🧙‍♂️ Kailan man? 🤷‍♂️

Mga pagbabago sa estado. Lagi.

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

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

Saan napupunta ang mga action effects? Event handlers. Mga pagbabago sa estado.

Na nagkataong isinasagawa sa parehong oras ng event handlers.

Maaaring Hindi Na Namin Kailangan ang isang Effects

Maaari nating gamitin ang useEffect dahil hindi natin alam na mayroon nang built-in na API mula sa React na maaaring malutas ang problemang ito.

Narito ang isang magandang mapagkukunan upang mabasa tungkol sa paksang ito: Maaaring Hindi Mo Kailangan ang isang Effect

Hindi natin kailangan ang useEffect para sa pagbabago ng data.

useEffect ➡️ useMemo (kahit na hindi natin kailangan ang useMemo sa karamihan ng mga kaso)

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

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

  // ...
}

Basahin at isipin muli ito nang mabuti 🧐.

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

  // ...
}

Sa halip na gamitin ang useEffect upang kalkulahin ang kabuuan, maaari nating gamitin ang useMemo upang i-memoize ang kabuuan. Kahit na ang variable ay hindi isang mahal na kalkulasyon, hindi na natin kailangang gamitin ang useMemo upang i-memoize ito dahil tayo ay nagpapalitan lamang ng pagganap para sa memorya.

Sa tuwing nakikita natin ang setState sa useEffect, ito ay isang babala na maaari nating gawing simple ito.

Effects gamit ang mga panlabas na store? useSyncExternalStore

useEffect ➡️ useSyncExternalStore

❌ Maling paraan:

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

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

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

  // ...
}

✅ Pinakamagandang paraan:

const Store = () => {
  const isConnected = useSyncExternalStore(
    // 👇 mag-subscribe
    storeApi.subscribe,
    // 👇 kumuha ng snapshot
    () => storeApi.getStatus() === 'connected',
    // 👇 kumuha ng server snapshot
    true
  )

  // ...
}

Hindi natin kailangan ang useEffect para sa pakikipag-usap sa mga magulang.

useEffect ➡️ eventHandler

❌ Maling paraan:

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

📈 Mas magandang paraan:

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

✅ Ang pinakamagandang paraan ay ang paglikha ng isang custom 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}
      >
        Toggle quick view
      </button>
    </div>
  )
}

Hindi natin kailangan ang useEft para sa pag-initialize ng mga global singleton.

useEffect ➡️ justCallIt

❌ Maling paraan:

const Store = () => {
  useEffect(() => {
    storeApi.authenticate() // 👈 Ito ay tatakbo ng dalawang beses!
  }, [])

  // ...
}

🔨 Ayusin natin:

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

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

    storeApi.authenticate()

    didAuthenticateRef.current = true
  }, [])

  // ...
}

➿ Isa pang paraan:

let didAuthenticate = false

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

    storeApi.authenticate()

    didAuthenticate = true
  }, [])

  // ...
}

🤔 Paano kung:

storeApi.authenticate()

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

🍷 SSR, huh?

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

🧪 Pagsusulit?

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

  appRoot.render(<Store />)
}

Hindi natin kailangang ilagay ang lahat sa loob ng isang component.

Hindi natin kailangan ang useEffect para sa pagkuha ng data.

useEffect ➡️ renderAsYouFetch (SSR) o useSWR (CSR)

❌ Maling paraan:

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

  useEffect(() => {
    let isCanceled = false

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

      setItems(data)
    })

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

  // ...
}

💽 Paraan ng 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) gamit ang async/await sa Server Component way:

// app/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/...')
  // Ang return value ay *hindi* na-serialize
  // Maaari kang magbalik ng Date, Map, Set, atbp.

  // Rekomendasyon: hawakan ang mga error
  if (!res.ok) {
    // Ito ay mag-a-activate ng pinakamalapit na `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) gamit ang useSWR sa Client Component way:

// 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) sa SSR way:

// 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) sa CSR way:

// 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 (SSR way:

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)

  // ...
}

⁉️ Talaga ⁉️ Ano ang dapat nating gamitin? useEffect? useQuery? useSWR?

o... use() 🤔

Ang use() ay isang bagong function ng React na tumatanggap ng isang pangako na kahawig ng await. Ang use() ay humahawak sa pangakong ibinabalik ng isang function sa isang paraan na tugma sa mga component, hook, at Suspense. Matuto nang higit pa tungkol sa use() sa React RFC.

function Note({ id }) {
  // Ito ay nagkuha ng isang tala nang asynchronously, ngunit para sa author ng component, mukhang isang synchronous operation.
  const note = use(fetchNote(id))
  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
    </div>
  )
}

Mga problema sa pagkuha sa useEffect

🏃‍♂️ Mga race condition

🔙 Walang instant na back button

🔍 Walang SSR o paunang nilalaman ng HTML

🌊 Paghabol sa talon

  • Reddit, Dan Abramov

Konklusyon

Mula sa pagkuha ng data hanggang sa pakikipaglaban sa mga imperative API, ang mga side effects ay isa sa pinakamahalagang pinagmumulan ng pagkabigo sa web app development. At maging tapat tayo, ang paglalagay ng lahat sa useEffect hook ay tumutulong lamang ng kaunti. Pasasalamat, mayroong isang agham (mabuti, matematika) sa mga side effects, na pormal na naisaayos sa mga state machine at statecharts, na maaaring makatulong sa atin na biswal na modelo at maunawaan kung paano orkestrahin ang mga effects, gaano man ka-kumplikado ang mga ito nang deklaratibo.

Mga Mapagkukunan