اگر شما هم در درک و استفاده از هوک useEffect مشکل دارید این را بدانید که تنها نیستید. هم برنامه نویسان مبتدی و هم توسعه‎‌دهندگان باتجربه هر دو به طور یکسان آن را یکی از پیچیده‌ترین هوک‌ها می‌دانند، زیرا نیاز به درک چند مفهوم ناآشنا برنامه نویسی دارد.

در این مقاله، می‌خواهیم دلیل وجود هوک useEffect، درک بهتر آن و نحوه استفاده صحیح از آن در پروژه‌های React را توضیح دهیم. همچنین در ویدیو ساخت درخواست‌های HTTP با استفاده از هوک useEffect کانال یوتیوب نیز دقیق‌تر در این مورد صحبت کرده‌ایم.

چرا به آن useEffect می‌گویند؟

هنگامی که در سال ۲۰۱۸ هسته React Hooks (مانند useState، useEffect و غیره) به این کتابخانه اضافه شد، بسیاری از توسعه‌دهندگان بعد از آشنایی با این هوک یعنی “useEffect” دچار ابهام شدند. اما منظور از “effect” دقیقا چیست؟

واژه effect به یک اصطلاح برنامه نویسی کاربردی به نام “side effect” اشاره دارد. ولی برای اینکه واقعاً بفهمیم side effect چیست، ابتدا باید مفهوم تابع pure را درک کنیم.

ممکن است به این موضوع توجه نداشته باشیم، اما اکثر اجزای React به عنوان توابع pure در نظر گرفته شده‌اند. شاید عجیب باشد که اجزای React را به عنوان توابع در نظر بگیریم، اما اینطور است. این موضوع کمک می‌کند که ببینیم یک کامپوننت React معمولی مانند یک تابع جاوااسکریپت تعریف شده است:

function MyReactComponent() {}

اکثر اجزای React توابع pure هستند، به این معنی که یک input دریافت می‌کنند و یک خروجی قابل پیش‌بینی از JSX تولید می‌کنند.

ورودی یک تابع جاوااسکریپت آرگومان است. با این حال، ورودی کامپوننت React چیست؟ props!

در اینجا ما یک کامپوننت User داریم که prop با نام name در آن تعریف شده است. در User، مقدار prop در المنت <h1> نمایش داده می‌شود.

export default function App() {
  return <User name="John Doe" />   
}
  
function User(props) {
  return <h1>{props.name}</h1>; // John Doe
}

مثال بالا یک تابع pure است زیرا با توجه به ورودی یکسان، همیشه خروجی یکسان را برمی‌گرداند. اگر در کامپوننت User، پراپ name با مقدار “John Doe” تعریف کنیم، خروجی ما John Doe خواهد بود.

مزایایی که توابع pure دارند این است که قابل پیش‌بینی و قابل اعتماد هستند و همینطور تست کردن آن‌ها کار آسانی است. این موضوع زمانی اهمیت پیدا می‌کند که ما نیاز داریم تا یک side effect در کامپوننت خود اجرا کنیم.

منظور از side effectها در React چیست؟

side effectها قابل پیش‌بینی نیستند زیرا آن‌ها عملیاتی هستند که با «دنیای بیرون» انجام می‌شوند.

زمانی که برای انجام کاری نیاز به دسترسی به خارج از کامپوننت‌های React خود داشته باشیم، یک side effect انجام می‌دهیم. با این حال، انجام side effect یک نتیجه قابل پیش‌بینی به ما نمی‌دهد.

به عنوان مثال زمانی که می‌خواهیم داده‌ها (مانند پست‌های وبلاگ) را از سروری که failed شده است درخواست کنیم و به جای آن داده‌ها، یک کد وضعیت ۵۰۰ به عنوان پاسخ برمی‌گرداند.

تقریباً همه برنامه‌ها، البته به غیر از برنامه‌های ساده برای این که بتوانند مرتبط با یکدیگر و به شکل درست کار کنند به side effectها متکی هستند.

رایج‌ترین side effectها عبارتند از:

useEffect به این دلیل وجود دارد تا راهی برای کنترل side effectها در داخل کامپوننت‌های pure ری‌اکت رائه دهد.

به عنوان مثال، اگر بخواهیم متا تگ title را تغییر دهیم تا نام کاربر را در تب مرورگر نمایش دهیم، می‌توانیم این کار را در خود کامپوننت هم انجام دهیم، اما نباید این کار را بکنیم.

function User({ name }) {
  document.title = name; 
  // This is a side effect. Don't do this in the component body!
    
  return <h1>{name}</h1>;   
}

زیرا اگر ما یک side effect را مستقیماً در بدنه کامپوننت اجرا کنیم، این کار مانع از render شدن کامپوننت React مد نظرمان می‌شود.

side effectها باید از فرآیند rendering جدا شوند. اگر نیاز به انجام یک side effect داریم، حتما باید پس از render شدن کامپوننت انجام شود. این دقیقا همان چیزی است که useEffect انجام می‌دهد.

به طور خلاصه، دلیل استفاده از useEffect این است که به ما این امکان را می‌دهد تا با دنیای بیرون تعامل داشته باشیم، اما بر روی render شدن یا عملکرد کامپوننتی که در آن تعریف شده است تأثیری نمی‌گذارد.

چگونه باید از useEffect استفاده کنیم؟

سینتکس اصلی useEffect به شکل زیر است:

// ۱٫ import useEffect
import { useEffect } from 'react';

function MyComponent() {
  // ۲٫ call it above the returned JSX  
  // ۳٫ pass two arguments to it: a function and an array
  useEffect(() => {}, []);
  
  // return ...
}

روش صحیح اجرای side effect در کامپوننت User به ترتیب زیر است:

  1. useEffect را از React” import” می‌کنیم
  2. آن را بالاتر از JSX بازگشتی در کامپوننت فراخوانی می‌کنیم
  3. دو آرگومان به آن ارسال می‌کنیم: یک تابع و یک آرایه
import { useEffect } from 'react';

function User({ name }) {
  useEffect(() => {
    document.title = name;
  }, [name]);
    
  return <h1>{name}</h1>;   
}

تابعی که به useEffect ارسال می‌شود یک تابع callback است و پس از render شدن کامپوننت فراخوانی می‌شود. در این تابع می‌توانیم یک یا چند side effect را انجام دهیم.

آرگومان دوم یک آرایه است که آرایه dependencyها نامیده می‌شود. این آرایه باید شامل تمام مقادیری باشد که side effect مدنظر ما به آن‌ها وابستگی دارد.

در مثال بالا، از آنجایی که title را بر اساس یک مقدار در scope بیرونی name تغییر می‌دهیم، باید آن را در آرایه dependency ها قرار دهیم.

کاری که این آرایه انجام می‌دهد این است که بررسی می‌کند که آیا یک مقدار (در این مثال name) هنگام render شدن تغییر پیدا کرده است یا خیر. اگر تغییر کرده باشد در این صورت، تابع use effect را دوباره اجرا خواهد کرد.

این یک موضوع منطقی است زیرا اگر name تغییر کند، ما می‌خواهیم که آن name تغییر یافته را نمایش دهیم و در نتیجه side effect را دوباره اجرا کنیم.

چگونه مشکلات رایج موجود را با useEffect برطرف کنیم؟

چندین نکته بسیار ظریف وجود دارد که با دانستن آن‌ها از ایجاد مشکل در useEffect جلوگیری می‌کنیم.

اگر ما هیچ آرایه dependency به useEffect معرفی نکنیم و فقط یک تابع در نظر بگیریم، در این صورت useEffect پس از هر بار render شدن اجرا خواهد شد. بنابراین هنگامی که می‌خواهیم state را در useEffect به‌روزرسانی کنیم، این موضوع می‌تواند مشکلاتی را ایجاد کند.

اگر فراموش کرده باشیم که dependencyها را به شکل درست تعریف کنیم و stateها هم به شکل local تنظیم شده باشند، در این صورت رفتار پیش‌فرض React این است که کامپوننت را دوباره render می‌کند. از آنجایی که useEffect بعد از هر render بدون آرایه dependency اجرا می‌شود، یک حلقه بی‌نهایت ایجاد خواهد شد.

function MyComponent() {
  const [data, setData] = useState([])  
    
  useEffect(() => {
    fetchData().then(myData => setData(myData))
    // Error! useEffect runs after every render without the dependencies array, causing infinite loop
  }); 
}

در مثال بالا پس از اولین رندر، useEffect اجرا می‌شود، state به‌روزرسانی می‌شود و باعث render مجدد می‌شود. در نتیجه useEffect دوباره اجرا شود و این فرآیند تا بی‌نهایت ادامه پیدا می‌کند. این یک حلقه بی‌نهایت (infinite loop) نام دارد و می‌تواند برنامه را دچار مشکل کند.

اگر state را در useEffect به‌روزرسانی می‌کنیم، حتما باید یک آرایه dependency خالی تعریف ‌کنیم. این کار باعث می‌شود که تابع effect فقط یک بار پس از اولین render کامپوننت اجرا شود. توصیه می‌کنیم هر زمانی که از useEffect استفاده می‌کنید شما هم این کار را انجام دهید.

یک مثال رایج برای این کار fetch کردن داده است. برای یک کامپوننت، ممکن است فقط بخواهیم یک بار داده را fetch کنیم، آن را در state قرار دهیم و سپس در JSX نمایش دهیم.

function MyComponent() {
  const [data, setData] = useState([])  
    
  useEffect(() => {
    fetchData().then(myData => setData(myData))
    // Correct! Runs once after render with empty array
  }, []); 
   
  return <ul>{data.map(item => <li key={item}>{item}</li>)}</ul>
}

تابع cleanup در useEffect چیست؟

آخرین بخش انجام صحیح side effect در React تابع cleanup است.

گاهی اوقات لازم است تا اجرای side effect را متوقف کنیم. به عنوان مثال، اگر یک تایمر شمارش معکوس با استفاده از تابع setInterval داشته باشیم و بخواهیم شمارش را متوقف کنیم باید از تابع clearInterval استفاده کنیم.

مثال دیگر استفاده از subscriptions با WebSockets است. subscriptionها زمانی که دیگر از آن‌ها استفاده نمی‌کنیم باید «خاموش» شوند، و این دقیقا همان کاری است که تابع cleanup انجام می‌دهد.

اگر state را با استفاده از setInterval تنظیم کنیم و آن side effect متوقف نشود، وقتی کامپوننت unmount شود و دیگر قابل استفاده نباشد، state همراه با کامپوننت از بین می‌رود، اما تابع setInterval به کار خود ادامه می‌دهد.

function Timer() {
  const [time, setTime] = useState(0);
    
  useEffect(() => {
    setInterval(() => setTime(1), 1000); 
    // counts up 1 every second
    // we need to stop using setInterval when component unmounts
  }, []);
}

مشکل این است که اگر کامپوننت در حال unmount شدن باشد setInterval سعی می‌کند تا متغیری از یک بخش از زمان state را که دیگر وجود ندارد، به‌روزرسانی کند. این مورد خطایی به نام memory leak است.

برای استفاده از تابع cleanup، باید تابعی را از داخل تابع useEffect برگردانیم. داخل این تابع می‌توانیم عمل cleanup را انجام دهیم، در مثال زیر از clearInterval استفاده می‌کنیم تا setInterval را متوقف کنیم.

function Timer() {
  const [time, setTime] = useState(0);
    
  useEffect(() => {
    let interval = setInterval(() => setTime(1), 1000); 

    return () => {
      // setInterval cleared when component unmounts
      clearInterval(interval);
    }
  }, []);
}

وقتی کامپوننت unmount شود، تابع cleanup فراخوانی می‌شود.

یک مثال متداول از unmount شدن یک کامپوننت، رفتن به یک صفحه جدید یا یک مسیر جدید در برنامه است که در آن کامپوننت دیگر render نمی‌شود.

هنگامی unmount شدن یک کامپوننت، تابع cleanup اجرا می‌شود. بنابراین interval پاک می‌شود و دیگر خطای تلاش برای به‌روزرسانی متغیر stateای که وجود ندارد، رخ نمی‌دهد.

در نهایت، پاک کردن side effect در هر موردی لازم نیست. این موضوع فقط در موارد معدود، مانند نیاز به توقف یک side effect مکرر در هنگام unmount شدن کامپوننت، مورد نیاز است.

 

منبع