- منتشر شده در
سلام React, سلام useEffect (امیدوارم اینطور باشد)
- نویسندگان
- نام
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
در این مقاله، به شما نشان خواهم داد که چگونه از 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 است.
عوارض جانبی کی اتفاق میافتند؟
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 کمک کند.