واکنش به ورودی کاربر بر اساس وضعیت در React
- مقایسه رابط کاربری اعلانی با دستوری
- تفکر در مورد رابط کاربری به صورت بیانی
- جمع بندی
- چالش ها
- پاسخ به چالش ها
ریاکت راهی اعلانی (declarative) برای تغییر رابط کاربری (UI) ارائه می دهد. به جای این که به صورت مستقیم قسمتهای مختلف رابط کاربری را تغییر دهید، وضعیتهای مختلفی که کامپوننت میتواند در آنها باشد را توصیف میکنید و در پاسخ به ورودیهای کاربر بین آنها جابهجا میشوید. این شبیه به نحوهی تفکر طراحان در مورد رابط کاربری است.
مقایسه رابط کاربری اعلانی با دستوری
وقتی یک رابط کاربری تعاملی طراحی می کنید، احتمالاً به این فکر میکنید که رابط کاربری چگونه در پاسخ به اقدامات کاربر تغییر میکند. فرض کنید فرمی دارید به کاربر اجازه میدهد پاسخی را ارسال کند:
- وقتی چیزی در فرم تایپ میکنید، دکمه "ارسال" فعال میشود.
- وقتی "ارسال" را فشار میدهید، هم فرم و هم دکمه غیرفعال میشوند و لودینگ ظاهر میشود.
- اگر درخواست موفقیتآمیز باشد، فرم پنهان میشود و پیام "متشکرم" ظاهر میشود.
- اگر درخواست ناموفق باشد، یک پیام خطا ظاهر میشود و فرم دوباره فعال میشود.
در برنامهنویسی دستوری، موارد بالا به طور مستقیم به نحوهی پیادهسازی تعامل مربوط میشود. شما مجبورید دقیقاً دستوراتی بر اساس آنچه که به تازگی اتفاق افتاده را برای تغییر رابط کاربری بنویسید. این را میتوانید به این شکل تصور کنید: تصور کنید که در کنار کسی در یک ماشین نشستهاید و به او مرحله به مرحله میگویید کجا برود.

آنها نمیدانند کجا میخواهید بروید، فقط به دستورات شما عمل میکنند. (و اگر راه را اشتباه بگویید، در جای اشتباهی خواهید بود!) به این دلیل است که به آن دستوری میگویند، زیرا شما باید به هر عنصر، از لودینگ تا دکمه، "دستوری" بدهید که به کامپیوتر بگویید چگونه باید رابط کاربری را بهروز کند.
در این مثال از برنامهنویسی رابط کاربری دستوری، فرم بدون ریاکت ساخته شدهاست و فقط از DOM مرورگر استفاده میکند:
مشاهده کد و نتیجه آن در CodeSandbox
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) {
// Pretend it's hitting the network.
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;
تغییر رابط کاربری بهصورت دستوری برای مثالهای ایزوله به خوبی کار میکند، اما در سیستمهای پیچیدهتر به مراتب سختتر میشود. تصور کنید که یک صفحه پر از فرمهای مختلف مثل این یکی را بهروزرسانی میکنید. اضافه کردن یک عنصر رابط کاربری جدید یا تعامل جدید نیاز به بررسی دقیق تمامی کدهای موجود دارد تا مطمئن شوید که باگی ایجاد نمی شود (به عنوان مثال، فراموش نکرده باشید که چیزی را نشان داده یا پنهان کنید).
ریاکت برای حل این مشکل طراحی شده است.
در ریاکت، شما به طور مستقیم رابط کاربری را تغییر نمیدهید--به این معنی که شما اجزا را به طور مستقیم فعال، غیرفعال، آشکار، یا مخفی نمیکنید. به جای آن، توصیف میکنید چه چیزی میخواهید نشان دهید، و ریاکت تشخیص میدهد که چگونه باید رابط کاربری را بهروز کند. تصور کنید که سوار تاکسی میشوید و به راننده میگویید کجا میخواهید بروید، به جای اینکه به او بگویید دقیقاً کجا باید بپیچد. وظیفه راننده است که شما را به آنجا برساند و او ممکن است حتی از میانبرهایی که میشناسد و شما حتی به آنها فکر نمی کنید هم استفاده کند!

تفکر در مورد رابط کاربری به صورت بیانی
شما در بالا دیدید که چگونه می توان یک فرم را به صورت دستوری پیادهسازی کرد. برای درک بهتر نحوه تفکر در ریاکت، مراحل زیر را برای دوباره پیادهسازی کردن این فرم در ریاکت دنبال می کنید:
- شناسایی وضعیتهای بصری مختلف کامپوننتها
- تعیین آنچه که تغییرات وضعیت را فعال میکند
- تعریف وضعیت در حافظه با استفاده از
useState - حذف هر وضعیت غیرضروری
- اتصال هندلرهای رویداد برای تغییر وضعیت
مرحله 1: شناسایی وضعیتهای بصری مختلف کامپوننت خود
در علوم کامپیوتر، ممکن است در مورد "ماشین وضعیت" شنیده باشید که در هر لحظه در یکی از چند "وضعیت" موجود قرار دارد. اگر شما با یک طراح کار میکنید، ممکن است پیشنمایشهایی برای وضعیتهای بصری مختلف دیده باشید. ریاکت در تقاطع طراحی و علوم کامپیوتر قرار گرفته، بنابراین هر دو این ایدهها منبع الهام او هستند.
ابتدا، شما باید تمام "وضعیتها"ی مختلف رابط کاربری که کاربر ممکن است ببیند را تجسم کنید:
- خالی: فرم دارای دکمه "ارسال" غیرفعال است.
- تایپ کردن: فرم دارای دکمه "ارسال" فعال است.
- ارسال: فرم کاملاً غیرفعال است. لودینگ نشان داده میشود.
- موفقیت: پیام "متشکرم" به جای فرم نشان داده میشود.
- خطا: همانند وضعیت تایپ کردن، اما با یک پیام خطای اضافی.
مثل یک طراح، باید وضعیتهای مختلف را قبل از اینکه منطق را به کد اضافه کنید، "مدلسازی" یا "پیشنمایش" کنید. به عنوان مثال، در کد زیر یک پیشنمایش برای نمایش جنبه بصری فرم وجود دارد. این پیشنمایش با ورودی ای به نام 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>
</>
)
}
نامگذاری ورودی ها مهم نیست و می توانید نام دلخواهتان را انتخاب کنید. سعی کنید status = 'empty' را به status = 'success' تغییر دهید تا پیام موفقیت ظاهر شود. درست کردن پیش نمایش اجازه میدهد قبل از اینکه منطقی برای کامپوننت ایجاد کنید، با سرعت بیشتری روی رابط کاربری کار کنید. در اینجا یک نمونه اولیه پیشرفته تر از همان کامپوننت را مشاهده می کنید که هنوز با ورودی status "کنترل" میشود:
export default function Form({
// Try '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>
</>
);
}
نمایش چندین وضعیت بصری به طور همزمان
اگر یک کامپوننت چندین وضعیت بصری داشته باشد، بهتر است که همه آنها را در یک صفحه نشان دهید:
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) نامیده میشوند.
مرحله 2: تعیین آنچه که تغییرات وضعیت را تحریک میکند
شما میتوانید به دو نوع ورودی برای تحریک بهروزرسانی وضعیت رسیدگی کنید:
- ورودیهای انسانی، مانند کلیک بر روی یک دکمه، تایپ در یک فیلد، ناوبری در یک لینک.
- ورودیهای کامپیوتری، مانند رسیدن پاسخ درخواست شبکه، اتمام زمان انتظار، بارگیری یک تصویر.
در هر دو مورد، شما باید برای فرمی که شما در حال توسعه هستید، باید وضعیتها را در پاسخ به چند ورودی مختلف تغییر دهید:
- تغییر متن ورودی (انسان) باید آن را از وضعیت خالی به وضعیت تایپ کردن یا برعکس تغییر دهد، بسته به اینکه آیا کادر متن خالی است یا خیر.
- کلیک بر روی دکمه ارسال (انسان) باید آن را به وضعیت در حال ارسال تغییر دهد.
- پاسخ موفق شبکه (کامپیوتر) باید آن را به وضعیت موفقیت تغییر دهد.
- پاسخ ناموفق شبکه (کامپیوتر) باید آن را به وضعیت خطا با پیام خطای مطابقت تبدیل کند.
توجه کنید که ورودیهای انسانی معمولاً نیاز به هندلرهای رویداد دارند!
برای درک این جریان، سعی کنید هر وضعیت را روی کاغذ به صورت یک دایره طراحی کنید و هر تغییر بین دو وضعیت را به عنوان یک پیکان بررسی کنید. شما میتوانید به این طریق بسیاری از جریانها را ترسیم کرده و نقصها را قبل از پیادهسازی برطرف کنید.

مرحله 3: نمایش وضعیت در حافظه با useState
سپس باید وضعیتهای بصری کامپوننت خود را در حافظه با استفاده از useState مدلسازی کنید. سادگی کلید انجام این کار است: هر بخش وضعیت یک "جزء در حال حرکت" است، و شما باید حداقل "جزء در حال حرکت" ممکن را داشته باشید. پیچیدگی بیشتر به باگهای بیشتری منجر میشود!
با حالتی که به طور قطع باید وجود داشته باشد شروع کنید. برای مثال، شما باید پاسخ (answer) ورودی را ذخیره کنید، و برای ذخیره آخرین خطای رخ داده (اگر وجود داشته باشد) نیز باید خطا (error) ذخیره کنید:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
سپس، به یک متغیر وضعیت نیاز دارید که نشان دهد کدام یک از وضعیتهای بصری را میخواهید نمایش دهید. معمولاً بیش از یک روش برای مدلسازی آن در حافظه وجود دارد، بنابراین باید در مورد آن آزمایش کنید.
اگر در مورد پیدا کردن بهترین راه با مشکل مواجه هستید، با اضافه کردن تعداد متغیر وضعیت لازم شروع کنید تا قطعاً مطمئن باشید که تمامی وضعیتهای بصری ممکن پوشش داده شدهاند:
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);
ایده اول احتمالاً بهترین نخواهد بود، اما اشکالی ندارد—بازنگری وضعیت جزئی از فرآیند است!
مرحله 4: حذف هر متغیر وضعیت غیرضروری
باید از تکرار در محتوای وضعیت اجتناب کنید تا فقط آنچه که ضروری است را پیگیری کنید. صرف کمی زمان برای بازنگری ساختار وضعیت باعث میشود که کامپوننتهای شما آسانتر درک شوند، تکرار را کاهش دهند و از معانی ناخواسته جلوگیری کنند. هدف این است که از نگهداری وضعیت در حافظه برای مواردی که نماینده هیچ وضعیتی از رابط کاربری نیست، جلوگیری کنید. (به عنوان مثال، هرگز نباید همزمان پیام خطا را نشان داد و ورودی را نیز غیرفعال کنید، در این صورت کاربر نمیتواند خطا را اصلاح کند!)
سوال های زیر را می توانید در مورد متغیر های وضعیت خود بپرسید:
- آیا این وضعیت باعث پارادوکس میشود؟ به عنوان مثال،
isTypingوisSubmittingنمیتوانند هر دوtrueباشند. پارادوکس معمولاً به این معنی است که وضعیت به اندازه کافی محدود نشدهاست. چهار ترکیب ممکن از دو مقدار بولین وجود دارد، اما تنها سه مورد با وضعیتهای معتبر مطابقت دارند. برای حذف "وضعیتهای ناممکن"، میتوانید آنها را در یکstatusترکیب کنید که باید یکی از سه مقدار:'typing'،'submitting'، یا'success'باشد. - آیا اطلاعات مشابه در حال حاضر در یک متغیر وضعیت دیگر موجود است؟ پارادوکسی دیگر:
isEmptyوisTypingنمیتوانند بهطور همزمانtrueباشند. با جدا کردن آنها به عنوان متغیرهای وضعیت مجزا، خطر ناهمگونی و ایجاد باگ وجود دارد. خوشبختانه، شما میتوانیدisEmptyرا حذف کنید و به جای آنanswer.length === 0را بررسی کنید. - آیا میتوانید همان اطلاعات را از معکوس یک متغیر وضعیت دیگر دریافت کنید؟ متغیر
isErrorلازم نیست زیرا میتوانید به جای آنerror !== nullرا بررسی کنید.
بعد از این تمیز کاری، شما با 3 متغیر وضعیت ضروری باقی میمانید (از 7 به 3 کاهش یافته است!):
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
شما میدانید که آنها ضروری هستند، زیرا نمیتوانید هیچکدام از آنها را بدون شکستن عملکرد حذف کنید.
حذف "وضعیتهای ناممکن" با یک کاهشدهنده
این سه متغیر نمایشی کافی از وضعیت این فرم هستند. با این حال، هنوز هم برخی وضعیتهای میانی وجود دارند که بی معنی هستند. به عنوان مثال، error غیر null زمانی که status برابر با 'success' است، معنی ندارد. برای مدلسازی وضعیت بهطور دقیقتر، میتوانید آن را به یک کاهشدهنده منتقل کنید. کاهشدهندهها به شما این امکان را میدهند که چندین متغیر وضعیت را در یک شیء واحد یکپارچه کنید و تمامی منطقهای مرتبط را در هم ادغام کنید!
مرحله 5: هندلرهای رویداد را برای تنظیم وضعیت متصل کنید
در نهایت، هندلرهای رویداد ایجاد کنید که وضعیت را بهروزرسانی کنند. در زیر فرم نهایی آمدهاست و همه هندلرهای رویداد متصل شده اند:
مشاهده اجرای کد در CodeSandbox
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) {
// Pretend it's hitting the network.
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);
});
}
اگرچه این کد طولانیتر از مثال دستوری اولیه است، اما شکنندگی کمتری دارد. بیان تمام تعاملات به عنوان تغییرات وضعیت به شما اجازه میدهد که بعداً وضعیتهای بصری جدیدی را بدون شکستن وضعیتهای موجود معرفی کنید. همچنین به شما این امکان را میدهد که آنچه را که باید در هر وضعیت نمایش داده شود تغییر دهید بدون این که منطق تعامل خود را تغییر دهید.
جمع بندی
- برنامهنویسی بیانی ( Declarative ) به معنای توصیف رابط کاربری برای هر وضعیت بصری است نه مدیریت آن به صورت میکرو (دستوری).
- هنگام توسعه یک کامپوننت:
- تمام وضعیتهای بصری آن را شناسایی کنید.
- محرکهای انسانی و کامپیوتری تغییرات وضعیت را تعیین کنید.
- وضعیت را با
useStateمدلسازی کنید. - وضعیتهای غیرضروری را حذف کنید تا از بروز باگها و پارادوکسها جلوگیری شود.
- هندلرهای رویداد را برای تنظیم وضعیت متصل کنید.
چالش ها
1. اضافه و حذف یک کلاس CSS
اینطور طراحی کنید که کلیک بر روی تصویر کلاس background--active را از <div> بیرونی حذف کند، اما کلاس picture--active را به <img> اضافه کند. کلیک دوباره بر روی پسزمینه باید کلاسهای CSS اصلی را بازگرداند.
از نظر بصری، شما باید انتظار داشته باشید که کلیک بر روی تصویر پسزمینه بنفش را حذف کند و حاشیه تصویر را هایلایت کند. کلیک بیرون از تصویر پسزمینه را هایلایت کرده، اما هایلایت حاشیه تصویر را حذف میکند.
مشاهده و اصلاح کد در CodeSandbox
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>
);
}
2. ویرایشگر پروفایل
در اینجا یک فرم کوچک با جاوااسکریپت ساده و DOM پیادهسازی شده است. با آن بازی کنید تا رفتار آن را درک کنید:
function handleFormSubmit(e) {
e.preventDefault();
if (editButton.textContent === 'Edit Profile') {
editButton.textContent = 'Save Profile';
hide(firstNameText);
hide(lastNameText);
show(firstNameInput);
show(lastNameInput);
} else {
editButton.textContent = 'Edit Profile';
hide(firstNameInput);
hide(lastNameInput);
show(firstNameText);
show(lastNameText);
}
}
function handleFirstNameChange() {
firstNameText.textContent = firstNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function handleLastNameChange() {
lastNameText.textContent = lastNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
این فرم بین دو حالت جابجا میشود: در حالت ویرایش، ورودیها را میبینید و در حالت مشاهده، فقط نتیجه را میبینید. برچسب دکمه بسته به حالتی که در آن هستید بین "ویرایش" و "ذخیره" تغییر میکند. وقتی ورودیها را تغییر میدهید، پیام خوشامدگویی در پایین در زمان واقعی بهروزرسانی میشود.
وظیفه شما این است که آن را در ریاکت دوباره پیادهسازی کنید. برای راحتی شما، کدهای قبلاً به JSX تبدیل شده است، اما شما باید آن را به گونهای تنظیم کنید که ورودیها مانند نسخه اصلی نشان داده و مخفی شوند.
مطمئن شوید که متن در پایین دکمه نیز بهروزرسانی میشود! مشاهده کد در CodeSandbox
export default function EditProfile() {
return (
<form>
<label>
First name:{' '}
<b>Jane</b>
<input />
</label>
<label>
Last name:{' '}
<b>Jacobs</b>
<input />
</label>
<button type="submit">
Edit Profile
</button>
<p><i>Hello, Jane Jacobs!</i></p>
</form>
);
}
3. بازنگری راهحل دستوری بدون ریاکت
در اینجا کد اصلی از چالش قبلی وجود دارد که به شکل دستوری و بدون ریاکت نوشته شده است: مشاهد کد در CodeSandbox
function handleFormSubmit(e) {
e.preventDefault();
if (editButton.textContent === 'Edit Profile') {
editButton.textContent = 'Save Profile';
hide(firstNameText);
hide(lastNameText);
show(firstNameInput);
show(lastNameInput);
} else {
editButton.textContent = 'Edit Profile';
hide(firstNameInput);
hide(lastNameInput);
show(firstNameText);
show(lastNameText);
}
}
function handleFirstNameChange() {
firstNameText.textContent = firstNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function handleLastNameChange() {
lastNameText.textContent = lastNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
تصور کنید که ریاکت وجود ندارد. آیا میتوانید این کد را به گونهای بازنگری کنید که منطق آن کمتر شکننده و بیشتر مشابه نسخه ریاکت باشد؟ اگر وضعیت بهطور صریح مانند ریاکت بود، چه شکلی میشد؟
اگر نمیدانید از کجا شروع کنید، کدی که در زیر آمده، بیشتر ساختار را در اختیار دارد. اگر از اینجا شروع می کنید، منطق گمشده را در تابع updateDOM پر کنید. (به کد اصلی در صورت نیاز مراجعه کنید.)
let firstName = 'Jane';
let lastName = 'Jacobs';
let isEditing = false;
function handleFormSubmit(e) {
e.preventDefault();
setIsEditing(!isEditing);
}
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
function setFirstName(value) {
firstName = value;
updateDOM();
}
function setLastName(value) {
lastName = value;
updateDOM();
}
function setIsEditing(value) {
isEditing = value;
updateDOM();
}
function updateDOM() {
if (isEditing) {
editButton.textContent = 'Save Profile';
// TODO: show inputs, hide content
} else {
editButton.textContent = 'Edit Profile';
// TODO: hide inputs, show content
}
// TODO: update text labels
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
پاسخ به چالش ها
چالش اول
این کامپوننت دو وضعیت بصری دارد: وقتی تصویر فعال است، و وقتی تصویر غیرفعال است:
- وقتی تصویر فعال است، کلاسهای CSS
backgroundوpicture picture--activeهستند. - وقتی تصویر غیرفعال است، کلاسهای CSS
background background--activeوpictureهستند. یک متغیر وضعیت بولی کافی است که به خاطر بسپارید آیا تصویر فعال است یا خیر. وظیفه اصلی این است که کلاسهای CSS را حذف یا اضافه کنید. اما، در ریاکت شما باید توصیف کنید چه چیزی را میخواهید ببینید نه اینکه اجزای UI را مدیریت کنید. بنابراین شما باید هر دو کلاس CSS را بر اساس وضعیت فعلی محاسبه کنید. شما همچنین باید توقف انتشار را انجام دهید تا کلیک بر روی تصویر به عنوان کلیک بر روی پسزمینه ثبت نشود. این نسخه را با کلیک بر روی تصویر و سپس بیرون از آن بررسی کنید: مشاهده کد و نتیجه اجرای آن در CodeSandbox
import { useState } from 'react';
export default function Picture() {
const [isActive, setIsActive] = useState(false);
let backgroundClassName = 'background';
let pictureClassName = 'picture';
if (isActive) {
pictureClassName += ' picture--active';
} else {
backgroundClassName += ' background--active';
}
return (
<div
className={backgroundClassName}
onClick={() => setIsActive(false)}
>
<img
onClick={e => {
e.stopPropagation();
setIsActive(true);
}}
className={pictureClassName}
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="https://i.imgur.com/5qwVYb1.jpeg"
/>
</div>
);
}
یا میتوانید دو تکه JSX جداگانه را برگردانید: مشاهده کد و اجرای آن در CodeSandbox
import { useState } from 'react';
export default function Picture() {
const [isActive, setIsActive] = useState(false);
if (isActive) {
return (
<div
className="background"
onClick={() => setIsActive(false)}
>
<img
className="picture picture--active"
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="https://i.imgur.com/5qwVYb1.jpeg"
onClick={e => e.stopPropagation()}
/>
</div>
);
}
return (
<div className="background background--active">
<img
className="picture"
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="https://i.imgur.com/5qwVYb1.jpeg"
onClick={() => setIsActive(true)}
/>
</div>
);
}
به خاطر داشته باشید که اگر دو تکه JSX مختلف یک درخت را توصیف کنند، باید تودرتویی آنها (اول <div> → اول <img>) یکسان باشد. در غیر این صورت، سوئیچ isActive کل درخت زیرین را دوباره ایجاد کرده و وضعیتش را بازنشانی میکند. به همین دلیل، اگر درخت JSX مشابهی در هر دو حالت برگردانده شود، بهتر است آنها را به عنوان یک تکه واحد از JSX بنویسید.
چالش دوم
شما به دو متغیر وضعیت برای نگهداری مقادیر ورودی نیاز خواهید داشت: firstName و lastName. همچنین به یک متغیر وضعیت isEditing نیاز دارید که نشان میدهد ورودیها را نمایش دهید یا خیر. شما نباید به متغیر fullName نیاز داشته باشید زیرا نام کامل همواره میتواند از firstName و lastName محاسبه شود.
در نهایت، باید از رندرینگ شرطی برای نمایش یا مخفی کردن ورودیها بسته به isEditing استفاده کنید.
import { useState } from 'react';
export default function EditProfile() {
const [isEditing, setIsEditing] = useState(false);
const [firstName, setFirstName] = useState('Jane');
const [lastName, setLastName] = useState('Jacobs');
return (
<form onSubmit={e => {
e.preventDefault();
setIsEditing(!isEditing);
}}>
<label>
First name:{' '}
{isEditing ? (
<input
value={firstName}
onChange={e => {
setFirstName(e.target.value)
}}
/>
) : (
<b>{firstName}</b>
)}
</label>
<label>
Last name:{' '}
{isEditing ? (
<input
value={lastName}
onChange={e => {
setLastName(e.target.value)
}}
/>
) : (
<b>{lastName}</b>
)}
</label>
<button type="submit">
{isEditing ? 'Save' : 'Edit'} Profile
</button>
<p><i>Hello, {firstName} {lastName}!</i></p>
</form>
);
}
این راهحل را با کد دستوری اصلی مقایسه کنید. آنها چه تفاوتی دارند؟
چالش سوم
منطق گمشده شامل جابهجایی نمایش ورودیها و محتوا، و بهروز کردن برچسبها بود: مشاهده نتیجه در CodeSandbox
let firstName = 'Jane';
let lastName = 'Jacobs';
let isEditing = false;
function handleFormSubmit(e) {
e.preventDefault();
setIsEditing(!isEditing);
}
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
function setFirstName(value) {
firstName = value;
updateDOM();
}
function setLastName(value) {
lastName = value;
updateDOM();
}
function setIsEditing(value) {
isEditing = value;
updateDOM();
}
function updateDOM() {
if (isEditing) {
editButton.textContent = 'Save Profile';
hide(firstNameText);
hide(lastNameText);
show(firstNameInput);
show(lastNameInput);
} else {
editButton.textContent = 'Edit Profile';
hide(firstNameInput);
hide(lastNameInput);
show(firstNameText);
show(lastNameText);
}
firstNameText.textContent = firstName;
lastNameText.textContent = lastName;
helloText.textContent = (
'Hello ' +
firstName + ' ' +
lastName + '!'
);
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
تابع updateDOM که شما نوشتید نشان میدهد که وقتی که شما وضعیت را تنظیم میکنید ریاکت چه کاری در پسزمینه انجام میدهد. (با این حال، ریاکت همچنین از تغییر DOM برای خصوصیات که از آخرین باری که تنظیم شدهاند تغییر نکردهاند، اجتناب میکند.)