در React وضعیت مثل یک عکس فوری است

متغیرهای وضعیت ممکن است شبیه متغیرهای عادی جاوا اسکریپت باشند که میتوانید آنها را خوانده و تغییر دهید. اما وضعیت بیشتر شبیه یک عکس، عمل میکند. مقدار دهی به آن متغیر، وضعیت قبلی شما را تغییر نمیدهد، بلکه یک رندر مجدد را فعال میکند.
تنظیم وضعیت رندر مجدد را فعال میکند
ممکن است تصور کنید که رابط کاربری شما مستقیماً در پاسخ به رویداد کاربر، مانند یک کلیک، تغییر میکند. در React، این مکانیزم کمی متفاوت عمل میکند. در مطالب قبلی، دیدید که تنظیم وضعیت درخواست رندر مجدد کامپوننت را از React می کند. این به معناست که برای واکنش رابط کاربری به رویدادها نیازمند بروز کردن وضعیت هستید.
در این مثال، وقتی دکمه "ارسال" را فشار میدهید، setIsSent(true)
به React میگوید که رابط کاربری را دوباره رندر کند:
مشاهده اجرای کد در CodeSandbox
import { useState } from 'react';
export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('Hi!');
if (isSent) {
return <h1>Your message is on its way!</h1>
}
return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true);
sendMessage(message);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
function sendMessage(message) {
// ...
}
وقتی دکمه را کلیک میکنید:
- تابع مدیریت رویداد
onSubmit
اجرا میشود. - فراخوانی
setIsSent(true)
متغیر وضعیتisSent
را بهtrue
تنظیم کرده و یک رندر مجدد را در صف قرار میدهد. - React مجدداً کامپوننت را بر اساس مقدار جدید
isSent
رندر میکند.
بیایید نگاهی دقیقتر به رابطه بین وضعیت و رندر بیندازیم.
رندر، اسنپ شاتی در زمان می گیرد
"رندر کردن" به این معنی است که React کامپوننت شما را که یک تابع است فراخوانی میکند. JSX ای که شما از آن تابع باز میگردانید مانند یک اسنپ شات از رابط کاربری در حال است. ویژگیها (props)، توابع مدیریت رویداد و متغیرهای محلی شما همه با استفاده از وضعیت خود در زمان رندر محاسبه شدهاند.
برخلاف یک عکس یا یک فریم فیلم، "اسنپ شات" (عکس فوری) رابط کاربری که کامپوننت بازمیگرداند، تعاملی است. این شامل منطق کامپوننت مانند توابع مدیریت رویداد که مشخص میکنند چه اتفاقی در پاسخ به ورودیها رخ میدهد، می باشد. React صفحه را به گونهای بهروز میکند که با این عکس فوری همخوانی داشته باشد و توابع مدیریت رویداد را هم متصل می کند. به همین دلیل، فشار دادن یک دکمه، رویداد کلیک را فعال میکند.
وقتی React یک کامپوننت را دوباره رندر میکند:
- React دوباره تابع شما را فراخوانی میکند.
- تابع شما یک عکس فوری جدید JSX را باز میگرداند.
- React سپس صفحه را بهروزرسانی میکند تا با اسنپ شاتی که تابع شما بازگردانده همخوانی داشته باشد.
به عنوان حافظه یک کامپوننت، وضعیت مانند یک متغیر عادی نیست که پس از بازگشت تابع شما ناپدید شود. وضعیت واقعاً درون React "زندگی" میکند - گویی که روی یک قفسه است! - خارج از تابع شما. وقتی React کامپوننت شما را فراخوانی میکند، به شما یک عکس فوری از وضعیت برای آن رندر خاص میدهد. کامپوننت شما یک عکس از رابط کاربری با مجموعهای تازه از ویژگیها و توابع مدیریت رویداد در JSX خود، که همه با استفاده از مقادیر وضعیت محاسبه شدهاند را رندر می کند!
در اینجا یک آزمایش کوچک برای نشان دادن چگونگی کار این مکانیزم، ترتیب داده ایم. در این مثال، شما ممکن است انتظار داشته باشید که کلیک بر روی دکمه "+3"، شمارنده را سه واحد افزایش دهد زیرا سه بار دستور setNumber(number + 1)
را فراخوانی میکند.
حال ببینید وقتی دکمه "+3" را کلیک میکنید چه اتفاقی میافتد:
مشاهده و اجرای کد در CodeSandbox
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
میبینید که متغیر number
تنها یک بار در هر کلیک افزایش مییابد!
ست کردن وضعیت فقط آن را برای در رندر اول، number
برابر با 0
بود. به همین دلیل، در رویداد onClick
در آن رندر، مقدار number
هنوز 0
است حتی پس از فراخوانی setNumber(number + 1)
:
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
این چیزی است که رویداد کلیک این دکمه به React میگوید:
setNumber(number + 1)
:number
برابر با0
است بنابراینsetNumber(0 + 1)
.- React آماده میشود تا
number
را در رندر بعدی به1
تغییر دهد.
- React آماده میشود تا
setNumber(number + 1)
:number
برابر با0
است بنابراینsetNumber(0 + 1)
.- React آماده میشود تا
number
را در رندر بعدی به1
تغییر دهد.
- React آماده میشود تا
setNumber(number + 1)
:number
برابر با0
است بنابراینsetNumber(0 + 1)
.- React آماده میشود تا
number
را در رندر بعدی به1
تغییر دهد.
- React آماده میشود تا
حتی اگر شما سه بار setNumber(number + 1)
را فراخوانی کردهاید، در تابع مدیریت رویداد این رندر متغیر number
همیشه 0
است، بنابراین شما حالت را سه بار به 1
تنظیم کردید. به همین دلیل، بعد از پایان رویداد، React کامپوننت را با مقدار number
برابر با 1
دوباره رندر میکند و نه 3
.
شما همچنین میتوانید این موضوع را با جایگزینی ذهنی متغیرهای حالت با مقادیر آنها در کد خود تصور کنید. از آنجا که متغیر حالت number
برای این رندر برابر با 0
است، رویداد آن به این شکل به نظر میرسد:
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
برای رندر بعدی، number
برابر با 1
است، بنابراین رویداد کلیک آن رندر به این شکل به نظر میرسد:
<button onClick={() => {
setNumber(1 + 1);
setNumber(1 + 1);
setNumber(1 + 1);
}}>+3</button>
به همین دلیل، کلیک بر روی دکمه، دوباره شمارنده را به 2
تنظیم میکند، سپس به 3
به کلیک بعدی و به همین ترتیب.
وضعیت در طول زمان
خب، این جالب بود. سعی کنید حدس بزنید که کلیک بر روی این دکمه چه چیزی را نمایش میدهد:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
alert(number);
}}>+5</button>
</>
)
}
اگر شما از روش جایگزینی مثل قبل استفاده کنید، میتوانید حدس بزنید که هشدار مقدار "0" را نشان میدهد:
setNumber(0 + 5);
alert(0);
اما اگر شما یک تایمر روی هشدار بگذارید، به طوری که فقط بعد از اینکه کامپوننت دوباره رندر شد، فعال شود؟ آیا "0" خواهد بود یا "5"؟ حدس بزنید!
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
متعجب شدی؟ اگر از روش جایگزینی استفاده کنید، میتوانید "عکس فوری" از وضعیت را که به هشدار ارسال میشود ببینید.
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
وضعیت ذخیره شده در React ممکن است تا زمان اجرای هشدار تغییر کرده باشد، اما با استفاده از "اسنپ شاتی" از وضعیت در زمان تعامل کاربر تنظیم شده یود!
مقدار یک متغیر حالت هرگز در یک رندر تغییر نمیکند، حتی اگر کد مدیریت رویداد آن غیرهمزمان باشد. در داخل آن رندر رویداد onClick
، مقدار number
برابر با 0
می ماند حتی بعد از اینکه دستور setNumber(number + 5)
اجرا شود. مقدار آن در زمان "گرفتن اسنپ شات" از رابط کاربری بوسیله فراخوانی کامپوننت شما، "ثابت" بود.
در اینجا مثالی از کاهش آسیبپذیر بودن رویدادها در برابر اشتباهات زمان بندی وجود دارد. در زیر فرمی قرار دارد که پیامی را با تأخیری پنج ثانیهای نمایش می دهد. این سناریو را بدین صورت تصور کنید:
- دکمه "ارسال" را فشار میدهید و "سلام" را برای آلیس ارسال میکنید.
- قبل از پایان تأخیر پنج ثانیهای، شما مقدار فیلد "به" را به "باب" تغییر میدهید.
شما انتظار دارید که alert
چه چیزی را نمایش دهد؟ آیا پیام "شما به آلیس سلام گفتید" را نمایش میدهد؟ یا "شما به باب سلام گفتید" را نمایش میدهد؟ بر اساس آنچه میدانید حدس بزنید و سپس امتحان کنید:
مشاهده نتیجه و اجرای کد در CodeSandbox
import { useState } from 'react';
export default function Form() {
const [to, setTo] = useState('Alice');
const [message, setMessage] = useState('Hello');
function handleSubmit(e) {
e.preventDefault();
setTimeout(() => {
alert(`You said ${message} to ${to}`);
}, 5000);
}
return (
<form onSubmit={handleSubmit}>
<label>
To:{' '}
<select
value={to}
onChange={e => setTo(e.target.value)}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
</select>
</label>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
React مقادیر وضعیت را در رویدادهای یک رندر "ثابت" نگه میدارد. شما نیازی به نگرانی درباره تغییر وضعیت در حین اجرای کد ندارید.
اما اگر بخواهید قبل از یک رندر مجدد، آخرین وضعیت را بخوانید چه؟ شما باید از تابع بهروزکننده وضعیت استفاده کنید که در مطلب بعد توضیح داده شده است!
جمع بندی
- تنظیم وضعیت، درخواست رندر مجدد جدید ایجاد میکند.
- React وضعیت را خارج از کامپوننت شما ذخیره میکند، انگار که روی یک قفسه است.
- هنگام فراخوانی
useState
، ری اکت به شما یک عکس فوری از وضعیت برای آن رندر میدهد. - متغیرها و توابع مدیریت رویدادها در رندرها حفظ نمیشوند. هر رندر دارای توابع مدیریت رویداد خاص خود است.
- هر رندر (و توابع داخل آن) همیشه "عکس فوری" وضعیت را که React به آن رندر داده است، خواهد دید.
- شما میتوانید به صورت ذهنی وضعیت را در توابع مدیریت رویداد جایگزین کنید، مشابه نحوه فرض JSX رندر شده.
- توابع مدیریت رویدادی که در گذشته ایجاد شدهاند، مقادیر وضعیت ناشی از رندری که در آن ایجاد شدهاند را دارند.
چالش ها
1. پیادهسازی چراغ راهنمایی
در اینجا یک کامپوننت چراغ راهنمایی وجود دارد که وقتی دکمه فشرده میشود تغییر میکند:
مشاهده و تغییر کد در CodeSandbox
import { useState } from 'react';
export default function TrafficLight() {
const [walk, setWalk] = useState(true);
function handleClick() {
setWalk(!walk);
}
return (
<>
<button onClick={handleClick}>
Change to {walk ? 'Stop' : 'Walk'}
</button>
<h1 style={{
color: walk ? 'darkgreen' : 'darkred'
}}>
{walk ? 'Walk' : 'Stop'}
</h1>
</>
);
}
یک alert
به مدیریت رویداد کلیک اضافه کنید. وقتی چراغ سبز است و میگوید "Walk"، کلیک کردن روی دکمه باید پیام "Stop is next" را نمایش دهد. وقتی چراغ قرمز است و میگوید "Stop"، کلیک کردن روی دکمه باید بگوید "Walk is next".
آیا این مهم است که alert
را قبل یا بعد از فراخوانی setWalk
قرار دهید؟
جواب چالش ها
چالش اول
alert
شما باید به شکل زیر باشد:
import { useState } from 'react';
export default function TrafficLight() {
const [walk, setWalk] = useState(true);
function handleClick() {
setWalk(!walk);
alert(walk ? 'Stop is next' : 'Walk is next');
}
return (
<>
<button onClick={handleClick}>
Change to {walk ? 'Stop' : 'Walk'}
</button>
<h1 style={{
color: walk ? 'darkgreen' : 'darkred'
}}>
{walk ? 'Walk' : 'Stop'}
</h1>
</>
);
}
اینکه آیا آن را قبل یا بعد از فراخوانی setWalk
قرار دادهاید هیچ تفاوتی ایجاد نمیکند. مقدار walk
مربوط به آن رندر ثابت است. فراخوانی setWalk
فقط آن را برای رندر بعدی تغییر میدهد، اما بر رویداد رندر قبلی تأثیری نخواهد داشت.
خط زیر ممکن است در ابتدا غیرمعمول به نظر برسد:
alert(walk ? 'Stop is next' : 'Walk is next');
اما اگر این جمله را بخوانید: "اگر چراغ راهنمایی 'Walk' را نشان دهد، پیام باید بگوید 'Stop is next'" متغیر walk
در داخل رویداد شما با مقدار walk
آن رندر مطابقت دارد و تغییر نمیکند.
شما میتوانید با استفاده از روش جایگزینی صحت این موضوع را تأیید کنید. وقتی walk
برابر با true
است، دارید:
<button onClick={() => {
setWalk(false);
alert('Stop is next');
}}>
Change to Stop
</button>
<h1 style={{color: 'darkgreen'}}>
Walk
</h1>
بنابراین کلیک بر روی "تغییر به ایست" یک رندر را با walk
برابر با false
در صف قرار میدهد و "ایستادن بعدی است" را هشدار میدهد.