قدرت هوک useActionState ریاکت را کشف کنید. بیاموزید چگونه مدیریت فرم را ساده کرده، وضعیتهای در حال انتظار را مدیریت میکند و تجربه کاربری را با مثالهای عملی بهبود میبخشد.
هوک useActionState در ریاکت: راهنمای جامع مدیریت مدرن فرمها
دنیای توسعه وب در حال تحول دائمی است و اکوسیستم ریاکت در خط مقدم این تغییر قرار دارد. با نسخههای اخیر، ریاکت ویژگیهای قدرتمندی را معرفی کرده است که اساساً نحوه ساخت اپلیکیشنهای تعاملی و انعطافپذیر را بهبود میبخشد. در میان تأثیرگذارترین این ویژگیها، هوک useActionState قرار دارد که یک تغییردهنده بازی برای مدیریت فرمها و عملیات ناهمزمان است. این هوک که قبلاً در نسخههای آزمایشی با نام useFormState شناخته میشد، اکنون یک ابزار پایدار و ضروری برای هر توسعهدهنده مدرن ریاکت است.
این راهنمای جامع شما را به یک شیرجه عمیق در دنیای useActionState میبرد. ما مشکلاتی که این هوک حل میکند، مکانیکهای اصلی آن، و نحوه استفاده از آن در کنار هوکهای مکملی مانند useFormStatus برای ایجاد تجربیات کاربری برتر را بررسی خواهیم کرد. چه در حال ساخت یک فرم تماس ساده باشید و چه یک اپلیکیشن پیچیده و پر از داده، درک useActionState کد شما را تمیزتر، اعلانیتر (declarative) و قویتر خواهد کرد.
مشکل: پیچیدگی مدیریت وضعیت فرم به روش سنتی
قبل از اینکه بتوانیم ظرافت useActionState را درک کنیم، ابتدا باید چالشهایی را که به آنها میپردازد، بشناسیم. سالها، مدیریت وضعیت فرم در ریاکت شامل یک الگوی قابل پیشبینی اما اغلب دستوپاگیر با استفاده از هوک useState بود.
بیایید یک سناریوی رایج را در نظر بگیریم: یک فرم ساده برای افزودن محصول جدید به یک لیست. ما باید چندین بخش از وضعیت را مدیریت کنیم:
- مقدار ورودی برای نام محصول.
- یک وضعیت بارگذاری یا در حال انتظار (pending) برای ارائه بازخورد به کاربر در حین تماس با API.
- یک وضعیت خطا برای نمایش پیامها در صورت شکست ارسال.
- یک وضعیت موفقیت یا پیام پس از اتمام کار.
یک پیادهسازی معمولی ممکن است چیزی شبیه به این باشد:
مثال: «روش قدیمی» با چندین هوک useState
// تابع API فرضی
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('نام محصول باید حداقل ۳ کاراکتر باشد.');
}
console.log(`محصول "${productName}" اضافه شد.`);
return { success: true };
};
// کامپوننت
{error}import { useState } from 'react';
function OldProductForm() {
const [productName, setProductName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);
setError(null);
try {
await addProductAPI(productName);
setProductName(''); // پاک کردن ورودی در صورت موفقیت
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};
return (
id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>
{isPending ? 'در حال افزودن...' : 'افزودن محصول'}
{error &&
);
}
این رویکرد کار میکند، اما چندین نقطه ضعف دارد:
- کد تکراری (Boilerplate): ما به سه فراخوانی جداگانه useState برای مدیریت چیزی نیاز داریم که از نظر مفهومی یک فرآیند ارسال فرم واحد است.
- مدیریت دستی وضعیت: توسعهدهنده مسئول تنظیم و بازنشانی دستی وضعیتهای بارگذاری و خطا به ترتیب صحیح در یک بلوک try...catch...finally است. این کار تکراری و مستعد خطا است.
- وابستگی شدید (Coupling): منطق مدیریت نتیجه ارسال فرم به شدت با منطق رندر کامپوننت در هم تنیده است.
معرفی useActionState: یک تغییر پارادایم
useActionState یک هوک ریاکت است که به طور خاص برای مدیریت وضعیت یک عملیات ناهمزمان، مانند ارسال فرم، طراحی شده است. این هوک با اتصال مستقیم وضعیت به نتیجه تابع اکشن، کل فرآیند را ساده میکند.
امضای آن واضح و مختصر است:
const [state, formAction] = useActionState(actionFn, initialState);
بیایید اجزای آن را بررسی کنیم:
actionFn(previousState, formData)
: این تابع ناهمزمان شماست که کار را انجام میدهد (مثلاً فراخوانی یک API). این تابع وضعیت قبلی و دادههای فرم را به عنوان آرگومان دریافت میکند. نکته مهم این است که هر چیزی که این تابع برمیگرداند، به وضعیت جدید تبدیل میشود.initialState
: این مقدار اولیه وضعیت قبل از اولین اجرای اکشن است.state
: این وضعیت فعلی است. در ابتدا مقدار initialState را نگه میدارد و پس از هر اجرا به مقدار بازگشتی actionFn شما بهروز میشود.formAction
: این یک نسخه جدید و بستهبندی شده (wrapped) از تابع اکشن شماست. شما باید این تابع را به پراپaction
عنصر<form>
بدهید. ریاکت از این تابع بستهبندی شده برای ردیابی وضعیت در حال انتظار (pending) اکشن استفاده میکند.
مثال عملی: بازنویسی کد با useActionState
حالا، بیایید فرم محصول خود را با استفاده از useActionState بازنویسی کنیم. بهبود بلافاصله مشهود است.
ابتدا، باید منطق اکشن خود را تطبیق دهیم. به جای پرتاب خطا (throwing errors)، اکشن باید یک شیء وضعیت را برگرداند که نتیجه را توصیف میکند.
مثال: «روش جدید» با useActionState
// تابع اکشن، طراحی شده برای کار با useActionState
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // شبیهسازی تأخیر شبکه
if (!productName || productName.length < 3) {
return { message: 'نام محصول باید حداقل ۳ کاراکتر باشد.', success: false };
}
console.log(`محصول "${productName}" اضافه شد.`);
// در صورت موفقیت، یک پیام موفقیت برگردانید و فرم را پاک کنید.
return { message: `"${productName}" با موفقیت اضافه شد`, success: true };
};
// کامپوننت بازنویسی شده
{state.message} {state.message}import { useActionState } from 'react';
// نکته: در بخش بعدی useFormStatus را برای مدیریت وضعیت در حال انتظار اضافه خواهیم کرد.
function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
ببینید چقدر تمیزتر شده است! ما سه هوک useState را با یک هوک useActionState جایگزین کردیم. مسئولیت کامپوننت اکنون صرفاً رندر کردن UI بر اساس شیء `state` است. تمام منطق کسبوکار به طور مرتب در تابع `addProductAction` کپسوله شده است. وضعیت به طور خودکار بر اساس آنچه اکشن برمیگرداند بهروز میشود.
اما صبر کنید، وضعیت در حال انتظار (pending) چه میشود؟ چگونه دکمه را در حین ارسال فرم غیرفعال کنیم؟
مدیریت وضعیتهای در حال انتظار با useFormStatus
ریاکت یک هوک همراه به نام useFormStatus ارائه میدهد که دقیقاً برای حل این مشکل طراحی شده است. این هوک اطلاعات وضعیت آخرین ارسال فرم را ارائه میدهد، اما با یک قانون حیاتی: باید از کامپوننتی فراخوانی شود که در داخل <form>
که میخواهید وضعیت آن را ردیابی کنید، رندر شده باشد.
این کار به جداسازی تمیز مسئولیتها تشویق میکند. شما یک کامپوننت مخصوص برای عناصر UI ایجاد میکنید که باید از وضعیت ارسال فرم آگاه باشند، مانند دکمه ارسال.
هوک useFormStatus یک شیء با چندین پراپرتی برمیگرداند که مهمترین آنها `pending` است.
const { pending, data, method, action } = useFormStatus();
pending
: یک مقدار بولین که اگر فرم والد در حال ارسال باشد `true` و در غیر این صورت `false` است.data
: یک شیء `FormData` حاوی دادههای در حال ارسال.method
: یک رشته که متد HTTP را نشان میدهد (`'get'` یا `'post'`).action
: ارجاعی به تابعی که به پراپ `action` فرم داده شده است.
ایجاد یک دکمه ارسال آگاه از وضعیت
بیایید یک کامپوننت اختصاصی `SubmitButton` ایجاد کرده و آن را در فرم خود ادغام کنیم.
مثال: کامپوننت SubmitButton
import { useFormStatus } from 'react-dom';
// نکته: useFormStatus از 'react-dom' وارد میشود، نه 'react'.
function SubmitButton() {
const { pending } = useFormStatus();
return (
{pending ? 'در حال افزودن...' : 'افزودن محصول'}
);
}
حالا، میتوانیم کامپوننت فرم اصلی خود را برای استفاده از آن بهروز کنیم.
مثال: فرم کامل با useActionState و useFormStatus
{state.message} {state.message}import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (تابع addProductAction بدون تغییر باقی میماند)
function SubmitButton() { /* ... همانطور که در بالا تعریف شد ... */ }
function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{/* میتوانیم یک کلید (key) اضافه کنیم تا ورودی را در صورت موفقیت ریست کنیم */}
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
با این ساختار، کامپوننت `CompleteProductForm` نیازی به دانستن چیزی در مورد وضعیت در حال انتظار ندارد. `SubmitButton` کاملاً خودکفا است. این الگوی ترکیبی (compositional) برای ساخت UIهای پیچیده و قابل نگهداری فوقالعاده قدرتمند است.
قدرت بهبود تدریجی (Progressive Enhancement)
یکی از عمیقترین مزایای این رویکرد جدید مبتنی بر اکشن، به ویژه هنگامی که با Server Actions استفاده میشود، بهبود تدریجی خودکار است. این یک مفهوم حیاتی برای ساخت اپلیکیشنها برای مخاطبان جهانی است، جایی که شرایط شبکه میتواند غیرقابل اعتماد باشد و کاربران ممکن است دستگاههای قدیمیتر داشته باشند یا جاوا اسکریپت را غیرفعال کرده باشند.
نحوه کار آن به این صورت است:
- بدون جاوا اسکریپت: اگر مرورگر کاربر جاوا اسکریپت سمت کلاینت را اجرا نکند، `<form action={...}>` مانند یک فرم HTML استاندارد عمل میکند. این یک درخواست تمام صفحه به سرور ارسال میکند. اگر از فریمورکی مانند Next.js استفاده میکنید، اکشن سمت سرور اجرا میشود و فریمورک کل صفحه را با وضعیت جدید (مثلاً نمایش خطای اعتبارسنجی) دوباره رندر میکند. اپلیکیشن کاملاً کاربردی است، فقط بدون روانی یک SPA.
- با جاوا اسکریپت: هنگامی که بسته جاوا اسکریپت بارگیری میشود و ریاکت صفحه را هیدراته (hydrate) میکند، همان `formAction` در سمت کلاینت اجرا میشود. به جای بارگذاری مجدد کل صفحه، مانند یک درخواست fetch معمولی عمل میکند. اکشن فراخوانی میشود، وضعیت بهروز میشود، و فقط بخشهای لازم کامپوننت دوباره رندر میشوند.
این بدان معناست که شما منطق فرم خود را یک بار مینویسید، و در هر دو سناریو به طور یکپارچه کار میکند. شما به طور پیشفرض یک اپلیکیشن انعطافپذیر و در دسترس میسازید که یک برد بزرگ برای تجربه کاربری در سراسر جهان است.
الگوهای پیشرفته و موارد استفاده
۱. Server Actions در مقابل Client Actions
`actionFn` که به useActionState میدهید میتواند یک تابع async استاندارد سمت کلاینت (مانند مثالهای ما) یا یک Server Action باشد. یک Server Action تابعی است که در سرور تعریف شده و میتواند مستقیماً از کامپوننتهای کلاینت فراخوانی شود. در فریمورکهایی مانند Next.js، شما با افزودن دستورالعمل "use server";
در بالای بدنه تابع، یکی از آنها را تعریف میکنید.
- Client Actions: ایدهآل برای تغییراتی (mutations) که فقط بر وضعیت سمت کلاینت تأثیر میگذارند یا APIهای شخص ثالث را مستقیماً از کلاینت فراخوانی میکنند.
- Server Actions: عالی برای تغییراتی که شامل پایگاه داده یا سایر منابع سمت سرور هستند. آنها با حذف نیاز به ایجاد دستی نقاط پایانی API برای هر تغییر، معماری شما را ساده میکنند.
زیبایی کار اینجاست که useActionState با هر دو به طور یکسان کار میکند. شما میتوانید یک اکشن کلاینت را با یک اکشن سرور جایگزین کنید بدون اینکه کد کامپوننت را تغییر دهید.
۲. بهروزرسانیهای خوشبینانه (Optimistic Updates) با `useOptimistic`
برای حسی حتی پاسخگوتر، میتوانید useActionState را با هوک useOptimistic ترکیب کنید. بهروزرسانی خوشبینانه زمانی است که شما UI را بلافاصله بهروز میکنید، با *فرض* اینکه عملیات ناهمزمان موفق خواهد بود. اگر شکست بخورد، شما UI را به وضعیت قبلی خود باز میگردانید.
یک اپلیکیشن رسانه اجتماعی را تصور کنید که در آن یک نظر اضافه میکنید. به طور خوشبینانه، شما نظر جدید را فوراً در لیست نشان میدهید در حالی که درخواست به سرور ارسال میشود. useOptimistic طوری طراحی شده است که دست در دست با اکشنها کار کند تا پیادهسازی این الگو را ساده کند.
۳. ریست کردن فرم در صورت موفقیت
یک نیاز رایج، پاک کردن ورودیهای فرم پس از ارسال موفقیتآمیز است. چند راه برای دستیابی به این هدف با useActionState وجود دارد.
- ترفند پراپ Key: همانطور که در مثال `CompleteProductForm` نشان داده شد، میتوانید یک `key` منحصربهفرد به یک ورودی یا کل فرم اختصاص دهید. وقتی کلید تغییر میکند، ریاکت کامپوننت قدیمی را unmount کرده و یک کامپوننت جدید را mount میکند که به طور موثر وضعیت آن را ریست میکند. گره زدن کلید به یک پرچم موفقیت (`key={state.success ? 'success' : 'initial'}`) یک روش ساده و مؤثر است.
- کامپوننتهای کنترلشده (Controlled Components): شما هنوز هم میتوانید در صورت نیاز از کامپوننتهای کنترلشده استفاده کنید. با مدیریت مقدار ورودی با useState، میتوانید تابع setter را برای پاک کردن آن در داخل یک useEffect که به وضعیت موفقیت از useActionState گوش میدهد، فراخوانی کنید.
اشتباهات رایج و بهترین شیوهها
- محل قرارگیری
useFormStatus
: به یاد داشته باشید، کامپوننتی که useFormStatus را فراخوانی میکند باید به عنوان فرزند<form>
رندر شود. اگر خواهر و برادر (sibling) یا والد آن باشد کار نخواهد کرد. - وضعیت قابل سریالسازی (Serializable): هنگام استفاده از Server Actions، شیء وضعیتی که از اکشن شما بازگردانده میشود باید قابل سریالسازی باشد. این بدان معناست که نمیتواند شامل توابع، Symbolها یا سایر مقادیر غیرقابل سریالسازی باشد. به اشیاء ساده، آرایهها، رشتهها، اعداد و مقادیر بولین پایبند باشید.
- در اکشنها خطا پرتاب نکنید: به جای `throw new Error()`، تابع اکشن شما باید خطاها را به آرامی مدیریت کرده و یک شیء وضعیت را برگرداند که خطا را توصیف میکند (مثلاً `{ success: false, message: 'خطایی رخ داد' }`). این تضمین میکند که وضعیت همیشه به طور قابل پیشبینی بهروز میشود.
- یک شکل وضعیت واضح تعریف کنید: از ابتدا یک ساختار ثابت برای شیء وضعیت خود ایجاد کنید. شکلی مانند `{ data: T | null, message: string | null, success: boolean, errors: Record
| null }` میتواند بسیاری از موارد استفاده را پوشش دهد.
useActionState در مقابل useReducer: یک مقایسه سریع
در نگاه اول، useActionState ممکن است شبیه به useReducer به نظر برسد، زیرا هر دو شامل بهروزرسانی وضعیت بر اساس وضعیت قبلی هستند. با این حال، آنها اهداف متفاوتی را دنبال میکنند.
useReducer
یک هوک همهمنظوره برای مدیریت انتقالهای پیچیده وضعیت در سمت کلاینت است. این هوک با ارسال (dispatch) اکشنها فعال میشود و برای منطق وضعیتی که تغییرات وضعیت ممکن و همزمان زیادی دارد (مانند یک ویزارد چند مرحلهای پیچیده) ایدهآل است.useActionState
یک هوک تخصصی است که برای وضعیتی طراحی شده که در پاسخ به یک عملیات واحد و معمولاً ناهمزمان تغییر میکند. نقش اصلی آن ادغام با فرمهای HTML، Server Actions و ویژگیهای رندر همزمان ریاکت مانند انتقالهای وضعیت در حال انتظار است.
نکته کلیدی: برای ارسال فرمها و عملیات ناهمزمان مرتبط با فرمها، useActionState ابزار مدرن و هدفمندی است. برای سایر ماشینهای وضعیت پیچیده سمت کلاینت، useReducer همچنان یک انتخاب عالی است.
نتیجهگیری: استقبال از آینده فرمها در ریاکت
هوک useActionState چیزی بیش از یک API جدید است؛ این نشاندهنده یک تغییر اساسی به سمت روشی قویتر، اعلانیتر و کاربرمحورتر برای مدیریت فرمها و تغییرات داده در ریاکت است. با پذیرش آن، شما به دست میآورید:
- کاهش کد تکراری: یک هوک واحد جایگزین چندین فراخوانی useState و هماهنگی دستی وضعیت میشود.
- وضعیتهای در حال انتظار یکپارچه: به طور یکپارچه UIهای بارگذاری را با هوک همراه useFormStatus مدیریت کنید.
- بهبود تدریجی داخلی: کدی بنویسید که با یا بدون جاوا اسکریپت کار کند و دسترسی و انعطافپذیری را برای همه کاربران تضمین کند.
- ارتباط ساده با سرور: یک تناسب طبیعی برای Server Actions که تجربه توسعه فول-استک را ساده میکند.
همانطور که پروژههای جدیدی را شروع میکنید یا پروژههای موجود را بازنویسی میکنید، به سراغ useActionState بروید. این نه تنها تجربه توسعهدهنده شما را با تمیزتر و قابل پیشبینیتر کردن کدتان بهبود میبخشد، بلکه شما را قادر میسازد تا اپلیکیشنهای با کیفیتتری بسازید که سریعتر، انعطافپذیرتر و برای مخاطبان متنوع جهانی در دسترس باشند.