گاهی اوقات میخواهید یک کامپوننت اطلاعاتی را به خاطر بسپارد (Remember)، اما دوست ندارید تغییر این اطلاعات باعث رندر مجدد (Re-render) کامپوننت و کندی برنامه شود. در این شرایط، ریآکت ابزاری به نام Ref را در اختیار شما قرار میدهد.
چطور یک Ref به کامپوننت اضافه کنیم؟
نحوه بهروزرسانی مقدار یک Ref چگونه است؟
تفاوتهای بنیادین بین Ref و State چیست؟
چطور به شکلی امن از Refها استفاده کنیم؟
برای شروع، باید هوک useRef را از ریآکت ایمپورت کنید:
import { useRef } from 'react';
در داخل کامپوننت، این هوک را صدا بزنید و مقدار اولیه را به عنوان تنها آرگومان به آن پاس دهید. به عنوان مثال، یک رف با مقدار اولیه 0:
const myRef = useRef(0);
هوک useRef یک شیء (Object) ساده با این ساختار برمیگرداند:
{
current: 0 // مقداری که به useRef پاس دادهاید
}
شما از طریق ویژگی myRef.current میتوانید به این مقدار دسترسی داشته باشید یا آن را تغییر دهید. این ویژگی کاملاً قابل تغییر (Mutable) است؛ یعنی برخلاف استیت، میتوانید همزمان آن را بخوانید و مستقیماً بازنویسی کنید. این فضا مثل یک جیب مخفی در کامپوننت شماست که ریآکت کاری به کار آن ندارد و تغییراتش را تعقیب نمیکند.
به این مثال ساده نگاه کنید:
import { useRef } from 'react';
export default function ClickCounter() {
const clickCountRef = useRef(0);
function handleClick() {
clickCountRef.current = clickCountRef.current + 1;
alert('شما ' + clickCountRef.current + ' بار کلیک کردید!');
}
return (
<button onClick={handleClick}>
کلیک کنید!
</button>
);
}
در کد بالا، با هر بار کلیک، مقدار current یک واحد اضافه شده و آلرت نشان داده میشود، اما کامپوننت به هیچ وجه دوباره رندر نمیشود. دکمه روی صفحه دقیقاً همان شکل اول باقی میماند.
شما میتوانید Ref و State را در یک کامپوننت با هم ترکیب کنید. فرض کنید میخواهیم یک تایمر بسازیم. برای نشان دادن زمان سپری شده به کاربر، به دیتای زنده نیاز داریم که روی صفحه رندر شود؛ پس زمان شروع را در استیت نگهمیداریم. اما برای متوقف کردن تایمر، به شناسه تایمر (interval ID) نیاز داریم تا متد clearInterval آن را بشناسد. از آنجا که این شناسه نقشی در ظاهر صفحه ندارد، آن را درون یک Ref ذخیره میکنیم:
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null); // نگهداری شناسه اینتروال بدون رندر ناخواسته
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
// پاکسازی اینتروال قبلی برای اطمینان
clearInterval(intervalRef.current);
// ذخیره شناسه تایمر در رف
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
// دسترسی مستقیم به شناسه برای متوقف کردن انیمیشن/تایمر
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h2>زمان سپری شده: {secondsPassed.toFixed(3)} ثانیه</h2>
<button onClick={handleStart}>شروع</button>
<button onClick={handleStop}>توقف</button>
</>
);
}
شاید به نظر برسد Refها به دلیل قابلیت جهش مستقیم (Mutation) راحتتر از استیتها هستند، اما در ۹۰ درصد مواقع شما باید از استیت استفاده کنید. رفها صرفاً یک «دریچه فرار (Escape Hatch)» برای ارتباط با دنیای خارج از ریآکت هستند.
| ویژگی | هوک useRef(initialValue) | هوک useState(initialValue) |
| خروجی هوک | یک شیء به فرمت { current: value } | یک آرایه دو عضوی شامل دیتای زنده و تابع توصیفی تغییر استیت |
| تغییر رندر؟ | خیر. تغییر مقدار current رندر مجدد ایجاد نمیکند. | بله. با اجرای متد ستاستیت، کامپوننت دوباره رندر میشود. |
| تغییرپذیری | Mutable؛ امکان تغییر مستقیم مقدار بیرون از فرآیند رندر. | Immutable؛ دیتای قبلی هرگز نباید مستقیماً دستکاری شود. |
| زمان استفاده | خواندن/نوشتن آن نباید لابلای کدهای رندر JSX انجام شود. | در هر ثانیهای قابل استفاده است و خروجی JSX کاملاً به آن وابسته است. |
⚠️ یک آزمایش اشتباه: اگر سعی کنید شمارندهای را که قرار است عددش روی دکمه تغییر کند با Ref بسازید، ریآکت متوجه تغییرات نشده و کاربر همیشه عدد اولیه (مثلاً 0) را روی دکمه خواهد دید؛ هرچند که در پشت صحنه مقدار رف در حال اضافه شدن باشد!
به طور کلی، زمانی به سراغ Ref بروید که کامپوننت شما نیاز دارد از فضای داخلی ریآکت خارج شده و با یک API خارجی (اکثراً داکیومنت مرورگر و مرورگر وب) بدون تأثیر بر ظاهر کلی کامپوننت ارتباط برقرار کند:
ذخیره و کنترل شناسههای تایمر (setTimeout و setInterval).
دسترسی و دستکاری مستقیم المانهای DOM (مانند فوکوس کردن روی یک اینپوت، اسکرول کردن صفحه به یک موقعیت خاص، یا محاسبه طول و عرض یک دیو).
ذخیره اشیایی که هیچ تأثیری روی کدهای ساختار JSX ندارند.
برای اینکه کدهای شما دچار باگهای غیرقابل پیشبینی نشوند، این سه اصل را رعایت کنید:
به رفها به چشم دریچه فرار نگاه کنید: اگر بخش زیادی از منطق برنامه و جریان دادههای شما (Data Flow) با رفها جلو میرود، احتمالاً باید معماری کدهای خود را بازنگری کنید.
در حین رندر (Rendering) مقدار ref.current را نخوانید یا تغییر ندهید: از آنجا که ریآکت زمان دقیق تغییر رف را تعقیب نمیکند، خواندن آن در حین رندر، رفتار خروجی کامپوننت را غیرقابل پیشبینی و نامعتبر میسازد. تنها استثنا، راهاندازی اولیه تنبل است (مانند: if (!ref.current) ref.current = new Client()).
محدودیتهای استیت برای رفها صادق نیست: استیتها مانند یک تصویر لحظهای (Snapshot) از هر رندر عمل میکنند و به صورت همزمان (Synchronous) تغییر نمیکنند. اما رفها کاملاً جاوااسکریپتی رفتار کرده و مقدارشان فوراً ثبت میشود:
ref.current = 5;
console.log(ref.current); // بلافاصله عدد 5 چاپ میشود
رفها ابزاری برای ذخیره اطلاعاتی هستند که در محاسبات رندر خروجی JSX دخالتی ندارند.
هر رف یک شیء ساده با ویژگی یکتای current است که به راحتی قابل خواندن و تغییر است.
برای دریافت رف از ریآکت، هوک useRef را صدا میزنیم.
رفها دیتای خود را در میان رندرهای پیاپی حفظ میکنند اما خودشان عامل رندر نیستند.
شایعترین کاربرد رفها، اتصال به المانهای واقعی DOM (مثلاً <input ref={inputRef} />) جهت مدیریت فوکوس و اسکرول است.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn