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

آرایهها در جاوااسکریپت قابل تغییر (mutable) هستند، اما هنگام ذخیرهسازی آنها در وضعیت (state) باید آنها را بهعنوان غیرقابل تغییر (immutable) در نظر بگیرید. درست مانند اشیا، وقتی میخواهید آرایهای که در وضعیت ذخیره شده را بهروزرسانی کنید، باید یک آرایه جدید ایجاد کنید (یا یک کپی از یک آرایه موجود بسازید) و سپس وضعیت را طوری تنظیم کنید که از آرایه جدید استفاده کند.
بهروزرسانی آرایهها بدون تغییر مستقیم
در جاوااسکریپت، آرایهها نوع دیگری از اشیا هستند. مانند اشیا، شما باید آرایههای وضعیت را به صورت فقط خواندنی در نظر بگیرید. این بدان معناست که نباید آیتمهای آرایه را مثل arr[0] = 'bird'
مقدار دهی کنید و همچنین نباید از متدهایی استفاده کنید که آرایه را تغییر میدهند، مانند push()
و pop()
.
بهجای این کار، هر بار که میخواهید یک آرایه را بهروزرسانی کنید، باید یک آرایه جدید به تابع تنظیم وضعیت ارسال کنید. برای انجام این کار، میتوانید با فراخوانی متدهای غیرقابل تغییر خود مانند filter()
و map()
از آرایه اصلی یک آرایه جدید بسازید. سپس میتوانید وضعیت را با آرایه جدید مقدار دهی کنید.
در اینجا یک جدول مرجع برای عملیات رایج آرایهها آورده ایم. وقتی با آرایههای وضعیت در React کار میکنید، باید از متدهای ستون چپ پرهیز کنید و بهجای آن متدهای ستون راست را ترجیح دهید:
پرهیز (تغییرات در آرایه) | ترجیح (بازگشت آرایه جدید) | |
---|---|---|
اضافه کردن | push , unshift | concat و [...arr] سینتکس گسترش |
حذف | pop , shift , splice | filter و slice |
جایگزینی | splice , arr[i] = ... تخصیص | map |
مرتبسازی | reverse , sort | آرایه را اول باید کپی کنید |
بهعلاوه، میتوانید از کتابخانه Immer استفاده کنید که به شما اجازه میدهد از متدهای هر دو ستون استفاده کنید.
متاسفانه در جاوااسکریپت slice
و splice
مشابه یکدیگر نامگذاری شدهاند، اما بسیار متفاوت هستند:
slice
به شما اجازه میدهد یک آرایه یا بخشی از آن را کپی کنید.splice
آرایه را تغییر میدهد (برای درج یا حذف آیتمها).
در React، شما معمولاً از slice
(بدون p
!) بسیار بیشتر استفاده خواهید کرد زیرا نمیخواهید اشیا یا آرایههای وضعیت را تغییر دهید. در مطلب بهروزرسانی اشیا معنی تغییر مستقیم (mutation) را توضیح دادیم و اینکه چرا برای وضعیت توصیه نمی کنیم که از آن استفاده کنید.
اضافه کردن به یک آرایه
متد push()
، آرایه را تغییر میدهد، که نباید از آن استفاده کنید:
مشاهده کد و اجرای آن در CodeSandbox
import { useState } from 'react';
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
artists.push({
id: nextId++,
name: name,
});
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
بهجای آن، یک آرایه جدید ایجاد کنید که شامل آیتمهای موجود و یک آیتم جدید در انتهای آن باشد. چندین روش برای انجام این کار وجود دارد، اما سادهترین آن استفاده از سینتکس گسترش است:
setArtists( // وضعیت را بروز کن
[ // با آرایه ای جدید
...artists, // که شامل تمامی آیتم های قدیم است
{ id: nextId++, name: name } // و یک آیتم جدید در انتها به آن اضافه شده
]
);
اکنون به درستی کار میکند: مشاهده کد و اجرای آن در CodeSandbox
import { useState } from 'react';
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
setArtists([
...artists,
{ id: nextId++, name: name }
]);
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
سینتکس گسترش آرایه همچنین به شما اجازه میدهد که یک آیتم را با قرار دادن آن قبل از ...artists
به اول آرایه اضافه کنید:
setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);
به این ترتیب گسترش میتواند کار push()
را با اضافه کردن به انتهای آرایه و unshift()
را با اضافه کردن به ابتدای آرایه انجام دهد. آن را در محیط آزمایش بالا امتحان کنید!
حذف از یک آرایه
سادهترین راه برای حذف یک آیتم از یک آرایه این است که آن را فیلتر کنید. به عبارت دیگر، شما یک آرایه جدید تولید میکنید که شامل آن آیتم نخواهد بود. برای انجام این کار از متد filter
استفاده کنید، بهعنوان مثال:
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
let initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [artists, setArtists] = useState(
initialArtists
);
return (
<>
<h1>Inspiring sculptors:</h1>
<ul>
{artists.map(artist => (
<li key={artist.id}>
{artist.name}{' '}
<button onClick={() => {
setArtists(
artists.filter(a =>
a.id !== artist.id
)
);
}}>
Delete
</button>
</li>
))}
</ul>
</>
);
}
چند بار روی دکمه "حذف" کلیک کنید و به هندلر کلیک آن را نگاه کنید.
setArtists(
artists.filter(a => a.id !== artist.id)
);
در اینجا، artists.filter(a => a.id !== artist.id)
به این معنی است که "یک آرایه ایجاد کنید که شامل مقادیر artists
است که شناسههای آنها با artist.id
متفاوت است باشد". به عبارت دیگر، دکمه "حذف" هر هنرمند، آن هنرمند را از آرایه فیلتر میکند و سپس درخواست بازنویسی با آرایه حاصل را میدهد. توجه داشته باشید که filter
آرایه اصلی را تغییر نمیدهد.
تغییر دادن یک آرایه
اگر میخواهید برخی یا تمامی آیتمهای آرایه را تغییر دهید، میتوانید از map()
برای ایجاد یک آرایه جدید استفاده کنید. تابعی که به map
ارسال میکنید، میتواند براساس داده یا اندیس هر آیتم (یا هر دو) تصمیم بگیرد چه کار کند.
در این مثال، یک آرایه، مختصات دو دایره و یک مربع را نگه میدارد. وقتی دکمه را فشار میدهید، فقط دایرهها به اندازه 50 پیکسل پایین میروند. این کار را با تولید یک آرایه جدید از دادهها با استفاده از map()
انجام میدهد:
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
let initialShapes = [
{ id: 0, type: 'circle', x: 50, y: 100 },
{ id: 1, type: 'square', x: 150, y: 100 },
{ id: 2, type: 'circle', x: 250, y: 100 },
];
export default function ShapeEditor() {
const [shapes, setShapes] = useState(
initialShapes
);
function handleClick() {
const nextShapes = shapes.map(shape => {
if (shape.type === 'square') {
// No change
return shape;
} else {
// Return a new circle 50px below
return {
...shape,
y: shape.y + 50,
};
}
});
// Re-render with the new array
setShapes(nextShapes);
}
return (
<>
<button onClick={handleClick}>
Move circles down!
</button>
{shapes.map(shape => (
<div
key={shape.id}
style={{
background: 'purple',
position: 'absolute',
left: shape.x,
top: shape.y,
borderRadius:
shape.type === 'circle'
? '50%' : '',
width: 20,
height: 20,
}} />
))}
</>
);
}
جایگزینی آیتمها در یک آرایه
معمولاً نیاز است که یک یا چند آیتم در یک آرایه را جایگزین کنید. تخصیصهایی مانند arr[0] = 'bird'
آرایه اصلی را تغییر میدهند، بنابراین بهجای آن باید از map
برای این کار استفاده کنید.
برای جایگزینی یک آیتم، با map
یک آرایه جدید ایجاد کنید. در داخل فراخوانی map
، شما اندیس آیتم را به عنوان دومین آرگومان دریافت خواهید کرد. از آن برای تصمیمگیری در مورد اینکه آیا آیتم اصلی (آرگومان اول) را بازگردانید یا چیز دیگری استفاده کنید، استفاده کنید:
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
let initialCounters = [
0, 0, 0
];
export default function CounterList() {
const [counters, setCounters] = useState(
initialCounters
);
function handleIncrementClick(index) {
const nextCounters = counters.map((c, i) => {
if (i === index) {
// Increment the clicked counter
return c + 1;
} else {
// The rest haven't changed
return c;
}
});
setCounters(nextCounters);
}
return (
<ul>
{counters.map((counter, i) => (
<li key={i}>
{counter}
<button onClick={() => {
handleIncrementClick(i);
}}>+1</button>
</li>
))}
</ul>
);
}
درج در یک آرایه
گاهی اوقات ممکن است بخواهید یک آیتم را در یک موقعیت خاص درج کنید که نه در ابتدای آرایه است و نه در انتها. برای انجام این کار، میتوانید از سینتکس گسترش ...
آرایه به همراه متد slice()
استفاده کنید. متد slice()
به شما اجازه میدهد "برشی" از آرایه را بگیرید. برای درج یک آیتم، آرایهای ایجاد میکنید که ابتدا برشی قبل از نقطه درج بزند، سپس آیتم جدید را اضافه کند و سپس بقیه آرایه اصلی را قرار دهد.
در این مثال، دکمه درج همیشه در ایندکس 1
آیتم را درج میکند:
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
let nextId = 3;
const initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState(
initialArtists
);
function handleClick() {
const insertAt = 1; // Could be any index
const nextArtists = [
// Items before the insertion point:
...artists.slice(0, insertAt),
// New item:
{ id: nextId++, name: name },
// Items after the insertion point:
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={handleClick}>
Insert
</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
ایجاد تغییرات دیگر در یک آرایه
برخی کارها وجود دارد که نمیتوانید به سادگی با سینتکس گسترش و متدهای غیرقابل تغییر مانند map()
و filter()
انجام دهید. بهعنوان مثال، ممکن است بخواهید یک آرایه را معکوس کنید یا مرتب سازید. متدهای جاوااسکریپت reverse()
و sort()
آرایه اصلی را تغییر میدهند و بنابراین نمیتوانید بهطور مستقیم از آنها استفاده کنید.
با این حال، میتوانید ابتدا آرایه را کپی کرده و سپس تغییرات مورد نظر را انجام دهید.
بهعنوان مثال: مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
const initialList = [
{ id: 0, title: 'Big Bellies' },
{ id: 1, title: 'Lunar Landscape' },
{ id: 2, title: 'Terracotta Army' },
];
export default function List() {
const [list, setList] = useState(initialList);
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
return (
<>
<button onClick={handleClick}>
Reverse
</button>
<ul>
{list.map(artwork => (
<li key={artwork.id}>{artwork.title}</li>
))}
</ul>
</>
);
}
در اینجا، شما از سینتکس گسترش [...list]
برای ایجاد کپی از آرایه اصلی استفاده میکنید. اکنون که یک کپی دارید، میتوانید از متدهای تغییر دهنده مانند nextList.reverse()
یا nextList.sort()
، یا حتی تخصیص آیتمهای فردی با nextList[0] = "چیزی"
استفاده کنید.
با این حال، حتی اگر شما یک آرایه را کپی کنید، نمیتوانید بهطور مستقیم آیتمهای موجود در آن را تغییر دهید. این به این دلیل است که کپی به صورت سطحی است - آرایه جدید همان آیتمها را به عنوان آرایه اصلی خواهد داشت. بنابراین اگر یک شی داخل آرایه کپیشده را تغییر دهید، در واقع وضعیت موجود را دستکاری میکنید. برای مثال، کدی مانند این یک مشکل است.
const nextList = [...list];
nextList[0].seen = true; // تغییر مستقیم ویژگی آیتم اول
setList(nextList);
اگرچه nextList
و list
دو آرایه متفاوت هستند، اما بنابراین با تغییر nextList[0].seen
، شما همچنین list[0].seen
را تغییر میدهید. این یک تغییر وضعیت مستقیم (state mutation) است که باید از آن جلوگیری کنید! شما میتوانید این مشکل را به روشی مشابه با بهروزرسانی اشیا تو در تو جاوااسکریپت حل کنید - با کپی کردن آیتمهایی که میخواهید تغییر دهید بهجای تغییر آنها. در ادامه روش انجام این کار را شرح می دهیم.
بهروزرسانی اشیا داخل آرایهها
اشیا واقعاً در "داخل" آرایهها قرار ندارند. ممکن است در کد به نظر برسد که در "داخل" آن هستند، اما هر شی داخل یک آرایه یک مقدار مجزا است که آرایه به آن "اشاره" میکند. به همین دلیل لازم است هنگام تغییر فیلدهای تو در تو مانند list[0]
احتیاط کنید. لیست آثار هنری یک شخص ممکن است به همان عنصر آرایه اشاره کند!
هنگام بهروزرسانی وضعیت تو در تو، باید از نقطهای که میخواهید بهروزرسانی کنید و تا سطح بالا کپی ایجاد کنید. بیایید ببینیم این چگونه کار میکند.
در این مثال، دو لیست آثار هنری جداگانه دارای وضعیت اولیه مشابه هستند. آنها باید جداسازی شده باشند، اما به دلیل یک تغییر، وضعیت آنها بهطور تصادفی به اشتراک گذاشته میشود و تیک زدن آیتم در یکی از لیستها بر روی لیست دیگر تأثیر میگذارد:
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(
initialList
);
function handleToggleMyList(artworkId, nextSeen) {
const myNextList = [...myList];
const artwork = myNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setMyList(myNextList);
}
function handleToggleYourList(artworkId, nextSeen) {
const yourNextList = [...yourList];
const artwork = yourNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setYourList(yourNextList);
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
مشکل در کدی مانند این است:
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);
اگرچه آرایه myNextList
جدید است، آیتمها خودشان همان آیتم های آرایه myList
هستند. بنابراین با تغییر artwork.seen
، آیتم آثار هنری اصلی تغییر میکند. آن آیتم آثار هنری همچنین در yourList
وجود دارد که باعث بروز باگ میشود. باگهای این چنینی ممکن است عیب یابی و رفعشان دشوار باشد، اما اگر از تغییر وضعیت جلوگیری کنید خوشبختانه برطرف میشوند.
شما میتوانید از
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
در اینجا علامت ...
سینتکس گسترش شی است که برای ایجاد کپی از یک شی استفاده میشود.
با این روش، هیچ یک از آیتمهای موجود در وضعیت تغییر نمیکند و باگ برطرف میشود:
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(
initialList
);
function handleToggleMyList(artworkId, nextSeen) {
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
}
function handleToggleYourList(artworkId, nextSeen) {
setYourList(yourList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
بهطور کلی، شما باید فقط اشیائی را که تازه ایجاد کردهاید تغییر دهید. اگر شما در حال درج یک اثر هنری جدید بودید، میتوانید آن را تغییر دهید، اما اگر با چیزی که در وضعیت وجود دارد سر و کار دارید، باید یک کپی ایجاد کنید.
نوشتن منطق بهروزرسانی مختصر با Immer
بهروزرسانی آرایههای تو در تو بدون تغییر مستقیم میتواند مانند اشیا کمی تکراری شود:
- بهطور کلی، نباید نیاز به بهروزرسانی وضعیت بیشتر از چند لایه داشته باشید. اگر اشیای وضعیت بسیار عمیق هستند، باید آنها را به صورت متفاوتی ساختاردهی کنید تا به صورت تخت باشند.
- اگر نمیخواهید ساختار وضعیت خود را تغییر دهید، ممکن است ترجیح دهید از Immer استفاده کنید، که به شما اجازه میدهد با استفاده از سینتکس راحت اما تغییر دهنده، کد نویسی کنید و فرآیند تولید کپیها را برای شما انجام میدهد.
در اینجا مثال لیست آثار هنری دوبارهنویسیشده با Immer را مشاهده می کنید:
مشاهده نتیجه اجرای کد در CodeSandbox
import { useState } from 'react';
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, updateMyList] = useImmer(
initialList
);
const [yourList, updateYourList] = useImmer(
initialList
);
function handleToggleMyList(id, nextSeen) {
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});
}
function handleToggleYourList(artworkId, nextSeen) {
updateYourList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
توجه داشته باشید که با Immer، تغییراتی مانند
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
این به این دلیل است که شما در حال تغییر وضعیت اصلی نیستید، بلکه در حال تغییر یک شی پیش نویس خاص هستید که توسط Immer ایجاد شدهاست. بهطور مشابه، میتوانید متدهای تغییردهنده مانند push()
و pop()
را بر روی محتویات draft
اعمال کنید.
در پسزمینه، Immer همیشه وضعیت بعدی را از صفر بر اساس تغییرات انجامشده بر روی پیش نویش میسازد. این کدهای رویداد را بسیار مختصر نگه میدارد بدون اینکه هرگز وضعیت را تغییر دهید.
جمع بندی
- شما میتوانید آرایهها را در وضعیت قرار دهید، اما نمیتوانید آنها را تغییر دهید.
- بهجای تغییر یک آرایه، یک نسخه جدید از آن ایجاد کنید و وضعیت را بآ آن بهروزرسانی کنید.
- میتوانید از سینتکس گسترش
[...arr, newItem]
برای ایجاد آرایههایی با آیتمهای جدید استفاده کنید. - میتوانید از
filter()
وmap()
برای ایجاد آرایههای جدید با آیتمهای فیلتر شده یا تغییر یافته استفاده کنید. - میتوانید از Immer برای مختصر نویسی استفاده کنید.
چالش ها
1. بهروزرسانی یک آیتم در سبد خرید
منطق handleIncreaseClick
را جوری کامل کنید تا با فشار دادن دکمه "+"، تعداد مربوطه افزایش یابد:
مشاهده و اطلاح کد در CodeSandbox
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
</li>
))}
</ul>
);
}
2. حذف یک آیتم از سبد خرید
دکمه "+" این سبد خرید کار میکند، اما دکمه "–" هیچ کاری انجام نمیدهد. شما باید یک هندلر رویداد به آن اضافه کنید تا با فشار دادن آن count
محصول مربوطه کاهش یابد. اگر هنگامی که count
برابر با 1 است روی "–" فشار دهید، محصول باید بهطور خودکار از سبد خرید حذف شود. مطمئن شوید که هرگز 0 نشان داده نشود.
مشاهده و اصلاح کد در CodeSandbox
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
setProducts(products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count + 1
};
} else {
return product;
}
}))
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
<button>
–
</button>
</li>
))}
</ul>
);
}
3. رفع تغییرات مستقیم با استفاده از متدهای غیرقابل تغییر
در این مثال، تمام هندلرهای رویداد در App.js
از تغییر مستقیم استفاده میکنند. در نتیجه، ویرایش و حذف آیتم ها کار نمیکند. توابع handleAddTodo
، handleChangeTodo
و handleDeleteTodo
را دوبارهنویسی کنید تا از متدهای غیرقابل تغییر استفاده کنند:
مشاهده و اصلاح کد در CodeSandbox
import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(
initialTodos
);
function handleAddTodo(title) {
todos.push({
id: nextId++,
title: title,
done: false
});
}
function handleChangeTodo(nextTodo) {
const todo = todos.find(t =>
t.id === nextTodo.id
);
todo.title = nextTodo.title;
todo.done = nextTodo.done;
}
function handleDeleteTodo(todoId) {
const index = todos.findIndex(t =>
t.id === todoId
);
todos.splice(index, 1);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
4. رفع تغییرات مستقیم با استفاده از Immer
این همان مثال چالش قبلی است. این بار، تغییرات را با استفاده از Immer اصلاح کنید. برای راحتی شما، useImmer
قبلاً ایمپورت شده است، بنابراین شما باید متغیر وضعیت todos
را تغییر دهید تا از آن استفاده کند.
مشاهده و حل چالش در CodeSandbox
import { useState } from 'react';
import { useImmer } from 'use-immer';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(
initialTodos
);
function handleAddTodo(title) {
todos.push({
id: nextId++,
title: title,
done: false
});
}
function handleChangeTodo(nextTodo) {
const todo = todos.find(t =>
t.id === nextTodo.id
);
todo.title = nextTodo.title;
todo.done = nextTodo.done;
}
function handleDeleteTodo(todoId) {
const index = todos.findIndex(t =>
t.id === todoId
);
todos.splice(index, 1);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
راه حل چالش ها
چالش اول
میتوانید از تابع map
برای ایجاد یک آرایه جدید استفاده کنید و سپس از سینتکس گسترش ...
شی برای ایجاد کپی از شی تغییر یافته برای آرایه جدید استفاده کنید:
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
setProducts(products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count + 1
};
} else {
return product;
}
}))
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
</li>
))}
</ul>
);
}
چالش دوم
شما میتوانید ابتدا از map
برای تولید یک آرایه جدید استفاده کنید و سپس از filter
برای حذف محصولات با count
برابر با 0
استفاده کنید:
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
setProducts(products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count + 1
};
} else {
return product;
}
}))
}
function handleDecreaseClick(productId) {
let nextProducts = products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count - 1
};
} else {
return product;
}
});
nextProducts = nextProducts.filter(p =>
p.count > 0
);
setProducts(nextProducts)
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
<button onClick={() => {
handleDecreaseClick(product.id);
}}>
–
</button>
</li>
))}
</ul>
);
}
چالش سوم
در handleAddTodo
میتوانید از سینتکس گسترش آرایه استفاده کنید. در handleChangeTodo
میتوانید با map
یک آرایه جدید ایجاد کنید. در handleDeleteTodo
میتوانید با filter
یک آرایه جدید ایجاد کنید. اکنون لیست به درستی کار میکند:
import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(
initialTodos
);
function handleAddTodo(title) {
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
]);
}
function handleChangeTodo(nextTodo) {
setTodos(todos.map(t => {
if (t.id === nextTodo.id) {
return nextTodo;
} else {
return t;
}
}));
}
function handleDeleteTodo(todoId) {
setTodos(
todos.filter(t => t.id !== todoId)
);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
چالش چهارم
با Immer، میتوانید کد را به صورت تغییر مستقیم بنویسید، به شرطی که تنها قسمتهایی که اشیا پیش نویسی که Immer به شما میدهد را تغییر دهید. در اینجا، همه تغییرات روی پیش نویش انجام میشود و بنابراین کد کار میکند:
import { useState } from 'react';
import { useImmer } from 'use-immer';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, updateTodos] = useImmer(
initialTodos
);
function handleAddTodo(title) {
updateTodos(draft => {
draft.push({
id: nextId++,
title: title,
done: false
});
});
}
function handleChangeTodo(nextTodo) {
updateTodos(draft => {
const todo = draft.find(t =>
t.id === nextTodo.id
);
todo.title = nextTodo.title;
todo.done = nextTodo.done;
});
}
function handleDeleteTodo(todoId) {
updateTodos(draft => {
const index = draft.findIndex(t =>
t.id === todoId
);
draft.splice(index, 1);
});
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
همچنین میتوانید رویکردهای تغییردهنده و غیرقابل تغییر را با Immer ترکیب و تطبیق دهید.
بهعنوان مثال، در این نسخه handleAddTodo
با تغییر شی Immer پیادهسازی شده است، در حالی که handleChangeTodo
و handleDeleteTodo
از متدهای غیرقابل تغییر map
و filter
استفاده میکنند:
مشاهده راه حل ترکیبی در CodeSandbox
import { useState } from 'react';
import { useImmer } from 'use-immer';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, updateTodos] = useImmer(
initialTodos
);
function handleAddTodo(title) {
updateTodos(draft => {
draft.push({
id: nextId++,
title: title,
done: false
});
});
}
function handleChangeTodo(nextTodo) {
updateTodos(todos.map(todo => {
if (todo.id === nextTodo.id) {
return nextTodo;
} else {
return todo;
}
}));
}
function handleDeleteTodo(todoId) {
updateTodos(
todos.filter(t => t.id !== todoId)
);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
با Immer، میتوانید سبک مورد نظر خود را برای هر مورد جداگانه انتخاب کنید.