ریاکت به صورت خودکار و هوشمند DOM (ساختار درختی صفحه وب) را بهروزرسانی میکند تا با خروجی رندر کامپوننتهای شما همخوانی داشته باشد. به همین دلیل، در شرایط عادی نیازی به دستکاری مستقیم DOM ندارید.
با این حال، گاهی اوقات مجبور میشوید به المانهای واقعی DOM که توسط ریآکت مدیریت میشوند دسترسی پیدا کنید؛ مثلاً برای فوکوس کردن روی یک اینپوت، اسکرول کردن صفحه به یک موقعیت خاص، یا محاسبه اندازه و موقعیت یک المان. از آنجا که ریآکت متد داخلی برای این کارها ندارد، شما به یک Ref متصل به آن المان نیاز خواهید داشت.
چطور با استفاده از اتریبیوت ref به یک گره DOM دسترسی پیدا کنیم؟
ارتباط بین ویژگی ref در JSX و هوک useRef چیست؟
چطور به المانهای DOM در یک کامپوننت فرزند دسترسی داشته باشیم؟
در چه شرایطی دستکاری مستقیم DOM توسط ما امن و بدون ریسک است؟
برای دسترسی به المانی که توسط ریآکت مدیریت میشود، ابتدا هوک useRef را صدا بزنید تا یک رف با مقدار اولیه null ساخته شود:
const myRef = useRef(null);
سپس، این رف را به عنوان اتریبیوت ref به تگ JSX مورد نظر خود اختصاص دهید:
<div ref={myRef}>
در ابتدا مقدار myRef.current برابر با null است. اما به محض اینکه ریآکت المان واقعی را در مرورگر خلق کند، مرجع (Reference) آن المان را داخل myRef.current قرار میدهد. از این پس میتوانید در رویدادهای خود از تمام APIهای بومی مرورگر روی آن استفاده کنید:
// اسکرول نرم به سمت المان مورد نظر
myRef.current.scrollIntoView();
در این مثال، با کلیک روی دکمه، نشانگر موس مستقیماً وارد اینپوت میشود:
import { useRef } from 'react';
export default function SearchForm() {
const inputRef = useRef(null);
function handleFocus() {
// استفاده از متد نیتیو .focus() مرورگر
inputRef.current.focus();
}
return (
<div className="form-group">
<input ref={inputRef} placeholder="چیزی تایپ کنید..." />
<button onClick={handleFocus}>روی اینپوت فوکوس کن</button>
</div>
);
}
اگر تعداد المانها ثابت باشد، به راحتی چند رف مجزا میسازیم. اما اگر لیستی پویا از المانها داشته باشیم (مثلاً یک گالری تصاویر حاصل از map) چه؟ طبق قوانین ریآکت، شما نمیتوانید هوک useRef را داخل حلقهها یا متد .map() صدا بزنید.
راهحل اصولی، پاس دادن یک تابع به اتریبیوت ref است که به آن Ref Callback میگویند. این متد به شما اجازه میدهد المانها را در یک آرایه یا شیء Map ذخیره کنید:
import { useRef } from 'react';
export default function ImageGallery() {
// ذخیره ساختار Map در داخل رف برای نگهداری گرههای مختلف DOM
const elementsRef = useRef(new Map());
const items = [
{ id: 'img1', url: 'cat1.jpg' },
{ id: 'img2', url: 'cat2.jpg' }
];
function scrollToImage(id) {
const node = elementsRef.current.get(id);
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
return (
<>
<nav>
<button onClick={() => scrollToImage('img2')}>برو به تصویر دوم</button>
</nav>
<ul>
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
if (node) {
elementsRef.current.set(item.id, node); // اضافه کردن به مپ
} else {
elementsRef.current.delete(item.id); // پاکسازی در صورت حذف المان
}
}}
>
<img src={item.url} alt="Gallery item" />
</li>
))}
</ul>
</>
);
}
کامپوننتهای شخصی شما به صورت پیشفرض المانهای DOM خود را به بیرون ابراز نمیکنند. اگر یک رف را به کامپوننت سفارشی خود پاس دهید، ریآکت به صورت خودکار آن را به عنوان یک پروپ معمولی در نظر میگیرد. شما میتوانید این پروپ را در فرزند دریافت کرده و به المان نیتیو داخلی متصل کنید:
import { useRef } from 'react';
// کامپوننت فرزند رف را مانند پروپهای معمولی دریافت میکند
function MyInput({ ref, label }) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
export default function MyForm() {
const customInputRef = useRef(null);
function handleClick() {
customInputRef.current.focus();
}
return (
<>
<MyInput ref={customInputRef} label="نام کاربری:" />
<button onClick={handleClick}>فوکوس</button>
</>
);
}
با این کار، customInputRef.current در کامپوننت پدر، دقیقاً به تگ <input> در اعماق کامپوننت فرزند اشاره خواهد کرد.
در ریآکت، فرآیند بهروزرسانی کامپوننت شامل دو مرحله کلیدی است:
مرحله رندر (Render Phase): ریآکت کامپوننتها را صدا میزند تا بفهمد چه چیزی باید روی صفحه بیاید (در این مرحله هنوز DOM تغییر نکرده و رفها null یا دیتای قدیمی هستند).
مرحله کامیت (Commit Phase): ریآکت تغییرات را روی DOM واقعی اعمال میکند.
ریآکت مقدار ref.current را دقیقاً در مرحله کامیت تنظیم میکند. درست قبل از اعمال تغییرات، مقدار رفها را null کرده و بلافاصله پس از آپدیت کامل درختِ مرورگر، گرههای جدید را به رفها متصل میکند. به همین دلیل است که همیشه باید درون رویدادها (Event Handlers) یا هوک useEffect به سراغ رفها بروید، نه در بدنه اصلی کامپوننت.
رفها یک «دریچه فرار» هستند. تا زمانی که کارهای شما غیرمخرب باشد (مانند فوکوس زدن، اسکرول کردن یا سنجش اندازه المان)، هیچ مشکلی پیش نخواهد آمد. اما اگر بخواهید المانهای تحت مدیریت ریآکت را به صورت دستی حذف کنید یا تغییر دهید، برنامه کرش خواهد کرد!
به این نمونه خطرناک نگاه کنید:
// ❌ نمونه کد پرمخاطره و اشتباه
{showText && <p ref={textRef}>سلام دنیا</p>}
<button onClick={() => textRef.current.remove()}>
حذف مستقیم از DOM مرورگر
</button>
اگر کاربر با دستور بومی مرورگر (.remove()) المان پاراگراف را حذف کند، ریآکت روحش هم از این ماجرا باخبر نمیشود! حالا اگر استیت showText تغییر کند و ریآکت بخواهد دوباره آن المان را مدیریت یا حذف کند، چون گره را در دنیای واقعی پیدا نمیکند، برنامه با خطای شدید (Crash) متوقف میشود.
🟩 قاعده کلی: المانهایی را که ساختار یا حذف و اضافهشدنشان توسط استیتهای ریآکت مدیریت میشود، به صورت دستی دستکاری نکنید. اگر مایلید یک کار دستی انجام دهید (مثلاً تزریق المان با یک کتابخانه دیگر)، این کار را روی یک تگ
<div>کاملاً خالی انجام دهید که ریآکت هیچ دلیلی برای تغییر دادن فرزندان آن ندارد.
با پاس دادن یک رف به ویژگی <tag ref={myRef}>، ریآکت المان واقعی مرورگر را در myRef.current میگذارد.
کارهای غیرمخرب مانند جابجایی اسکرول، فوکوس و اندازهگیری المانها بهترین کاربردهای رفهای DOM هستند.
کامپوننتهای سفارشی به طور پیشفرض DOM خود را باز نمیکنند، مگراینکه رف را به عنوان پروپ به فرزند پاس داده و به تگهای بومی وصل کنید.
هرگز المانهایی را که ریآکت به صورت شرطی رندر میکند، مستقیماً حذف یا دگرگون نکنید تا جلوی کرشهای ناگهانی برنامه را بگیرید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn