منتشر شده در

سلام React, سلام useEffect (امیدوارم اینطور باشد)

نویسندگان

در این مقاله، به شما نشان خواهم داد که چگونه از React برای جایگزینی useEffect در اکثر موارد استفاده کنید.

من "Goodbye, useEffect" by David Khoursid را تماشا می‌کردم و 🤯 ذهنم را به طرز شگفت انگیزی 😀 به چالش کشید. با شما موافقم که useEffect به قدری زیاد استفاده شده است که کد ما را کثیف و نگهداری آن را سخت می‌کند. من مدت طولانی از useEffect استفاده می‌کردم و به استفاده نادرست از آن اعتراف می‌کنم. مطمئن هستم که React ویژگی‌هایی دارد که باعث می‌شود کد من تمیزتر و نگهداری آن آسان‌تر شود.

useEffect چیست؟

useEffect یک Hook است که به ما امکان می‌دهد عوارض جانبی را در کامپوننت‌های تابعی انجام دهیم. این Hook componentDidMount ، componentDidUpdate و componentWillUnmount را در یک API منحصر به فرد ترکیب می‌کند. این یک Hook قابل توجه است که به ما امکان می‌دهد کارهای زیادی انجام دهیم. اما این Hook همچنین بسیار خطرناک است و می‌تواند باعث ایجاد خطاهای زیادی شود.

چرا useEffect خطرناک است؟

به مثال زیر نگاهی بیندازید:

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

این یک شمارنده ساده است که هر ثانیه افزایش می‌یابد. از useEffect برای تنظیم یک interval استفاده می‌کند. همچنین از useEffect برای پاک کردن interval هنگامی که کامپوننت unmount می‌شود، استفاده می‌کند. کد بالا یک نمونه استفاده گسترده از useEffect است. این یک مثال ساده است، اما یک مثال وحشتناک است.

مشکل این مثال این است که interval هر بار که کامپوننت رندر می‌شود، تنظیم می‌شود. اگر کامپوننت به هر دلیلی رندر شود، interval دوباره تنظیم می‌شود. interval دو بار در ثانیه اجرا می‌شود. این مشکل در این مثال ساده مشکلی ایجاد نمی‌کند، اما هنگامی که interval پیچیده‌تر شود، می‌تواند مشکل بزرگی ایجاد کند. همچنین می‌تواند باعث ایجاد نشت حافظه شود.

چگونه آن را برطرف کنیم؟

راه‌های زیادی برای برطرف کردن این مشکل وجود دارد. یک راه استفاده از useRef برای ذخیره 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>
}

کد بالا خیلی بهتر از مثال قبلی است. interval را هر بار که کامپوننت رندر می‌شود، تنظیم نمی‌کند. اما هنوز نیاز به بهبود دارد. هنوز کمی پیچیده است. و هنوز از useEffect استفاده می‌کند که یک Hook بسیار خطرناک است.

useEffect برای عوارض جانبی نیست

همانطور که در مورد useEffect می‌دانیم، componentDidMount ، componentDidUpdate و componentWillUnmount را در یک API منحصر به فرد ترکیب می‌کند. بیایید چند نمونه از آن را بیاوریم:

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

درک آن ساده است. useEffect برای انجام عوارض جانبی هنگامی که کامپوننت mount ، update و unmount می‌شود، استفاده می‌شود. اما نه تنها برای انجام عوارض جانبی استفاده می‌شود. همچنین برای انجام عوارض جانبی هنگامی که کامپوننت re-renders می‌شود، استفاده می‌شود. ایده خوبی نیست که عوارض جانبی را هنگام re-renders انجام دهیم. این می‌تواند باعث ایجاد خطاهای زیادی شود. بهتر است از Hook های دیگر برای انجام عوارض جانبی هنگامی که کامپوننت re-renders می‌شود، استفاده کنیم.

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 یک state setter نیست

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

من خواندن این مستندات را توصیه می‌کنم: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Imperative vs Declarative

Imperative: هنگامی که اتفاقی می‌افتد، این effect را اجرا کنید.

Declarative: هنگامی که اتفاقی می‌افتد، باعث تغییر state می‌شود و بسته به (dependency array) قسمت‌هایی از state که تغییر کرده‌اند، این effect باید اجرا شود، اما فقط اگر شرایطی صدق کند. و React ممکن است دوباره آن را برای بدون دلیل رندر همزمان اجرا کند.

Concept vs Implementation

Concept:

useEffect(() => {
  doSomething()

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

Implementation:

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

  // oops, I forgot the 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])

نوشتن این کد ترسناک است. علاوه بر این، در کد ما معمولی خواهد بود و خراب می‌شود. 😱🤮

عوارض جانبی کجا می‌روند؟

React 18 عوارض جانبی را دو بار در mount (در حالت strict mode) اجرا می‌کند. Mount/effect (╯°□°)╯︵ ┻━┻ -> Unmount (simulated)/cleanup ┬─┬ /( º _ º /) -> Remount/effect (╯°□°)╯︵ ┻━┻

آیا باید در خارج از کامپوننت قرار گیرد؟ useEffect پیش فرض؟ اه... شرم آور. اوه... 🤔 نمی‌توانستیم آن را در render قرار دهیم زیرا باید بدون عوارض جانبی باشد زیرا render فقط مثل سمت راست یک معادله ریاضی است. باید فقط نتیجه محاسبه باشد.

useEffect برای چیست؟

Synchronization

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

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

Where do action effects go?

Event handlers. Sorta.

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

اطلاعات عالی در Beta React.js وجود دارد. من خواندن آن را توصیه می‌کنم. به ویژه "Can event handlers have side effects?" part.

مطمئنا! Event handlers بهترین مکان برای عوارض جانبی هستند.

یک منبع عالی دیگر که می‌خواهم به آن اشاره کنم، Where you can cause side effects است.

در React، عوارض جانبی معمولا در داخل Event handlers قرار می‌گیرند.

اگر همه گزینه‌های دیگر را مصرف کرده‌اید و نمی‌توانید Event handler مناسب را برای عوارض جانبی خود پیدا کنید، هنوز می‌توانید آن را با یک فراخوانی useEffect در کامپوننت خود به JSX بازگردانده شده خود متصل کنید. این به React می‌گوید که آن را بعدا، پس از رندر شدن، هنگامی که عوارض جانبی مجاز هستند، اجرا کند. ** با این حال، این رویکرد باید آخرین گزینه شما باشد. **

"Effects happen outside of rendering" - David Khoursid.

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

UI یک عملکرد از state است. همانطور که همه state های فعلی رندر می‌شوند، UI فعلی را تولید می‌کند. به همین ترتیب، هنگامی که یک event اتفاق می‌افتد، state جدیدی ایجاد می‌کند. و هنگامی که state تغییر می‌کند، UI جدیدی می‌سازد. این پارادایم هسته React است.

عوارض جانبی کی اتفاق می‌افتند؟

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

State transitions. Always.

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

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

عوارض جانبی کجا می‌روند؟ Event handlers. State transitions.

که اتفاقا در همان زمان با Event handlers. اجرا می‌شوند.

ممکن است به یک عوارض جانبی نیاز نداشته باشیم

ممکن است از useEffect استفاده کنیم زیرا نمی‌دانیم که قبلاً یک API ساخته شده در React وجود دارد که می‌تواند این مشکل را حل کند.

در اینجا یک منبع عالی برای خواندن در مورد این موضوع وجود دارد: You Might Not Need an Effect

ما نیازی به useEffect برای **تبدیل داده ** نداریم.

useEffect ➡️ useMemo (اگرچه در اکثر موارد به useMemo نیاز نداریم)

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

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

  // ...
}

دوباره با دقت 🧐 آن را بخوانید و فکر کنید.

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

  // ...
}

به جای استفاده از useEffect برای محاسبه total، می‌توانیم از useMemo برای memoize کردن total استفاده کنیم. حتی اگر متغیر یک محاسبه گران قیمت نیست، نیازی به استفاده از useMemo برای memoize کردن آن نداریم زیرا در واقع کارایی را با حافظه مبادله می‌کنیم.

هر زمان که setState را در useEffect می‌بینیم، یک علامت هشدار است که می‌توانیم آن را ساده‌تر کنیم.

عوارض جانبی با فروشگاه‌های خارجی؟ useSyncExternalStore

useEffect ➡️ useSyncExternalStore

❌ راه نادرست:

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

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

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

  // ...
}

✅ بهترین راه:

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

  // ...
}

ما نیازی به useEffect برای **ارتباط با والدین ** نداریم.

useEffect ➡️ eventHandler

❌ راه نادرست:

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

📈 راه بهتر:

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

✅ بهترین راه این است که یک 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>
  )
}

ما نیازی به useEft برای **اولیه سازی singletones جهانی ** نداریم.

useEffect ➡️ justCallIt

❌ راه نادرست:

const Store = () => {
  useEffect(() => {
    storeApi.authenticate() // 👈 This will run twice!
  }, [])

  // ...
}

🔨 بیایید آن را برطرف کنیم:

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

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

    storeApi.authenticate()

    didAuthenticateRef.current = true
  }, [])

  // ...
}

➿ راه دیگر:

let didAuthenticate = false

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

    storeApi.authenticate()

    didAuthenticate = true
  }, [])

  // ...
}

🤔 چطور اگر:

storeApi.authenticate()

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

🍷 SSR، اوه؟

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

🧪 تست؟

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

  appRoot.render(<Store />)
}

لزوماً نیازی نیست که همه چیز را در داخل یک کامپوننت قرار دهیم.

ما نیازی به useEffect برای **دریافت داده ** نداریم.

useEffect ➡️ renderAsYouFetch (SSR) یا useSWR (CSR)

❌ راه نادرست:

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

  useEffect(() => {
    let isCanceled = false

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

      setItems(data)
    })

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

  // ...
}

💽 Remix way:

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) با async/await در Server Component way:

// 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) با useSWR در 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) در 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) در 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)

  // ...
}

⁉️ واقعا ⁉️ چه چیزی باید استفاده کنیم؟ useEffect? useQuery? useSWR?

یا... فقط use() 🤔

use() یک عملکرد جدید React است که یک promise را قبول می‌کند و از نظر مفهومی شبیه به await است. use() promise برگردانده شده توسط یک عملکرد را به طریقی که با کامپوننت‌ها، Hook ها و Suspense سازگار باشد، مدیریت می‌کند. در مورد use() در RFC React بیشتر مطلع شوید.

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

مشکلات دریافت داده در useEffect

🏃‍♂️ شرایط رقابتی

🔙 هیچ دکمه برگشت آنیمی وجود ندارد

🔍 هیچ SSR یا محتوای HTML اولیه وجود ندارد

🌊 تعقیب آبشار

  • Reddit، Dan Abramov

نتیجه

از دریافت داده تا مبارزه با API های imperative، عوارض جانبی یکی از مهم‌ترین منابع ناامیدی در توسعه برنامه‌های وب هستند. و بیایید صادق باشیم، قرار دادن همه چیز در Hook های useEffect فقط کمی کمک می‌کند. خوشبختانه، یک علم (خوب، ریاضیات) برای عوارض جانبی وجود دارد، که در ماشین‌های حالت و statecharts صورت‌بندی شده است، که می‌تواند به ما در مدل سازی و درک نحوه هماهنگ سازی عوارض جانبی، صرف نظر از اینکه چه قدر پیچیده باشند، به طور declarative کمک کند.

منابع