پاسخ به ورودی به کمک state
ری اکت یک روش اعلانی برای ایجاد تغییر در رابط کاربری فراهم کرده است.به جای دستکاری مستقیم هر بخش رابط کاربری به تنهایی،شما state های متفاوتی را که کامپوننت مورد نظرتان می تواند داشته باشد تعریف کرده،و در پاسخ به ورودی کاربر بین آنها جا به جا شوید.این روش مشابه دیدگاه طراحان درباره رابط کاربری است.
You will learn
- تفاوت برنامه نویسی رابط کاربری اعلانی و برنامه نویسی رابط کاربری دستوری
- برشمردن state های بصری که کامپوننت شما می تواند داشته باشد
- چگونه تغییرات بین state های بصری مختلف را از طریق کد اجرا کنیم؟
چگونه رابط کاربری اعلانی با دستوری مقایسه می شود
وقتی شما تعاملات رابط کاربری را طراحی می کنید،احتمالا به چگونگی تغییرات رابط کاربری در پاسخ به اقدامات کاربر فکر می کنید.فرمی را در نظر بگیرید که امکان ارسال پاسخی را به کاربر می دهد:
- وقتی شما چیزی داخل فرم تایپ می کنید،دکمه “ارسال” فعال می شود.
- وقتی شما دکمه “ارسال” را فشار می دهید،دکمه و فرم غیرفعال میشوند. و یک اسپینر ظاهر می شود.
- اگر درخواست شبکه موفقیت آمیز باشد،فرم پنهان شده، و پیام “تشکر” ظاهر می شود.
- اگر درخواست شبکه ناموفق باشد، یک پیغام خطا ظاهر شده، و فرم دوباره فعال می شود.
در برنامه نویسی دستوری، موارد فوق مستقیما با نحوه پیاده سازی تعامل توسط شما مرتبط است. شما ملزم هستید که دستورالعمل های دقیق برای دستکاری رابط کاربری بر اساس آنچه رخ می دهد را، بنویسید.روش دیگری برای تصور این موضوع این است که: فرض کنید که در کنار شخصی در یک ماشین سوار می شوید و قدم به قدم به او می گویید که کجا برود.
Illustrated by Rachel Lee Nabors
او نمی داند که شما می خواهید به کجا بروید،و تنها دستورات شما را دنبال می کند.(و اگر مسیرها را اشتباه بگیرید، به مقصد اشتباه می رسید!) به این روش،دستوری می گویند زیرا شما باید به هر المنت اعم از اسپینر و دکمه فرمان بدهید و به کامپیوتر بگویید که رابط کاربری را چگونه بروزرسانی کند.
در این مثال از برنامه نویسی رابط کاربری دستوری، فرم بدون استفاده از ری اکت ساخته می شود؛ و تنها از DOM مرورگر استفاده می کند :
async function handleFormSubmit(e) { e.preventDefault(); disable(textarea); disable(button); show(loadingMessage); hide(errorMessage); try { await submitForm(textarea.value); show(successMessage); hide(form); } catch (err) { show(errorMessage); errorMessage.textContent = err.message; } finally { hide(loadingMessage); enable(textarea); enable(button); } } function handleTextareaChange() { if (textarea.value.length === 0) { disable(button); } else { enable(button); } } function hide(el) { el.style.display = 'none'; } function show(el) { el.style.display = ''; } function enable(el) { el.disabled = false; } function disable(el) { el.disabled = true; } function submitForm(answer) { // فرض کنید که درخواست به شبکه ارسال می شود. return new Promise((resolve, reject) => { setTimeout(() => { if (answer.toLowerCase() === 'istanbul') { resolve(); } else { reject(new Error('Good guess but a wrong answer. Try again!')); } }, 1500); }); } let form = document.getElementById('form'); let textarea = document.getElementById('textarea'); let button = document.getElementById('button'); let loadingMessage = document.getElementById('loading'); let errorMessage = document.getElementById('error'); let successMessage = document.getElementById('success'); form.onsubmit = handleFormSubmit; textarea.oninput = handleTextareaChange;
دستکاری رابط کاربری به شکل دستوری برای مثالهای خاص به خوبی عمل می کند،اما مدیریت آن در سیستمهای پیچیده تر بطور تصاعدی دشوارتر می شود.بروزرسانی صفحه ای پر از فرم های مختلف مانند این مورد را در نظر بگیرید.اضافه کردن یک المنت جدید رابط کاربری یا یک تعامل جدید نیازمند بررسی دقیق کدهای موجود است که مطمئن شوید باگ جدیدی تولید نکرده اید (مثلا فراموش کردن نمایش دادن یا پنهان کردن چیزی).
ری اکت به وجود آمد تااین مشکل را حل کند.
در ری اکت، شما مجبور نیستید مستقیما رابط کاربری را دستکاری کنید—به این معنا که فعال کردن،غیرفعال کردن،نمایش، و یا پنهان کردن کامپوننتها را به طور مستقیم انجام نمی دهید. درعوض، شما چیزی که می خواهید نمایش دهید را اعلام می کنید، و ری اکت می فهمد چگونه رابط کاربری را بروزرسانی کند.فرض کنید که سوار تاکسی شوید و به جای آنکه به راننده بگویید دقیقا به کدام طرف بپیچد،بگویید که کجا می خواهید بروید.این وظیفه راننده است که شما را به مقصد برساند، وممکن است حتی میانبرهایی را بلد باشد که شما در نظر نگرفته اید!
Illustrated by Rachel Lee Nabors
فکر کردن درباره رابط کاربری بصورت اعلانی
شما نحوه پیاده سازی یک فرم به صورت دستوری را بالاتر دیدید.برای درک بهتر نحوه تفکر در ری اکت، درزیر به پیاده سازی مجدد این رابط کاربری در ری اکت خواهید پرداخت :
- state های بصری مختلف کامپوننت خود را شناسایی کنید
- تعیین کنید چه چیزی باعث آن تغییرات state می شود
- state را در حافظه با استفاده از
useState
نشان دهید - هرگونه متغیر state غیرضروری را حذف کنید
- event handler ها را برای مقداردهی state متصل کنید
قدم اول : state های بصری مختلف کامپوننت خود را شناسایی کنید
در علوم کامپیوتر،ممکن است درباره “state machine” که در یکی از چندین “states” قرار دارد بشنوید.اگر با یک طراح کار می کنید،ممکن است نمونه هایی برای “state های بصری” مختلف دیده باشید.ری اکت در تقاطع طراحی و علوم کامپیوتر قرار دارد، بنابراین هر دوی این ایده ها منابع الهام هستند.
ابتدا،شما نیاز دارید که تمام “state” های مختلف رابط کاربری را که کاربر ممکن است ببیند را بصری سازی کنید :
- خالی: فرم دارای یک دکمه “ارسال” غیرفعال است.
- در حال تایپ: فرم دارای یک دکمه “ارسال” فعال است.
- در حال ارسال: فرم کاملاً غیرفعال است.اسپینر نمایش داده می شود.
- موفقیت: پیام “متشکرم” به جای فرم نمایش داده می شود.
- خطا: مانند در حال تایپ، اما با یک پیغام خطای اضافی.
درست مانند یک طراح، قبل از اینکه منطق را اضافه کنید می خواهید برای state های مختلف “نمونه های اولیه” یا “نمونه” ایجاد کنید.برای مثال، در اینجا یک نمونه فقط برای بخش بصری فرم آورده شده است.این نمونه با یک prop به نام status
با مقدار پیشفرض 'empty'
کنترل می شود:
export default function Form({ status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea /> <br /> <button> Submit </button> </form> </> ) }
شما می توانستید آن prop را هرچیزی بنامید، نام گذاری اهمیتی ندارد.status = 'empty'
را به status = 'success'
ویرایش کنید تا پیغام موفقیت ظاهر شود.نمونه سازی به شما اجازه می دهد قبل از وارد کردن منطق به کد روی رابط کاربری تکرار انجام دهید. اینجا یک نمونه پیاده سازی بیشتر از همان کامپوننت وجود دارد که همچنان با status
prop ” کنترل می شود”:
export default function Form({ // 'submitting', 'error', 'success' را امتحان کنید: status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea disabled={ status === 'submitting' } /> <br /> <button disabled={ status === 'empty' || status === 'submitting' }> Submit </button> {status === 'error' && <p className="Error"> Good guess but a wrong answer. Try again! </p> } </form> </> ); }
Deep Dive
اگر یک کامپوننت دارای تعداد زیادی state های بصری باشد،نمایش همه آنها در یک صفحه راحت تر خواهد بود :
import Form from './Form.js'; let statuses = [ 'empty', 'typing', 'submitting', 'success', 'error', ]; export default function App() { return ( <> {statuses.map(status => ( <section key={status}> <h4>Form ({status}):</h4> <Form status={status} /> </section> ))} </> ); }
صفحاتی مانند این اغلب به نام ‘living styleguides’ یا ‘storybooks’ شناخته می شوند.
قدم دوم : تعیین کنید چه چیزی باعث آن تغییرات state می شود
شما می توانید در پاسخ به دو نوع ورودی بروزرسانی های state را فعال کنید:
- ورودی های کاربر، مانند کلیک یک دکمه، تایپ کردن در یک فیلد، پیمایش یک پیوند.
- ورودی های کامپیوتر، مانند دریافت پاسخ از شبکه، اتمام زمان مقرر، بارگذاری یک تصویر.
Illustrated by Rachel Lee Nabors
در هر دو مورد، شما باید برای بروزرسانی رابط کاربری متغیرهای state را تنظیم کنید. برای فرمی که در حال توسعه آن هستید، نیاز به تغییر state در پاسخ به چند ورودی متفاوت دارید:
- تغییر ورودی متن (کاربر) باید آن را از state خالی به درحال تایپ یا برعکس تغییر دهد، بسته به اینکه ورودی متن خالی است یا خیر.
- کلیک بر روی دکمه ارسال (کاربر) باید آن را به state در حال ارسال تغییر دهد.
- پاسخ موفق شبکه (کامپیوتر) باید آن را به state موفقیت تغییر دهد.
- پاسخ ناموفق شبکه (کامپیوتر) باید آن را به state خطا با پیغام خطای متناسب تغییر دهد.
برای کمک به تصویر سازی این گردش کار، سعی کنید هر state را به عنوان یک دایره برچسب دار، و هر تغییر بین دو state را به عنوان یک پیکان ترسیم کنید.به این ترتیب می توانید چندین گردش کار را ترسیم کرده و باگ ها را قبل از پیاده سازی حل کنید.
قدم سوم: state را در حافظه با استفاده از useState
نشان دهید
حال شما باید state های بصری کامپوننت خود را در حافظه با useState
نشان دهید. سادگی کلید است: هر قطعه state یک “قطعه متحرک” است، و شما کمترین تعداد “قطعه متحرک” ممکن را می خواهید. پیچیدگی بیشتر به باگهای بیشتر منجر می شود!
با state ای شروع کنید که حتما باید وجود داشته باشد. مثلا، شما به answer
برای ذخیره ورودی، و به error
(اگر موجود باشد) برای ذخیره آخرین error نیاز دارید:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
سپس، شما نیاز به یک متغیر state دارید که نمایانگر state های بصری که می خواهید نمایش داده شوند باشد. معمولا بیش از یک راه برای نمایش آن در حافظه وجود دارد،پس شما باید با آن آزمایش کنید.
اگر شما در تلاش هستید که به سرعت به بهترین راه دست یابید با افزودن تعداد کافی state آغاز کنید که قطعا مطمئن باشید تمام حالات بصری ممکن پوشش داده شده است:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
احتمالا اولین ایده شما بهترین نخواهد بود، اما اشکالی ندارد—بازسازی state بخشی از فرایند است!
قدم چهارم: هرگونه متغیر state غیرضروری را حذف کنید
شما می خواهید از تکرار محتوای state جلوگیری کنید بنابراین فقط چیزی که ضروری است را دنبال می کنید. صرف زمان اندک روی بازسازی ساختار state باعث فهم ساده تر کامپوننت شما، کاهش تکرار، و اجتناب از معانی ناخواسته خواهد شد. هدف شما جلوگیری از مواردی است که state موجود در حافظه، رابط کاربری صحیحی که شما بخواهید کاربر ببیند را نمایش نمی دهد. (مثلا، شما هرگز نمی خواهید که همزمان پیغام خطا نمایش داده شود و فیلد ورودی هم غیرفعال باشد، یا کاربر قادر به تصحیح خطا نباشد!)
اینجا تعدادی پرسش وجود دارد که می توانید درباره متغیرهای state بپرسید:
- آیا این state باعث تناقض می شود؟ مثلا،
isTyping
وisSubmitting
نمیتوانند همزمانtrue
باشند. یک تناقض معمولا به این معناست که یک state به اندازه کافی محدود نیست. چهار ترکیب ممکن از دو مقدار بولین وجود دارد، اما تنها سه مورد با state های معتبر مطابقت دارند. برای حذف state “غیرممکن”، می توانید این موارد را در یکstatus
ترکیب کنید که باید یکی از این سه مقادیر باشد:'typing'
،'submitting'
، یا'success'
. - آیا همان اطلاعات از قبل در state دیگری موجود است؟ یک تناقض دیگر:
isEmpty
وisTyping
نمی توانند همزمانtrue
باشند. با جدا کردن آن ها به عنوان متغیرهای state، خطر عدم هماهنگی بین آنها و تولید باگ وجود دارد. خوشبختانه، شما می توانیدisEmpty
را حذف کنید و به جای آنanswer.length === 0
را بررسی کنید. - آیا می توانید همان اطلاعات را از معکوس یک متغیر state دیگر به دست آورید؟ نیازی به
isError
نیست زیرا شما می توانیدerror !== null
را به جای آن بررسی کنید.
با جمع بندی این بخش، شما به سه متغیر state ضروری محدود شده اید (کاهش یافته از 7!):
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'.
شما میدانید که این سه متغیر ضروری هستند چرا که نمیتوانید هیچکدام از آنها را بدون اینکه عملکرد برنامه را دچار مشکل کنید حذف کنید.
Deep Dive
این سه متغیر نمایانگری مناسب از state این فرم هستند. اگرچه هنوز برخی از state های میانی وجود دارند که کاملا منطقی نیستند. مثلا، وقتی که status
'success'
است یک error
غیر null منطقی نیست. برای مدلسازی دقیقتر state، شما می توانید آن را به یک reducer منتقل کنید. Reducer ها به شما این امکان را می دهند که چندین متغیر state را در یک شی واحد ترکیب کنید و تمامی منطق مربوطه را یکپارچه سازید!
قدم پنجم: event handler ها را برای مقداردهی state متصل کنید
در نهایت، event handler هایی را ایجاد کنید که state را بروزرسانی کند. در زیر فرم نهایی، با تمامی event handler های متصل موجود است:
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // فرض کنید که درحال ارتباط با شبکه است. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
با وجود اینکه این کد طولانی تر از مثال دستوری اصلی است، اما به مراتب حساسیت کمتری دارد. تشریح همه تعاملها به عنوان تغییرات state بعدا به شما اجازه معرفی state های جدید بصری را بدون تاثیر روی موارد موجود می دهد. همچنین به شما اجازه می دهد هرآنچه که باید در هر state نمایش داده شود را بدون تغییر منطق تعامل تغییر دهید.
Recap
- برنامه نویسی اعلانی به معنای توصیف رابط کاربری برای هر state بصری به جای مدیریت جزئیات رابط کاربری است (دستوری).
- هنگام توسعه یک کامپوننت:
- تمامی state های بصری آن را شناسایی کنید.
- تغییرات state توسط عاملهای انسانی و کامپیوتری را تعیین کنید.
- state را با استفاده از
useState
مدل کنید. - state های غیرضروری را به منظور اجتناب از باگها و تناقضات حذف کنید.
- event handler ها را برای تنظیم state متصل کنید.
Challenge 1 of 3: حذف و اضافه کردن یک کلاس CSS
طوری برنامه ریزی کنید که با کلیک روی تصویر کلاس CSS background--active
از <div>
بیرونی حذف شود، اما کلاس picture--active
به <img>
اضافه شود.کلیک مجدد برروی پس زمینه باید کلاسهای اصلی CSS را بازیابی کند.
از نظر بصری، انتظار می رود که با کلیک روی تصویر، پس زمینه بنفش حذف شود و حاشیه تصویر برجسته شود. کلیک خارج از تصویر باعث برجسته سازی پس زمینه می شود، اما برجستگی حاشیه تصویر را حذف می کند.
export default function Picture() { return ( <div className="background background--active"> <img className="picture" alt="Rainbow houses in Kampung Pelangi, Indonesia" src="https://i.imgur.com/5qwVYb1.jpeg" /> </div> ); }