فرم چندمرحلهای در React؛ هر قدم را جدا اعتبار کن، ارسال را یکجا
فرم طولانی را یکجا نشان دادن کاربر را خسته میکند. شکستن به چند قدم بار ذهنی را کم میکند، به شرطی که قدمها واقعاً گروه معنایی باشند نه صرفاً برش یک ستون دیتابیس. عدد تبدیل جادویی ندارم و نقل نمیکنم. چیزی که در عمل دیدهام این است: اگر کاربر نداند کجای کار است، یا با برگشت به عقب دادهاش پاک شود، چندمرحلهای از فرم ساده بدتر است.
این نوشته درباره ثبتنام، درخواست خدمت، یا ویزارد پیکربندی در React است. ابزار مشخص را دین نکن. Formik سالها کار را راه انداخت. امروز در کدبیس Next.js بیشتر React Hook Form کنار Zod میبینم، یا حتی state محلی اگر فرم کوچک است. انتخاب کتابخانه مسئله دوم است. مدل داده و مرز اعتبار مسئله اول است.
قبل از کامپوننت، قدمها را روی کاغذ بچین
سؤالها را از «چه فیلدی داریم» شروع نکن. از «کاربر در هر نشست ذهنی چه تصمیمی میگیرد» شروع کن. نام و ایمیل یک تصمیم است. ساخت رمز و تکرار آن تصمیم بعدی است. ترجیح اعلان، اگر واقعاً لازم است، آخر صف است نه وسط هویت.
هر قدم باید بدون قدمهای بعدی قابل فهم باشد. اگر فیلد قدم سوم فقط در کنار فیلد قدم اول معنی دارد، گروهبندی غلط است. چهار قدم برای بیست فیلد معمولاً زیاد است. سه قدم شفاف بهتر از هفت قدم با یک فیلد است.
قدم آخر را مرور بگذار نه فرم تازه. کاربر باید ببیند چه چیزی ارسال میشود. این همان جایی است که غلط املایی ایمیل را خودش میگیرد، به شرطی که مرور واقعاً مقدار را نشان دهد نه فقط بگوید «همهچیز آماده است».
یک آبجکت، نه چند جزیره state
حالت فرم را یک آبجکت نگه دار که از اول شکل کامل پیشنویس را دارد، حتی اگر بعضی کلیدها هنوز خالیاند. بالا و پایین رفتن بین قدمها فقط اندیس را عوض میکند، نه حافظه را.
پاک کردن state قدم قبلی هنگام Next، باگ کلاسیک است. کاربر برمیگردد و فیلد خالی میبیند و مطمئن میشود محصول به او احترام نمیگذارد. اگر قدم را unmount میکنی، داده باید بیرون از آن کامپوننت بماند: در والد، در context کوچک همین ویزارد، یا در خودِ کتابخانه فرم.
URL را برای قدم حساس در نظر بگیر. `?step=2` یا مسیر جدا، اگر کاربر رفرش کرد یا لینک را برای خودش نگه داشت، کمتر گیج میشود. برای فرم کوتاه، state محلی کافی است. برای درخواستی که پر کردنش ده دقیقه طول میکشد، پیشنویس را جایی پایدارتر از حافظه تب بگذار و صریح بگو چه چیزی ذخیره شده. ذخیره بیخبر، خودش مسئله حریم خصوصی است.
Redux برای یک فرم تقریباً همیشه اضافه است. اگر اپ از قبل استور سراسری دارد، باز هم پیشنویس فرم را داخل همان استور جهانی نریز مگر اینکه چند صفحه واقعاً به آن نیاز داشته باشند. عمر پیشنویس کوتاهتر از عمر اپ است.
اعتبار را دو لایه کن
لایه اول: آیا کاربر حق دارد از این قدم بگذرد؟ لایه دوم: آیا کل سند برای سرور قابل قبول است؟
لایه اول باید روی همان فیلدها باشد. ایمیل را با قاعده ساده و پیام فارسی روشن چک کن. رمز را با قانونی که واقعاً اجرا میکنی، نه با جملهای که UI میگوید و API نادیده میگیرد. خطای قدم دیگر را وسط این قدم نشان نده.
لایه دوم اسکیمای کامل است، همان چیزی که سرور هم باید تکرار کند. کلاینت برای تجربه است. سرور برای صحت است. اگر فقط Yup یا Zod در مرورگر باشد، یک کلاینت دیگر میانبر میزند. در Next.js این اسکیما را بگذار پشت Server Action یا Route Handler، و همان را، اگر اندازه باندل مهم است، آگاهانه در کلاینت هم استفاده کن. اشتراک کد خوب است؛ کپی قانون با دو معنی مختلف خوب نیست.
اعتبار را موقع رفتن به قدم بعد اجرا کن، نه با هر کلید اگر فرم سنگین است. برای فیلدهای حساس مثل ایمیل، اعتبار بعد از blur معمولاً مودبتر از فریاد زدن وسط تایپ است. بعد از اولین تلاش ناموفق، میتوانی همان فیلد را با تغییر مقدار دوباره بسنجی تا کاربر اثر اصلاح را ببیند.
ناوبری و خطا را همسطح محصول بساز
دکمه بازگشت باید بدون اعتبارِ مانع کار کند. برگشتن حق کاربر است، حتی اگر همین قدم ناقص باشد. دکمه ادامه فقط اگر لایه اول قبول کرد فعال شود، یا اگر نشد، خطا را به فیلد مربوط وصل کن و تمرکز را همانجا ببر.
نشانگر پیشرفت را به تعداد فیلد گره نزن. «قدم ۲ از ۴» کافی است اگر عنوان قدم هم دیده شود. نوار درصدی که با یک فیلد اختیاری سی درصد میپرد، اطلاعات جعلی است.
خطای سرور را به قدم درست برگردان. اگر API گفت ایمیل تکراری است و تو فقط یک toast کلی نشان دادی، کاربر باید حدس بزند کدام قدم. کد خطا را به اندیس قدم نگاشت کن، مقدارهای دیگر را نگه دار، و همان فیلد را برجسته کن.
ارسال دوباره را جدی بگیر. دکمه را تا تمام شدن درخواست غیرفعال کن. اگر درخواست واقعاً چیزی میسازد، کلید یکتا بفرست تا retry شبکه دو رکورد نسازد. پیام موفقیت باید بگوید چه اتفاقی افتاد، نه فقط «موفق». اگر مرحله بعد تأیید ایمیل است، همان را بنویس.
دسترسی و ساختار
هر قدم یک عنوان باشد. فیلدها label واقعی داشته باشند، نه فقط placeholder. خطا با `aria-describedby` به ورودی وصل شود. تمرکز بعد از خطای ادامه روی اولین فیلد نامعتبر برود. با صفحهکلید بشود تمام مسیر را رفت.
اگر قدمها را با تب نشان میدهی، الگوی تب و الگوی ویزارد را قاطی نکن. ویزارد ترتیب دارد؛ تب ترتیب را تضمین نمیکند. برای ویزارد، دکمه صریح بهتر از تب است.
کامپوننت هر قدم را جدا کن تا بشود بدون مرورگر تست کرد: با این مقدار، آیا اجازه عبور هست، پیام خطا چیست، و آیا بازگشت داده را پاک میکند. این تست ارزان است و بیشتر باگهای ویزارد همینجا هستند. E2E را برای یک مسیر کامل تا پاسخ سرور بگذار، نه برای هر قاعده رمز.
کجا فرم چندمرحلهای نساز
دو فیلد. یا سه فیلد که همه برای یک تصمیم لازماند. چندمرحلهای کردنشان فقط کلیک اضافه است. همچنین اگر پشت هر قدم یک درخواست شبکه اجباری و کند داری، کاربر فکر میکند محصول گیر کرده. اعتبار محلی را فوری نگه دار و کار سنگین را برای ارسال آخر، مگر اینکه قدم واقعاً به جواب سرور وابسته باشد، مثل بررسی دامنه سازمان.
ویزارد را با انیمیشن اسلاید قایم نکن. اگر جهت حرکت برای کاربر نامفهوم است، انیمیشن کمکی نکرده. حرکت کوتاه و قابل قطع بهتر از نمایش تئاتری است.
پرسشهای کوتاه
Formik هنوز غلط است؟ نه. اگر کدبیس با آن پایدار است، بازنویسی بهخاطر مد بیدلیل است. برای فرم تازه در اپ TypeScript، اسکیمای مشترک با سرور معمولاً مهمتر از نام کتابخانه است.
میشود اعتبار همه قدمها را آخر کار یکجا نشان داد؟ میشود و کاربر متنفر میشود. مانع عبور را همان قدم بگیر، اسکیمای کامل را موقع ارسال تکرار کن.
پیشنویس را در localStorage بگذارم؟ برای داده کمخطر و دستگاه شخصی گاهی بله. برای رمز، مدرک هویتی، یا سیستم مشترک، نه. اگر ذخیره میکنی، پاک شدن بعد از ارسال موفق را هم بنویس.