در مقالهی قبلی با هوکهای پایه و پرکاربرد React مانند useState، useEffect و useContext آشنا شدیم؛ هوکهایی که تقریباً در هر پروژهی React حضور دارند و بخش جداییناپذیر توسعهی رابط کاربری محسوب میشوند. اما React مجموعهای از هوکهای پیشرفتهتر و تخصصیتری را نیز ارائه میدهد که در شرایط خاص، قابلیتهای قدرتمند و کاربردی خود را نشان میدهند.
در این مقاله، به معرفی و بررسی پنج هوک کمتر شناختهشده اما بسیار مفید React میپردازیم:
- useActionState
- useTransition
- useDeferredValue
- useImperativeHandle
- useId
این هوکها برای حل مشکلات مختلف مثل عملکرد بهتر، کار با DOM، مدیریت فرمها یا بهبود تجربه کاربری ساخته شدهاند. آشنایی با این هوکها به برنامهنویسها کمک میکند تا کدی منظمتر و مناسبتر برای پروژههای پیشرفته بنویسند.
هوک useActionState در ری اکت
در بسیاری از برنامههای تحت وب، لازم است پس از انجام یک عمل توسط کاربر بتوانیم نتیجهی آن عمل را ذخیره کنیم، خطاها را نمایش دهیم یا مثلاً وضعیت «در حال ارسال» را نشان دهیم. اینجاست که هوک useActionState وارد عمل میشود. این هوک کاربردی از ریاکت، مدیریت عملیات کاربر مثل ارسال فرم و تغییرات async در state را بسیار سادهتر میکند.
به جای نوشتن کدهای تکراری و پیچیده، useActionState یک روش بهینه برای مدیریت این وضعیتها فراهم میکند. در واقع این هوک یک «عمل کاربر» (مثل کلیک یا ارسال فرم) را به یک قطعهی state متصل میکند و به صورت خودکار state را بر اساس نتیجهی آن عمل بهروزرسانی میکند.
برای استفاده از این هوک، ابتدا باید آن را از پکیج React ایمپورت نمایید:
import { useActionState } from "react";
سپس در درون تابع کامپوننت، با فراخوانی useActionState میتوانید یک مقدار اولیه برای وضعیت تعریف کرده و در کنار آن، تابعی برای مدیریت عملیات فرم و همچنین یک مقدار boolean برای تشخیص وضعیت در حال اجرا بودن عملیات دریافت نمایید:
const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);
ورودیهای useActionState
- fn: تابعی که هنگام ارسال فرم با کلیک روی دکمه اجرا میشود. این تابع بهعنوان ورودی، ابتدا وضعیت قبلی فرم را دریافت میکند (که در ابتدا همان initialState است، و در دفعات بعد مقدار برگشتی از اجرای قبلی تابع خواهد بود). سپس سایر آرگومانهایی را دریافت میکند که معمولاً در فرمها استفاده میشوند.
- initialState: مقداری که میخواهید state در ابتدا با آن مقدار دهی شود. میتواند هر نوع مقدار قابل سریالسازی (شیء، رشته، عدد و ...) باشد. توجه داشته باشید که این مقدار فقط در اولین اجرا لحاظ میشود و پس از آن نادیده گرفته میشود.
- permalink (اختیاری): این یک آدرس اختصاصی برای صفحهای است که فرم قرار است در آن چیزی را تغییر دهد. اگر صفحه شما محتوای متغیر دارد (مثلاً لیست خبرها)، و اگر کاربر قبل از اینکه جاوااسکریپت کامل بارگذاری شود فرم را ارسال کند، مرورگر کاربر را به همین آدرس permalink میبرد.
خروجیهای useActionState
- وضعیت فعلی (state): در اولین رندر، این مقدار همان initialState است. پس از اجرای تابع fn، مقدار آن برابر با خروجی تابع خواهد بود.
- تابع جدید action: این تابع را میتوانید به ویژگی action در تگ <form> یا ویژگی formAction در دکمهها بدهید.
- پرچم isPending: مقدار boolean که مشخص میکند آیا عملیات در حال انجام است یا خیر.
مثال ساده: در این مثال، دو محصول داریم که کاربر میتواند با کلیک روی دکمه خرید هر کدام از آن ها، یک کتاب خاص را به سبد خرید اضافه کند. این کار به کمک هوک useActionState و یک تابع سمت سرور (addToCart) انجام میشود. بسته به آیتم انتخابشده، پیام موفقیت یا خطا به کاربر نمایش داده میشود.
import { useActionState, useState } from "react";
import { addToCart } from "./actions.js";
function AddToCartForm({itemID, itemTitle}) {
const [message, formAction, isPending] = useActionState(addToCart, null);
return (
<form action={formAction}>
<h2>{itemTitle}</h2>
<input type="hidden" name="itemID" value={itemID} />
<button type="submit">Add to Cart</button>
{isPending ? "Loading..." : message}
</form>
);
}
export default function App() {
return (
<>
<AddToCartForm itemID="1" itemTitle="JavaScript: The Definitive Guide" />
<AddToCartForm itemID="2" itemTitle="JavaScript: The Good Parts" />
</>
)
}
فایل actions.ts (سمت سرور):
"use server";
export async function addToCart(prevState, queryData) {
const itemID = queryData.get('itemID');
if (itemID === "1") {
return "Added to cart";
} else {
// Add a fake delay to make waiting noticeable.
await new Promise(resolve => {
setTimeout(resolve, 2000);
});
return "Couldn't add to cart: the item is sold out.";
}
}
تابع addToCart روی سرور اجرا میشود و با توجه به شناسه کالا (itemID) بررسی میکند که آیا آیتم میتواند به سبد خرید اضافه شود یا نه. اگر شناسه آیتم "1" باشد، پیام موفقیت "Added to cart" را برمیگرداند. اگر شناسه آیتم چیز دیگری باشد، بعد از یک تأخیر دو ثانیهای، پیام خطا برمیگرداند.
در سمت کلاینت، هوک useActionState به طور خودکار تابع addToCart را به فرم وصل میکند. هر بار که روی دکمه "Add to Cart" کلیک میشود، فرم ارسال میشود و اطلاعات آیتم (itemID) به تابع سرور فرستاده میشود. نتیجه این عملیات (موفق بودن یا خطا دادن) به صورت یک پیام به کاربر نمایش داده میشود.
استفاده از useActionState باعث میشود که کل روند ارسال داده به سرور و دریافت نتیجه (چه موفقیت چه خطا) به سادگی و بدون نیاز به مدیریت دستی state انجام شود. به طور مشخص:
- اتصال فرم به تابع سمت سرور: استفاده از useActionState باعث میشود که تابع addToCart به عنوان action فرم ثبت شود. به این معنی که هر بار کاربر فرم را ارسال میکند، اطلاعات فرم به طور خودکار به تابع سرور منتقل میشود. این فرآیند دیگر نیازی به نوشتن کد اضافی برای جمعآوری دادههای فرم و ارسال آنها ندارد و همه این مراحل به صورت یکپارچه انجام میشوند.
- مدیریت وضعیت لودینگ (isPending): وقتی درخواست ارسال میشود، useActionState به طور خودکار متوجه میشود که درخواست در حال انجام است و مقدار isPending را true میکند. این امکان را میدهد که کاربر پیام "Loading..." ببیند و تا زمان رسیدن نتیجه، دکمه یا فرم دوباره قابل ارسال نباشد.
- دریافت و نمایش پیام نتیجه (message): هر نتیجهای که از سرور برگردد، به طور خودکار در متغیر message ذخیره میشود. این message میتواند مستقیماً در UI به کاربر نمایش داده شود.
در نتیجه، استفاده از useActionState باعث میشود مدیریت state و منطق فرمها سادهتر شود. دیگر نیازی به تعریف useState یا نوشتن توابع جداگانه برای کنترل وضعیت فرم و پیامها یا استفاده از useEffect وجود ندارد و همه این مراحل به صورت خودکار انجام میشود. در نتیجه، کد بسیار خوانا تر خواهد بود.
موارد استفاده از useActionState:
استفاده از اطلاعات بازگشت داده شده از action فرم: این قابلیت به شما امکان میدهد تا هر اطلاعاتی که بعد از ارسال فرم و اجرای action سمت سرور بازگردانده میشود، مستقیماً در کامپوننت React خود دریافت و استفاده کنید.
هوک useTransition در ری اکت
در برخی از قسمتهای رابط کاربری، ممکن است نیاز داشته باشیم بخشی از UI را در پسزمینه رندر کنیم تا تجربه کاربری روانتر باشد. هوک useTransition دقیقاً برای چنین موقعیتهایی طراحی شده است. این هوک به شما اجازه میدهد برخی از تغییرات در state را بهعنوان «کماهمیتتر» علامتگذاری کنید تا ریاکت آنها را با اولویت پایینتر اجرا کند.
هوک useTransition یکی از ابزارهای قدرتمند ری اکت برای مدیریت پردازشهای پرهزینه در رابط کاربری است که به کمک آن میتوانید بخشهایی از صفحه را با تاخیر و در پسزمینه بهروزرسانی کنید تا قسمتهای حیاتی و فوری، مثل تایپ کاربر یا کلیک دکمهها، بدون وقفه و سریع نمایش داده شوند.
برای استفاده صحیح از useTransition، باید آن را در بالاترین سطح تابع کامپوننت خود فراخوانی کنید:
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
هوک useTransition هیچ پارامتری دریافت نمیکند، و یک آرایه شامل دقیقاً دو مقدار بازمیگرداند:
- isPending: یک مقدار boolean است که نشان میدهد آیا یک عملیات transition در حال اجرا است یا خیر.
- startTransition: تابعی است که به شما اجازه میدهد برخی از بهروزرسانیهای state را بهعنوان یک transition علامتگذاری کنید. این کار باعث میشود آن بهروزرسانیها با اولویت کمتر و بدون ایجاد وقفه در تعاملهای فوری کاربر انجام شوند.
تابع startTransition که توسط useTransition برگردانده میشود، به شما این امکان را میدهد که یک بهروزرسانی را به عنوان «Transition» علامتگذاری کنید؛ یعنی این بهروزرسانی با اولویت پایینتر انجام شود تا رابط کاربری برای کاربر سریع و روان باقی بماند.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
توابعی که درون startTransition فراخوانی میشوند، «Action» نامیده میشوند. این توابع معمولاً شامل یک عمل (Action) هستند که باید با اولویت پایینتر اجرا شود. بر اساس یک قرارداد نامگذاری (Convention)، بهتر است هر callback یا تابعی که قرار است داخل startTransition اجرا شود، نامش action یا پسوند Action داشته باشد تا کد خواناتر باشد.
function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => {
startTransition(async () => {
await submitAction();
});
}}
>
Submit
</button>
);
}
در کد بالا، کامپوننت SubmitButton یک دکمه دارد که هنگام کلیک یک عملیات را انجام میدهد. تابع submitAction همان عملیاتی است که باید انجام شود و به این کامپوننت به عنوان prop داده میشود. با استفاده از هوک useTransition، وقتی روی دکمه کلیک میشود، عملیات ارسال داخل تابع startTransition قرار میگیرد و این کار باعث میشود عملیات با اولویت پایینتر و به صورت غیر مسدود کننده (non-blocking) اجرا شود و برنامه برای کاربر قفل نشود.
مثال ساده: فرض کنید یک فرم پیچیده با چندین ورودی دارید که باید اعتبارسنجی (validation) شوند. در این شرایط، بهتر است که کاربر بتواند همچنان با فرم تعامل داشته باشد (مثلاً تایپ کند یا بین فیلدها جابجا شود)، در حالی که عملیات اعتبارسنجی در پسزمینه در حال انجام است. در چنین موقعیتی، استفاده از هوک useTransition بسیار مفید است. این هوک به شما کمک میکند عملیات سنگین و زمانبر مثل اعتبارسنجی را با اولویت پایینتر اجرا کنید.
import React, { useState, useTransition } from 'react';
function ComplexForm() {
const [formData, setFormData] = useState({ name: '', email: '' });
const [isPending, startTransition] = useTransition();
const [validationMessage, setValidationMessage] = useState('');
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prevData => ({ ...prevData, [name]: value }));
startTransition(() => {
// Simulate validation process
setValidationMessage(`Validating ${name}...`);
setTimeout(() => {
setValidationMessage(`${name} is valid.`);
}, 2000);
});
};
return (
<div>
<input type="text" name="name" value={formData.name} onChange={handleChange} placeholder="Name" />
<input type="email" name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
{isPending && <div>{validationMessage}</div>}
</div>
);
}
در این مثال، مقادیر فیلدهای فرم بلافاصله پس از هر تغییر بهروزرسانی میشوند. اعتبارسنجی هر فیلد نیز به کمک startTransition و به صورت deferred (با اولویت پایینتر) انجام میشود تا UI روان بماند و کاربر بتواند بدون وقفه، سایر فیلدها را ویرایش کند. در نهایت پیام اعتبارسنجی به صورت غیر همزمان به کاربر نمایش داده میشود و تجربه کاربری بهتری ایجاد میگردد.
موارد استفاده از useTransition:
- انجام بهروزرسانیهای غیر مسدود کننده (non-blocking): برخی عملیاتها را میتوان به گونهای انجام داد که اجرای برنامه برای کاربر متوقف نشود و تجربه کاربری روان باقی بماند.
- فراهمسازی امکان اجرای اکشن (action) توسط سایر کامپوننتها: میتوانید قابلیت یا اکشن خاصی را از طریق props در اختیار بخشهای دیگر برنامه قرار دهید تا آنها نیز بتوانند از این عملکرد استفاده کنند.
- نمایش وضعیت در حال انجام بودن یا انتظار (pending): در زمانی که یک عملیات هنوز به پایان نرسیده است، میتوان وضعیت در حال انجام بودن آن را به کاربر اطلاع داد.
- جلوگیری از نمایش غیرضروری حالت لودینگ: برنامه را میتوان به گونهای پیکربندی کرد که فقط در مواقع ضروری حالت لودینگ نمایش داده شود و از ایجاد معطلیهای بیدلیل برای کاربر جلوگیری گردد.
- نمایش خطا به کاربر با استفاده از Error Boundary: در صورت بروز خطا هنگام اجرای برنامه، میتوان با بهرهگیری از Error Boundary پیام مناسبی به کاربر نمایش داد تا از علت مشکل مطلع شود و دچار سردرگمی نگردد.
هوک useDeferredValue در ری اکت
گاهی اوقات مقدار یک state خیلی سریع تغییر میکند، اما نمیخواهیم رابط کاربری همزمان با هر تغییر فوراً بهروزرسانی شود؛ زیرا این کار ممکن است عملکرد برنامه را کاهش دهد. در این شرایط، میتوان از هوک useDeferredValue استفاده کرد. مثلاً هنگام جستجوی یک کلمه در یک لیست بزرگ، کاربر میتواند بلافاصله حروف تایپشده را ببیند، اما فیلتر شدن لیست بر اساس این حروف با کمی تاخیر و در پسزمینه انجام میشود. این کار کمک میکند حتی اگر پردازش لیست زمانبر باشد، برنامه همچنان روان و بدون تاخیر برای کاربر نمایش داده شود.
برای استفاده از useDeferredValue، ابتدا آن را در از پکیج ایمپورت کنید:
import { useDeferredValue } from 'react';
سپس به این شکل در بالاترین سطح تابع کامپوننت فراخوانی کنید:
function SearchPage() {
const deferredValue = useDeferredValue(value);
// ...
}
ورودیهای useDeferredValue
- value: مقداری است که با هر بار تایپ کاربر تغییر میکند (در این مثال query). این مقدار بهعنوان ورودی به useDeferredValue داده شده تا نسخهای از آن بهصورت تأخیری در اختیار ما قرار بگیرد.
- initialValue (اختیاری): مقداری است که در اولین رندر کامپوننت استفاده میشود. اگر این پارامتر را مشخص نکنید، useDeferredValue در رندر اولیه تاخیری اعمال نمیکند، زیرا هنوز مقدار قبلیای برای جایگزینی وجود ندارد.
خروجیهای useDeferredValue
currentValue: در اولین رندر، مقدار بازگشتی deferred همان مقدار اولیه (initialValue) یا همان مقداری است که شما به هوک دادهاید. اما وقتی مقدار اصلی تغییر میکند، ریاکت ابتدا مقدار قبلی را نشان میدهد تا رابط کاربری سریع بماند و بعد در پسزمینه، مقدار جدید را بهروزرسانی می کند و نمایش میدهد.
برای اینکه بهروزرسانی بعضی بخشهای رابط کاربری را به تعویق بیندازید، میتوانید هوک useDeferredValue را در ابتدای کامپوننت خود فراخوانی کنید. این کار به شما اجازه میدهد وقتی دادههای جدید هنوز آماده نشدهاند، محتوای قبلی نمایش داده شود تا تجربه کاربری روان بماند.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
در اولین بار اجرا (initial render)، مقدار deferredQuery دقیقاً همان query است که شما وارد کردهاید. اما هنگام تغییر، مقدار جدید با کمی تاخیر نمایش داده میشود و مقدار قبلی تا آماده شدن مقدار جدید در رابط کاربری باقی میماند.
در مثال زیر، مقدار query بلافاصله بعد از تایپ کاربر بهروزرسانی میشود و در input نمایش داده میشود. اما مقدار deferredQuery تا زمانی که دادههای جدید بارگذاری شوند، همچنان مقدار قبلی را نگه میدارد. به همین دلیل، کامپوننت SearchResults تا آماده شدن نتایج جدید، همان نتایج قبلی (stale results) را نمایش میدهد و دیگر بلافاصله پیام «Loading...» نمایش داده نمیشود.
برای مثال اگر ابتدا "a" را تایپ کنید و نتایج نمایش داده شوند، سپس مقدار را به "ab" تغییر دهید، رابط کاربری همچنان نتایج مربوط به "a" را نمایش میدهد تا زمانی که دادههای جدید برای "ab" بارگذاری و جایگزین شوند. کامپوننت App:
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
کامپوننت SearchResults:
import {use} from 'react';
import { fetchData } from './data.js';
export default function SearchResults({ query }) {
if (query === '') {
return null;
}
const albums = use(fetchData(`/search?q=${query}`));
if (albums.length === 0) {
return <p>No matches for <i>"{query}"</i></p>;
}
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
این رفتار باعث میشود که کاربر در حین تایپ، افت عملکرد یا پرش در رابط کاربری احساس نکند، زیرا نتایج قبلی بلافاصله حذف نمیشوند و تاخیرها به شکل طبیعی در پسزمینه مدیریت میشوند.
مقایسه هوکهای useTransition و useDeferredValue
هوکهای useTransition و useDeferredValue هر دو برای مدیریت اولویت در بهروزرسانیهای رابط کاربری استفاده میشوند، اما نحوهی عملکرد آنها متفاوت است.
- useTransition: کدی را که state را تغییر میدهد در بر میگیرد. یعنی اگر شما کنترل کامل روی بهروزرسانی state دارید و میخواهید آن را با اولویت پایینتری انجام دهید، از این هوک استفاده کنید.
- useDeferredValue: این هوک مقداری را در خود نگه میدارد که وابسته به یک state در حال تغییر است. اگر به تابعی که مقدار state را تغییر میدهد دسترسی ندارید (برای مثال وقتی مقدار به صورت prop از بیرون به کامپوننت شما ارسال میشود)، میتوانید از این هوک استفاده کنید.
به طور کلی، هوک useTransition برای زمانی مناسب است که شما مستقیماً کدی را در اختیار دارید که state را تغییر میدهد و میخواهید آن تغییر با اولویت پایینتری انجام شود. در مقابل، useDeferredValue زمانی کاربرد دارد که به مقدار نهایی وابسته به یک state دسترسی دارید، اما کنترل مستقیمی بر خود فرآیند بهروزرسانی آن ندارید. استفاده همزمان از این دو هوک توصیه نمیشود!
موارد استفاده از useDeferredValue
- نمایش محتوای قدیمی در حالی که محتوای جدید در حال بارگذاری است.
- مشخص کردن اینکه محتوا هنوز بهروزرسانی نشده و نسخهی قدیمی است.
- به تعویق انداختن رندر مجدد بخشی از رابط کاربری برای بهبود عملکرد و تجربه کاربری.
هوک useImperativeHandle در ری اکت
هوک useImperativeHandle یک هوک در ریاکت است که به شما اجازه میدهد وقتی یک ref به کامپوننت خود متصل میکنید، فقط بعضی قابلیتها یا متدهای دلخواه را به کامپوننت والد نشان بدهید. یعنی به جای اینکه کل یک المنت یا کامپوننت را با ref کنترل کنید، فقط بخشهایی که لازم دارید (مثل متد focus یا هر دستور دیگری) را در اختیار والد قرار میدهید. از این هوک معمولاً در موقعیتهایی مانند اعتبارسنجی فرمها یا مدیریت ورودیهای کاربر که والد نیاز دارد بهطور مستقیم با فرزند تعامل داشته باشد استفاده میشود.
فرم کلی استفاده از useImperativeHandle به این صورت است:
useImperativeHandle(ref, createHandle, dependencies?)
ورودیهای useImperativeHandle
- ref: یک prop است که از طرف والد به کامپوننت MyInput داده میشود تا بتواند با فرزند ارتباط داشته باشد.
- createHandle: یک تابع ساده و بدون مقدار ورودی است که چیزی را برمیگرداند که والد باید از طریق ref به آن دسترسی داشته باشد. معمولاً این چیز، یک شی با چند تابع یا ویژگی است.
- dependencies (اختیاری): این گزینه یعنی باید همه مقادیری که در داخل تابع createHandle استفاده کردهاید و ممکن است تغییر کنند (مثل props، state یا متغیرهایی که داخل کامپوننت ساختهاید) را بهعنوان وابستگی (dependencies) مشخص کنید. این لیست باید ثابت باشد و به شکل [dep1, dep2, dep3] در کد بنویسید.
باید useImperativeHandle را در ابتدای کامپوننت خود بنویسید تا تعیین کنید والد دقیقاً به چه متدهایی از طریق ref دسترسی داشته باشد:
import { useImperativeHandle } from 'react';
function MyInput({ ref }) {
useImperativeHandle(ref, () => {
return {
// اینجا ویژگیهایی که والد باید ببیند را قرار دهید
};
}, []);
اگر میخواهید والد فقط به یک نود از Dom دسترسی داشته باشد، کافی است ref را به همان نود بدهید.
function MyInput({ ref }) {
return <input ref={ref} />;
}
در مثال بالا، اگر ref را به MyInput بدهید، در واقع به خود input داخل آن دسترسی پیدا میکنید.
اما اگر میخواهید فقط موارد خاصی را به والد نشان بدهید (نه کل input)، باید useImperativeHandle را در بالای کامپوننت خود صدا بزنید:
import { useImperativeHandle } from 'react';
function MyInput({ ref }) {
useImperativeHandle(ref, () => {
return {
// ... your methods ...
};
}, []);
return <input />;
};
در این روش، دیگر ref را مستقیماً به input نمیدهید! مثلاً اگر فقط میخواهید چند متد خاص مثل focus و scrollIntoView در دسترس والد باشد، باید یک ref جدا برای input بسازید. بعد با useImperativeHandle فقط همین متدها را به والد معرفی کنید تا والد فقط به همین موارد دسترسی داشته باشد، نه به کل input.
import { useRef, useImperativeHandle } from 'react';
function MyInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input ref={inputRef} />;
};
اکنون والد فقط میتواند متدهای focus و scrollIntoView را روی MyInput صدا بزند و به کل عنصر <input> دسترسی مستقیم ندارد.
مثال ساده: در این مثال، یک فرم داریم که شامل یک ورودی سفارشی به نام MyInput و یک دکمه است. در کامپوننت Form، با استفاده از useRef یک ref تعریف میشود و این ref به کامپوننت MyInput داده میشود.
کامپوننت App:
import { useRef } from 'react';
import MyInput from './MyInput.js';
export default function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
// This won't work because the DOM node isn't exposed:
// ref.current.style.opacity = 0.5;
}
return (
<form>
<MyInput placeholder="Enter your name" ref={ref} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
کامپوننت MyInput:
import { useRef, useImperativeHandle } from 'react';
function MyInput({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
};
export default MyInput;
در مثال بالا، زمانی که کاربر روی دکمه "Edit" کلیک میکند، تابع handleClick اجرا میشود و متد focus که قبلاً از طریق useImperativeHandle در MyInput تعریف شده بود، فراخوانی میشود. این کار باعث میشود input داخل MyInput فوکوس شود.
اما اگر بخواهید به ویژگیهای دیگر DOM مانند style دسترسی پیدا کنید، این امکان وجود ندارد. چون با استفاده از useImperativeHandle فقط متدهای مشخص مثل focus و scrollIntoView در اختیار والد قرار داده شدهاند و دسترسی مستقیم به کل عنصر <input> وجود ندارد.
این روش باعث میشود کنترل بیشتری روی امکاناتی که در اختیار والد قرار میگیرد داشته باشید و فقط عملکردهای مورد نیاز را در دسترس بگذارید. به طور کلی، هوک useImperativeHandle زمانی کاربرد دارد که بخواهید کنترل مشخص و محدودی از یک کامپوننت فرزند را در اختیار والد قرار دهید. بهجای افشای کل ساختار داخلی یا DOM، میتوانید تنها متدها یا قابلیتهای موردنیاز را به والد منتقل کنید.
تفاوت useImperativeHandle با useRef
وقتی از useRef استفاده میکنیم، فقط یک اشارهگر ساده (ref) به کامپوننت فرزند داریم. این اشارهگر فقط به ما اجازه میدهد به خود المنت یا مقدار داخلی آن دسترسی داشته باشیم؛ مثلاً روی input فوکوس کنیم یا مقدارش را بخوانیم. اما useImperativeHandle این امکان را میدهد که خودمان تعیین کنیم والد از طریق ref به چه چیزهایی دسترسی داشته باشد. مثلاً فقط یک یا چند تابع خاص را در اختیار والد قرار دهیم. این باعث میشود والد کنترل بیشتری بهصورت امن و مشخص داشته باشد، بدون اینکه همه بخش های کامپوننت فرزند را ببیند.
موارد استفاده از useImperativeHandle
- در اختیار گذاشتن یک ref سفارشی برای کامپوننت والد
- فراهم کردن متدهای imperative دلخواه برای استفاده در والد
هوک useId در ری اکت
هوک useId یکی از هوکهای داخلی React است که برای تولید شناسههای یکتا (unique IDs) مورد استفاده قرار میگیرد. این شناسههای یکتا معمولاً در مواردی مانند ایجاد ارتباط بین عناصر مختلف جهت افزایش دسترسیپذیری، ساخت شناسه برای مجموعهای از عناصر مرتبط مانند لیستهای کشویی یا تبها، و تعیین یک پیشوند مشترک برای تمامی شناسهها در یک کامپوننت کاربرد دارند. همچنین، در پروژههایی که از رندر سمت سرور (SSR) استفاده میکنند، useId این اطمینان را ایجاد میکند که شناسهها در سرور و مرورگر هماهنگ باشند و در نتیجه مشکلی از نظر hydration پیش نیاید. این هوک باعث میشود شناسهها همیشه یکتا، قابل پیشبینی و بدون تداخل باشند و کار با رابطهای تعاملی سادهتر شود.
برای استفاده از هوک useId، باید آن را در بالاترین سطح کامپوننت خود فراخوانی کنید تا یک شناسهی یکتا تولید شود:
import { useId } from 'react';
function PasswordField() {
const passwordHintId = useId();
// ..
میتوانید شناسهای که با useId تولید شده را به ویژگیهای مختلف المنتها بدهید:
<>
<input type="password" aria-describedby={passwordHintId} />
<p id={passwordHintId}>
</>
مثال ساده: ویژگیهای دسترسیپذیری در HTML، مانند aria-describedby، به شما اجازه میدهند بین دو تگ ارتباط معنایی برقرار کنید. برای مثال، میتوانید مشخص کنید که یک عنصر (مثل یک input) توسط عنصر دیگری (مثل یک p) توضیح داده میشود. در HTML معمولی این رابطه بهصورت زیر نوشته میشود:
<label>
Password:
<input
type="password"
aria-describedby="password-hint"
/>
</label>
<p id="password-hint">
The password should contain at least 18 characters
</p>
با این حال، نوشتن ID بهصورت ثابت (hardcoded) در React روش مناسبی نیست. چون ممکن است یک کامپوننت چند بار در یک صفحه رندر شود، اما شناسهها باید یکتا باشند و تکرار آنها باعث بروز خطا در HTML و اختلال در دسترسیپذیری میشود. بهجای این کار، بهتر است از هوک useId استفاده کنید تا برای هر نمونه از کامپوننت، یک ID منحصربهفرد تولید شود:
import { useId } from 'react';
function PasswordField() {
const passwordHintId = useId();
return (
<>
<label>
Password:
<input
type="password"
aria-describedby={passwordHintId}
/>
</label>
<p id={passwordHintId}>
The password should contain at least 18 characters
</p>
</>
);
}
اکنون حتی اگر کامپوننت PasswordField چندین بار در صفحه رندر شود، شناسههایی که با useId تولید شدهاند با یکدیگر تداخل نخواهند داشت. مثلا در حالت زیر:
export default function App() {
return (
<>
<h2>Choose password</h2>
<PasswordField />
<h2>Confirm password</h2>
<PasswordField />
</>
);
}
در این مثال PasswordField دو بار فراخوانی شده، اما باعث مشکل نمی شود، چون هر کدام با شناسه مخصوص به خود اجرا می شوند.
بنابراین، هوک useId یک شناسهی یکتا تولید میکند که مخصوص همان کامپوننت و همان فراخوانی است و در طول عمر کامپوننت تغییر نمیکند. اگر چند بار در یک کامپوننت فراخوانی شود، هر بار یک ID متفاوت میسازد. همچنین اگر کامپوننت در چند جای مختلف استفاده شود، هر نمونه شناسهی منحصربهفرد خود را خواهد داشت.
یکی از نکات مورد توجه درباره این هوک این است که نباید از useId برای تولید key در لیستها استفاده کنید! این هوک برای تولید شناسههای یکتا در ویژگیهایی مانند id کاربرد دارد، نه برای کلید در لیستها. کلیدها باید از دادهها گرفته شوند، پایدار و قابل پیشبینی باشند تا React بتواند بهدرستی تغییرات را مدیریت کند.
موارد استفاده از useId
- تولید شناسههای یکتا برای ویژگیهای مربوط به دسترسیپذیری (مانند aria-* و htmlFor)
- ایجاد شناسه برای چند عنصر مرتبط در یک کامپوننت
- تعیین یک پیشوند مشترک برای همهی شناسههای تولیدشده
- اطمینان از هماهنگی شناسهها بین کلاینت و سرور (در پروژههای SSR)
جمع بندی
در این مقاله با مجموعهای از هوکهای پیشرفته و کمتر شناختهشدهی React آشنا شدیم که هر یک نقش مهمی در بهبود تجربه کاربری، سادهتر شدن مدیریت وضعیت و فراهمکردن کنترل دقیقتر بر روی DOM دارند. هوکهایی مانند useActionState، useTransition، useDeferredValue، useImperativeHandle و useId ابزارهایی هستند که در صورت استفاده بهجا و صحیح، توسعه اپلیکیشنهای واکنشگرا، سریع و قابل نگهداری را بسیار سادهتر میکنند.
هر یک از این هوکها برای سناریوهای خاصی طراحی شدهاند و به توسعهدهندگان کمک میکنند تا کدی حرفهایتر، تمیزتر و کارآمدتر بنویسند.