بروزرسانی اشیا وضعیت در React

وضعیت میتواند هر نوع مقدار جاوا اسکریپت را نگه دارد، از جمله اشیا. اما نباید به صورت مستقیم اشیایی را که با وضعیت تعریف کرده اید تغییر دهید. در عوض، زمانی که میخواهید یک شی را بروزرسانی کنید، باید شی جدیدی ایجاد کنید (یا یک کپی از یک شی موجود بسازید) و سپس حالت را برای استفاده از آن تنظیم کنید.
تغییر (mutation) چیست؟
شما میتوانید هر نوع مقدار جاوا اسکریپت را در متغیر وضعیت ذخیره کنید.
const [x, setX] = useState(0);
تا به حال شما با اعداد، رشتهها و بولین ها کار کردهاید. این نوع مقادیر جاوا اسکریپت "غیرقابل تغییر" هستند، به این معنی که تغییر نمیکنند یا "فقط خواندنی" هستند. شما میتوانید رندر مجدد را برای جایگزین کردن یک مقدار فعال کنید:
setX(5);
حالت x
از 0
به 5
تغییر کرده است، اما خود عدد تغییر نکرده است. امکان تغییر هیچ یک از مقادیر اولیه ساخته شده مانند اعداد، رشتهها و بولینها در جاوا اسکریپت وجود ندارد.
حال یک شی وضعیت را در نظر بگیرید:
const [position, setPosition] = useState({ x: 0, y: 0 });
از نظر فنی، تغییر محتوای خود شی ممکن است. که این تغییر نامیده میشود. مانند مثال زیر:
position.x = 5;
با این حال، اگرچه اشیای وضعیت در React از نظر فنی قابل تغییر هستند، اما باید آنها را مانند اشیا غیرقابل تغییر (مثل اعداد، بولیها و رشتهها) در نظر بگیرید. به جای تغییر آنها، باید همیشه آنها را جایگزین کنید.
وضعیت را فقط خواندنی در نظر بگیرید
به عبارت دیگر، باید هر شی جاوا اسکریپتی که در متغیر وضعیت قرار میدهید را به عنوان فقط خواندنی در نظر بگیرید.
این مثال شیای را در متغیر وضعیت نگه میدارد که موقعیت فعلی نشانگر را نشان می دهد. نقطه قرمز باید زمانی که شما کلیک میکنید یا نشانگر را روی صفحه حرکت میدهید، حرکت کند. اما نقطه در موقعیت اولیه باقی میماند:
مشاهده کد و نتیجه اجرای آن در CodeSandbox
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
);
}
مشکل در این قطعه کد است.
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
این کد شی تخصیص یافته به position
را از رندر قبلی تغییر میدهد. اما بدون استفاده از تابع تنظیم حالت، React متوجه تغییر شی نمی شود. بنابراین React هیچ اقدامی برای رندر مجدد انجام نمیدهد. این مانند این است که بخواهید بعد از صرف غذا، سفارش خود را تغییر دهید. در حالی که تغییر حالت در برخی از موارد ممکن است، اما آن را توصیه نمیکنیم. شما باید مقدار حالت را که در رندر به آن دسترسی دارید، به عنوان فقط خواندنی در نظر بگیرید.
برای [فعال کردن رندر مجدد](صف کردن چندین بروزرسانی وضعیت در React | مستندات React نسخه 19) باید یک
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
با setPosition
، به React میگویید:
position
را با این شی جدید جایگزین کنید- و کامپوننت را دوباره رندر کنید
توجه کنید که اکنون نقطه قرمز چگونه با حرکت موس یا کلیک و لمس صفحه جابجا می شود:
مشاهده اجرای کد در CodeSandbox
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
);
}
تغییر محلی خوب است
کدی مانند کد زیر که شی وضعیت موجود را تغییر میدهد، مشکل ساز است:
position.x = e.clientX;
position.y = e.clientY;
اما کدی مانند کد زیر خوب است زیرا شما در حال تغییر شی تازهای که ایجاد کردهاید هستید:
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);
در واقع، کد بالا معادل نوشتن کد زیر است:
setPosition({
x: e.clientX,
y: e.clientY
});
تغییر فقط زمانی مشکلساز است که شما اشیا موجود را که قبلاً در وضعیت هستند، تغییر دهید. تغییر شیای که تازه ایجاد کردهاید، اشکالی ندارد زیرا هیچ کد دیگری به آن ارجاع داده نشده است. و تغییر آن به طور تصادفی بر کدی که به آن وابسته است تأثیر نمیگذارد. این روش به "تغییر محلی" مشهور است. شما حتی میتوانید در حین رندر، تغییر محلی انجام دهید.
کپی کردن اشیا با استفاده از سینتکس گسترش
در مثال قبل، شی position
همیشه از روی موقعیت نشانگر موس ایجاد میشود. اما گاهی ممکن است بخواهید دادههای موجود را به عنوان بخشی از شی جدید، استفاده کنید. به عنوان مثال، ممکن است بخواهید فقط یک فیلد را در یک فرم بروزرسانی کنید، اما مقادیر قبلی را برای تمام فیلدهای دیگر حفظ کنید.
برای مثال فیلدهای ورودی فرم زیر کار نمیکنند زیرا رویدادهای onChange
وضعیت را تغییر میدهند:
مشاهده اجرای کد در CodeSandbox
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]'
});
function handleFirstNameChange(e) {
person.firstName = e.target.value;
}
function handleLastNameChange(e) {
person.lastName = e.target.value;
}
function handleEmailChange(e) {
person.email = e.target.value;
}
return (
<>
<label>
First name:
<input
value={person.firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
value={person.lastName}
onChange={handleLastNameChange}
/>
</label>
<label>
Email:
<input
value={person.email}
onChange={handleEmailChange}
/>
</label>
<p>
{person.firstName}{' '}
{person.lastName}{' '}
({person.email})
</p>
</>
);
}
به عنوان مثال، کد زیر وضعیت را از رندر قبلی تغییر میدهد:
person.firstName = e.target.value;
راه مطمئن برای دستیابی به عملکردی که شما به دنبال آن هستید، ایجاد یک شی جدید و پاس دادن آن به setPerson
است. اما در اینجا، میخواهید دادههای موجود را نیز به شی جدید انتقال دهید زیرا تنها یکی از فیلدها تغییر کرده است:
setPerson({
firstName: e.target.value, // نام جدید از فیلد ورودی
lastName: person.lastName,
email: person.email
});
برای این کار میتوانید از علامت ...
که سینتکس گسترش شی است استفاده کنید تا نیازی به کپی کردن همه ویژگی ها به صورت جداگانه نداشته باشید.
setPerson({
...person, // کپی کردن ویژگی های قدیمی
firstName: e.target.value // اما نام روی قبلی های نوشته می شود
});
اکنون فرم کار میکند!
توجه کنید که برای هر فیلد ورودی یک متغیر حالت جداگانه تعریف نکرده اید. برای فرمهای بزرگ، نگهداشتن تمام دادهها در یک شی بسیار راحت است - به شرطی که آن را به درستی بروزرسانی کنید!
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]'
});
function handleFirstNameChange(e) {
setPerson({
...person,
firstName: e.target.value
});
}
function handleLastNameChange(e) {
setPerson({
...person,
lastName: e.target.value
});
}
function handleEmailChange(e) {
setPerson({
...person,
email: e.target.value
});
}
return (
<>
<label>
First name:
<input
value={person.firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
value={person.lastName}
onChange={handleLastNameChange}
/>
</label>
<label>
Email:
<input
value={person.email}
onChange={handleEmailChange}
/>
</label>
<p>
{person.firstName}{' '}
{person.lastName}{' '}
({person.email})
</p>
</>
);
}
توجه داشته باشید که سینتکس گسترش ...
"سطحی" است - این فقط اشیا را تا سطح اول کپی میکند. با وجود اینکه این کار را سریع انجام می شود، اما به این معنی است که اگر بخواهید یک خاصیت تو در تو را بروزرسانی کنید، باید بیش از یک بار از آن استفاده کنید.
استفاده از یک تابع مدیریت رویداد برای چندین فیلد
از براکتهای [
و ]
نیز می توان داخل تعریف شی و برای مشخص کردن خاصیتی با نام پویا استفاده کنید. این همان مثال قبل است، اما فقط با یک تابع مدیریت رویداد به جای سه تابع متفاوت:
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]'
});
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value
});
}
return (
<>
<label>
First name:
<input
name="firstName"
value={person.firstName}
onChange={handleChange}
/>
</label>
<label>
Last name:
<input
name="lastName"
value={person.lastName}
onChange={handleChange}
/>
</label>
<label>
Email:
<input
name="email"
value={person.email}
onChange={handleChange}
/>
</label>
<p>
{person.firstName}{' '}
{person.lastName}{' '}
({person.email})
</p>
</>
);
}
در اینجا، e.target.name
به خاصیت name
داده شده به عنصر <input>
اشاره دارد.
بروزرسانی شی تو در تو
ساختار یک شی تو در تو مانند این را در نظر بگیرید:
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
اگر میخواهید person.artwork.city
را بروزرسانی کنید، واضح است که چگونه میتوانید این کار را با تغییر انجام دهید:
person.artwork.city = 'New Delhi';
اما در React، شما حالت را به عنوان غیرقابل تغییر در نظر میگیرید! برای تغییر city
، باید ابتدا شی جدید artwork
(که با دادههای شی قبلی پیش پر شده است) تولید کنید و سپس شی جدید person
که به artwork
جدید اشاره میکند را تولید کنید:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
یا، به صورت زیر به عنوان یک فراخوانی تابع واحد بنویسید:
setPerson({
...person, // کپی کردن تمامی ویژگی ها
artwork: { // اما این ویژگی را جایگزین کن
...person.artwork, // با دیتای فعلی
city: 'New Delhi' // اما نام شهر را جایگزین کن
}
});
این روش کمی طولانی میشود، اما برای بسیاری از موارد خوب و مؤثر عمل می کند:
مشاهده نتیجه کد در CodeSandbox
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value
});
}
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
function handleCityChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
city: e.target.value
}
});
}
function handleImageChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
image: e.target.value
}
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<label>
Image:
<input
value={person.artwork.image}
onChange={handleImageChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
اشیا واقعاً تو در تو نیستند
شیای مانند شی کد زیر "تو در تو" به نظر میرسد:
let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};
با این حال، "تو در تو" یک راه نادرست برای فکر کردن درباره نحوه رفتار اشیا است. وقتی کد اجرا میشود، چیزی به نام "شی تو در تو" وجود ندارد. شما واقعاً به دو شی مختلف نگاه میکنید:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
شی obj1
"درون" obj2
نیست. به عنوان مثال، obj3
نیز میتواند "به" obj1
اشاره کند:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
let obj3 = {
name: 'Copycat',
artwork: obj1
};
اگر مقدار obj3.artwork.city
را تغییر دهید، بر روی obj2.artwork.city
و obj1.city
تأثیر میگذارد. این به این دلیل است که obj3.artwork
، obj2.artwork
و obj1
همان شی هستند. اما زمانی که اشیا را به عنوان "تو در تو" در نظر میگیرید، دیدن این قضیه کمی دشوار می شود. در عوض، اینها اشیا جداگانهای هستند که با ویژگیها به یکدیگر اشاره میکنند.
نوشتن منطق بروزرسانی مختصر با Immer
اگر وضعیت زیاد تو در تو باشد، ممکن است بخواهید از روش تخت کردن استفاده کنید. اما اگر نمیخواهید ساختار شی وضعیت خود را تغییر دهید، می توانید از یک میانبر بجای سینتکس گسترش تو در تو استفاده کنید. Immer یک کتابخانه محبوب است که به شما اجازه میدهد با استفاده از سینتکس راحت ویژگی ها را تغییر دهید و به صورت اتوماتیک کپی ای از روش شی تولید کنید. با Immer، کدی که مینویسید به گونهای به نظر میرسد که در حال "شکستن قوانین" و تغییر یک شی هستید:
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
اما بر خلاف یک تغییر معمولی، این تغییر وضعیت، مقدار شی قدیمی را دستکاری نمیکند!
Immer چگونه کار میکند؟
پیش نویش ارائه شده توسط Immer، نوع خاصی از شی است که به آن Proxy گفته میشود، که آنچه شما می کنید را "ضبط" میکند. به همین خاطر شما میتوانید به راحتی هر چه میخواهید را تغییر دهید! در پسزمینه، Immer متوجه میشود که کدام بخشهای draft
تغییر کردهاند و یک شی کاملاً جدید تولید میکند که شامل ویرایشهای شما است.
برای امتحان کردن Immer:
- دستور
npm install use-immer
را اجرا کنید تا Immer را به عنوان وابستگی به پروژه خود اضافه کنید - سپس
import { useState } from 'react'
را بهimport { useImmer } from 'use-immer'
تغییر دهید
در اینجا مثال قبل که به Immer تبدیل شده است:
مشاهده نحوه استفاده از Immer در CodeSandbox
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}
function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}
function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
function handleImageChange(e) {
updatePerson(draft => {
draft.artwork.image = e.target.value;
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<label>
Image:
<input
value={person.artwork.image}
onChange={handleImageChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
توجه کنید که چه قدر رویدادها مختصرتر شدهاند. شما میتوانید به مقدار دلخواه از useState
و useImmer
در یک کامپوننت به صورتی ترکیبی استفاده کنید. Immer یک روش عالی برای مختصر نگه داشتن رویدادهای بروزرسانی است، به ویژه اگر شی وضعیت تو در تو باشد و کپی کردن اشیا به کد تکراری منتهی شود.
چرا تغییر حالت در React توصیه نمیشود؟
به دلایل زیر:
- اشکالزدایی: اگر از
console.log
استفاده کنید و وضعیت را تغییر ندهید، لاگهای قبلی شما با تغییرات جدید تحتتأثیر قرار نمیگیرند. بنابراین میتوانید ببینید که وضعیت چطور بین رندرها تغییر کرده است. - بهینهسازیها: استراتژیهای رایج بهینهسازی React به این شکل هستند که در صورت یکسان بودن ورودی ها یا وضعیت قبلی با وضعیت بعدی، از محاسبات مجدد جلوگیری کنند. اگر وضعیت را هرگز مستقیما تغییر ندهید، بررسی اینکه آیا تغییری وجود داشته است بسیار سریع خواهد بود. اگر
prevObj === obj
باشد، میتوانید مطمئن باشید که درون شی چیزی تغییر نکرده است. - ویژگیهای جدید: ویژگیهای جدید React که در حال ساخت آنها هستیم به این بستگی دارند که حالت به عنوان یک عکس فوری در نظر گرفته شود. اگر شما نسخههای قبلی وضعیت را مستقیما تغییر دهید، ممکن است نتوانید از ویژگیهای جدید استفاده کنید.
- تغییرات مورد نیاز: افزودن برخی ویژگیهای اپلیکیشن، مانند پیادهسازی Undo/Redo، نمایش تاریخچه تغییرات یا اجازه دادن به کاربر برای ریست کردن یک فرم به مقادیر قبلی، زمانی که هیچ چیزی تغییر نکرده باشد، سادهتر است. به این دلیل که میتوانید نسخههای قبلی حالت را در حافظه نگه دارید و در مواقع لازم از آنها دوباره استفاده کنید. اگر با رویکرد تغییر مستقیم کار کنید، افزودن چنین ویژگیهایی میتواند دشوار باشد.
- پیادهسازی سادهتر: از آنجا که React به تغییر (mutation) تکیه نمیکند، نیازی به انجام کار خاصی با اشیا ندارد. نیازی به جاسوسی ویژگیها، همیشه درون پروکسیها قرار دادن آنها یا انجام کارهای دیگر هنگام راهاندازی ندارد، همانطور که بسیاری از راهحلهای "واکنش گرا" این کارها را انجام میدهند. به همین دلیل است که React به شما اجازه میدهد هر شیای را در متغیر وضعیت قرار دهید - بدون توجه به اندازه آن - بدون مواجه شدن با مشکلات اضافی کارایی یا صحت.
در عمل، میتوانید "با دور زدن" React وضعیت را تغییر دهید، اما ما به شدت توصیه میکنیم که این کار را نکنید تا بتوانید از ویژگیهای جدید React که با این رویکرد توسعه یافتهاند، استفاده کنید. آیندگان و شاید حتی خودتان در آینده از این کارتان تشکر کنید!
جمع بندی
- تمام وضعیت ها در React را به عنوان غیرقابل تغییر در نظر بگیرید.
- زمانی که اشیا را در وضعیت ذخیره میکنید، تغییر آنها باعث نمیشود رندری صورت گیرد و وضعیت در "عکس فوری" رندرهای قبلی را تغییر میدهد.
- به جای تغییر یک شی، یک نسخه جدید از آن ایجاد کنید و با تنظیم وضعیت به آن، یک رندر مجدد را فعال کنید.
- میتوانید از سینتکس
{...obj, something: 'newValue'}
گسترش شی، برای ایجاد کپی از اشیا استفاده کنید. - سینتکس گسترش سطحی است: فقط یک سطح از شی عمیق را کپی میکند.
- برای بهروزرسانی یک شی تو در تو، نیاز به ایجاد کپی از ژرفای جایی دارید که آن را بروزرسانی میکنید.
- برای کاهش کد تکراری کپی کردن اشیا، از Immer استفاده کنید.
چالش ها
1. اصلاح بروزرسانیهای نادرست حالت
فرم زیر چند باگ دارد. دکمهای را که امتیاز را چند بار زیاد میکند، کلیک کنید. متوجه می شوید که افزایش نمییابد. سپس نام کوچک را ویرایش کنید و متوجه می شوید که امتیاز به طور ناگهانی با تغییرات شما "همگام" شده است. در نهایت، نام خانوادگی را ویرایش کنید و متوجه می شوید که امتیاز کاملاً ناپدید شده است.
وظیفه شما این است که تمام این باگها را برطرف کنید. در حین اصلاح آنها، توضیح دهید که چرا هر یک از آنها اتفاق میافتد.
مشاهده کد و تغییر آن در CodeSandbox
import { useState } from 'react';
export default function Scoreboard() {
const [player, setPlayer] = useState({
firstName: 'Ranjani',
lastName: 'Shettar',
score: 10,
});
function handlePlusClick() {
player.score++;
}
function handleFirstNameChange(e) {
setPlayer({
...player,
firstName: e.target.value,
});
}
function handleLastNameChange(e) {
setPlayer({
lastName: e.target.value
});
}
return (
<>
<label>
Score: <b>{player.score}</b>
{' '}
<button onClick={handlePlusClick}>
+1
</button>
</label>
<label>
First name:
<input
value={player.firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
value={player.lastName}
onChange={handleLastNameChange}
/>
</label>
</>
);
}
2. یافتن و اصلاح تغییرات مستقیم
یک جعبه قابل جابجایی روی یک پسزمینه ثابت وجود دارد. شما میتوانید رنگ جعبه را با استفاده از ورودی انتخاب رنگ تغییر دهید.
اما یک باگ وجود دارد. اگر ابتدا جعبه را جابجا کنید، سپس رنگ آن را تغییر دهید، پسزمینه (که نباید جابجا شود!) به موقعیت جعبه "پرش" میکند. اما این نباید اتفاق بیفتد: ویژگی position
از Background
به initialPosition
تنظیم شده است که { x: 0, y: 0 }
است. چرا پسزمینه پس از تغییر رنگ حرکت میکند؟
باگ را پیدا کنید و آن را اصلاح کنید.
اگر چیزی به طور غیرمنتظره تغییر کند، یک تغییر مستقیم وجود دارد. تغییر را در App.js
پیدا کنید و آن را اصلاح کنید.
مشاهده کد و اصلاح آن در CodeSandbox
import { useState } from 'react';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, setShape] = useState({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
shape.position.x += dx;
shape.position.y += dy;
}
function handleColorChange(e) {
setShape({
...shape,
color: e.target.value
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}
3. بروزرسانی یک شی با Immer
این چالش همان چالش باگدار قبلی است. این بار، تغییر را با استفاده از Immer اصلاح کنید. برای راحتی شما، useImmer
از قبل وارد شده است، بنابراین باید متغیر وضعیت shape
را با استفاده از آن تغییر دهید.
مشاهده و اصلاح کد در CodeSandbox
import { useState } from 'react';
import { useImmer } from 'use-immer';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, setShape] = useState({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
shape.position.x += dx;
shape.position.y += dy;
}
function handleColorChange(e) {
setShape({
...shape,
color: e.target.value
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}
راه حل چالش ها
چالش اول
در اینجا یک نسخه با هر دو باگ برطرف شده را مشاهده می کنید: مشاهده راه حل در CodeSandbox
import { useState } from 'react';
export default function Scoreboard() {
const [player, setPlayer] = useState({
firstName: 'Ranjani',
lastName: 'Shettar',
score: 10,
});
function handlePlusClick() {
setPlayer({
...player,
score: player.score + 1,
});
}
function handleFirstNameChange(e) {
setPlayer({
...player,
firstName: e.target.value,
});
}
function handleLastNameChange(e) {
setPlayer({
...player,
lastName: e.target.value
});
}
return (
<>
<label>
Score: <b>{player.score}</b>
{' '}
<button onClick={handlePlusClick}>
+1
</button>
</label>
<label>
First name:
<input
value={player.firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
value={player.lastName}
onChange={handleLastNameChange}
/>
</label>
</>
);
}
مشکل رویداد handlePlusClick
این بود که شی player
را مستقیما تغییر میداد. در نتیجه، React متوجه تغییر و نیاز به رندر مجدد نمی شد و امتیاز را روی صفحه بهروزرسانی نمی کرد. به همین دلیل بود که وقتی نام کوچک را ویرایش کردید، حالت بهروزرسانی شد و یک رندر مجدد را فعال کرد که باعث بروز شدن امتیاز نیز می شد.
مشکل دیگر در رویداد handleLastNameChange
این بود که آن فیلدهای ...player
موجود را در شی جدید کپی نمی کرد. به همین دلیل بود که امتیاز بعد از ویرایش نام خانوادگی از بین رفت.
چالش دوم
مشکل در تغییر مستقیم درون handleMove
بود. این تابع مقدار shape.position
را مستقیما تغییر میداد، اما این همان شی است که initialPosition
به آن اشاره میکند. به همین دلیل است که هم شکل و هم پسزمینه حرکت میکنند. (این یک تغییر مستقیم است، بنابراین تغییر تا زمانی که یک بهروزرسانی غیرمرتبط -مانند تغییر رنگ- فعال نشود ، منعکس نمیشود.)
راهحل این است که تغییر مستقیم را از handleMove
حذف کنید و از سینتکس گسترش برای کپی کردن شی استفاده کنید. توجه داشته باشید که عملگر +=
یک تغییر مستقیم است، بنابراین باید آن را بازنویسی کنید تا از عملیات معمولی +
استفاده شود.
مشاهده راه حل در CodeSandbox
import { useState } from 'react';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, setShape] = useState({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
setShape({
...shape,
position: {
x: shape.position.x + dx,
y: shape.position.y + dy,
}
});
}
function handleColorChange(e) {
setShape({
...shape,
color: e.target.value
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}
چالش سوم
این راهحل با Immer دوباره نوشته شده است. توجه کنید که چگونه رویدادها به صورت تغییرپذیر نوشته شدهاند، اما باگ رخ نمیدهد. این به این دلیل است که Immer هرگز اشیا موجود را تغییر نمیدهد. مشاهده راه حل در CodeSandbox
import { useImmer } from 'use-immer';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, updateShape] = useImmer({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
updateShape(draft => {
draft.position.x += dx;
draft.position.y += dy;
});
}
function handleColorChange(e) {
updateShape(draft => {
draft.color = e.target.value;
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}