چرا useEffect دو بار اجرا میشود و چگونه میتوان آن را در React به خوبی مدیریت کرد؟
یک شمارنده و یک console.log() در یک useEffect دارم که هر تغییر در حالت من را ثبت میکند، اما useEffect دو بار در هنگام نمایش فراخوانی میشود. از React 18 استفاده میکنم.
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
اجرای دوبارهی useEffect در هنگام نمایش در حالت توسعه (development) و با فعال بودن StrictMode در React 18 امری طبیعی است. این رفتار به منظور بهبود عملکرد و یافتن باگهای احتمالی در کد شماست. React در حالت توسعه، اجزای شما را دوبار رندر میکند تا اطمینان حاصل کند که کد شما در برابر remounting مقاوم است. این ویژگی به React اجازه میدهد تا بخشهایی از رابط کاربری را بدون از دست رفتن وضعیت، اضافه یا حذف کند. این موضوع به خصوص در مواردی که برنامه شما از روشی غیرمنتظره رفتار میکند، بسیار مفید است. در حالت تولید (production)، این رفتار وجود ندارد و useEffect فقط یک بار اجرا میشود.
در بیشتر موارد، نیازی به تغییر کد خود برای حل این مشکل نیست. اگر useEffect شما با تغییرات در state بهروزرسانی میشود، این رفتار طبیعی است. اما در مواردی مانند استفاده از setInterval، fetch یا هر نوع فراخوانی شبکهای و یا تعامل با منابع خارجی، ضروری است که از تابع پاکسازی (cleanup function) استفاده شود تا از بروز مشکلات احتمالی مانند نشت حافظه یا درخواستهای اضافی جلوگیری شود.
مثال: اگر در داخل useEffect از setInterval استفاده میکنید، باید در تابع پاکسازی آن را با clearInterval متوقف کنید. به این ترتیب، در هر بار باز رندر، تایمر قبلی متوقف و تایمر جدیدی شروع میشود و از ایجاد چندین تایمر در حالت توسعه جلوگیری میشود.
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
return () => clearInterval(id);
}, []);
در مواردی که با fetch کار میکنید، استفاده از AbortController برای لغو درخواستهای بینیاز، در تابع پاکسازی بسیار مهم است. به این ترتیب، اگر قبل از اتمام درخواست، کامپوننت unmount شود، درخواست لغو میشود و از ایجاد درخواستهای اضافی و مشکلات احتمالی جلوگیری میشود.
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", { signal: abortController.signal });
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
// مدیریت خطا
}
}
};
fetchUser();
return () => abortController.abort();
}, []);
اگر نیاز به اجرای useEffect تنها در اولین رندر دارید، میتوانید از useRef استفاده کنید. این روش اجرای useEffect را تنها در اولین بار ممکن میکند.
const effectRan = useRef(false);
useEffect(() => {
if (!effectRan.current) {
console.log("effect applied - only on the FIRST mount");
}
return () => (effectRan.current = true);
}, []);
اگرچه React بهطور پیشفرض رفتار مناسبی را برای useEffect تضمین میکند، درک مکانیزم remounting و نحوه استفاده از تابع پاکسازی برای نوشتن کد بهتر و بدون باگ بسیار مهم است. در صورتیکه به مشکلاتی برمیخورید، مطمئن شوید که از useEffect در جای مناسب و با توجه به بهترین شیوهها استفاده میکنید. از ایجاد درخواستهای اضافی به سرور در حالت توسعه نگران نباشید، زیرا این رفتار به منظور کشف باگها در کد شما طراحی شده است.
`