به روز رسانی آرایهها در State
آرایهها در جاوااسکریپت mutable (قابل تغییر مستقیم) هستند، اما توصیه میشود هنگامی که آنها را در state ذخیره میکنید، با آنها به شکل غیرقابل تغییر برخورد کنید. درست مانند objectها، هنگامی که میخواهید یک آرایه را در state ذخیره کنید، لازم است که یک آرایه جدید بسازید (یا یک آرایه موجود را کپی کنید)، و سپس state را به آرایه جدید تنظیم کنید تا از آن استفاده شود.
You will learn
- چگونه با state ریاکت به آرایه آیتم اضافه کرده، آیتمهای موجود را حذف کرده یا تغییر دهیم
- چگونه یک object داخل یک آرایه را به روز رسانی کنیم
- چگونه به کمک Immer هنگام کپی کردن آرایه از تکرار زیاد جلوگیری کنیم
به روز رسانی آرایهها
در جاوااسکریپت، آرایهها نوعی object هستند. مانند objectها، باید با آرایهها در state بهصورت فقطخواندنی رفتار کنید. این یعنی نباید آیتمهای یک آرایه را با arr[0] = 'bird'
دوباره مقداردهی کنید، و همچنین نباید از متدهایی که آرایه را تغییر میدهند، مانند push()
و pop()
، استفاده کنید.
در عوض، هر بار که میخواهید یک آرایه را بهروزرسانی کنید، باید یک آرایه جدید را به تابع تنظیم state خود بدهید. برای این کار میتوانید با فراخوانی متدهای بدون تغییر (non-mutating) مانند filter()
و map()
یک آرایه جدید از آرایه اصلی بسازید. سپس میتوانید state را به آرایه جدید تنظیم کنید.
در اینجا یک جدول مرجع از عملیاتهای رایج بر روی آرایهها را مشاهده میکنید. در هنگام کار کردن با آرایهها در state ریاکت، باید از استفاده از متدهای ستون سمت راست خودداری کرده، و در عوض از متدهای ستون سمت چپ استفاده کنید:
خودداری شود (آرایه را تغییر میدهد) | ترجیح داده شود (آرایه جدید ایجاد میکند) | |
---|---|---|
اضافه کردن | push , unshift | concat , [...arr] spread syntax (مثال) |
حذف کردن | pop , shift , splice | filter , slice (example) |
جایگزین کردن | splice , arr[i] = ... assignment | map (example) |
مرتب سازی | reverse , sort | ابتدا از آرایه کپی بگیرید (example) |
بهعنوان جایگزین میتوانید از Immer استفاده کنید که امکان استفاده از متدهای هر دو ستون را فراهم میکند.
افزودن به آرایه
متد push()
آرایه را تغییر میدهد، که مطلوب نیست:
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> </> ); }
در عوض، باید یک آرایه جدید ایجاد کنید که شامل آیتمهای فعلی و یک آیتم جدید در انتها باشد. برای انجام این کار، چندین راه وجود دارد، اما آسانترین راه این است که از سینتکس ...
array spread (پخش آرایه) استفاده کنید:
setArtists( // Replace the state
[ // with a new array
...artists, // that contains all the old items
{ id: nextId++, name: name } // and one new item at the end
]
);
حالا کد بهدرستی کار میکند:
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> </> ); }
همچنین، عملگر spread آرایه این امکان را به شما میدهد که با قرار دادن آیتم پیش از ...artists
، آن را به ابتدای آرایه اضافه کنید:
setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);
بدینگونه، عملگر spread میتواند کار متد push()
را با افزودن به انتهای آرایه و کار unshift()
را با افزودن به ابتدای آرایه انجام دهد. آن را در sandbox فوق امتحان کنید!
حذف کردن از آرایه
آسان ترین راه برای حذف کردن یک آیتم از یک آرایه این است که آن را فیلتر کنید. به بیان دیگر، شما یک آرایه جدید ایجاد میکنید که آن آیتم را شامل نمیشود. برای انجام این عمل، از متد filter
استفاده کنید:
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> </> ); }
چند بار بر دکمه “Delete” کلیک کنید و به click handler آن نگاه کنید.
setArtists(
artists.filter(a => a.id !== artist.id)
);
در اینجا، artists.filter(a => a.id !== artist.id)
به این معنی است که “یک آرایه ایجاد کن که شامل artists
ای باشد که ID آنها با artist.id
متفاوت است”. به بیان دیگر، دکمه “Delete” هر artist همان artist را از آرایه فیلتر میکند و سپس درخواست یک رندر مجدد با آرایه حاصل میدهد. توجه کنید که filter
آرایه اصلی را تغییر نمیدهد.
تغییر دادن آرایه
اگر میخواهید تعدادی از آیتمها یا تمامی آیتمهای آرایه را تغییر دهید، میتوانید از map()
برای ایجاد یک آرایه جدید استفاده کنید. تابعی که به map
میدهید میتواند بر اساس داده یا اندیس هر آیتم (یا هر دو) تصمیم بگیرد که چه کند.
در این مثال، یک آرایه مختصات دو دایره و یک مربع را نگه میدارد. هنگامی که دکمه را فشار میدهید، فقط دایرهها را به اندازهٔ 50 پیکسل به پایین حرکت میدهد. این کار با ایجاد یک آرایهٔ جدید از دادهها توسط map()
انجام میشود:
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
، اندیس آیتم (آرگومان دوم) را دریافت میکنید. از این اندیس استفاده کنید تا مشخص کنید آیتم اصلی (آرگومان اول) را بازگردانید یا مقدار دیگری را.
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()
به شما این امکان را میدهد که یک “slice (برش)” از آرایه جدا کنید. برای الحاق یک آیتم، آرایهای میسازید که ابتدا برش قبل از نقطهٔ الحاق را گسترش میدهد، سپس آیتم جدید را قرار میدهد و در نهایت باقی آرایه اصلی را اضافه میکند.
در این مثال، دکمهٔ الحاق همیشه آیتم را در اندیس 1
الحاق میکند:
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> </> ); }
اعمال تغییرات دیگر بر یک آرایه
برخی اعمال تنها با استفاده از سینتکس spread و متدهای non-mutating مانند map()
و filter()
قابل انجام نیستند. برای مثال، ممکن است بخواهید یک آرایه را معکوس یا مرتب کنید. متدهای reverse()
و sort()
در جاوااسکریپت آرایه اصلی را تغییر میدهند، بنابراین نمیتوانید مستقیماً از آنها استفاده کنید.
با این حال، شما میتوانید ابتدا آرایه را کپی کرده، و سپس تغییرات را بر آن اعمال کنید.
برای مثال:
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]
spread برای ایجاد یک کپی از آرایه اصلی استفاده میکنید. حال که یک کپی از آن دارید، میتوانید از متدهای mutating مانند nextList.reverse()
یا nextList.sort()
استفاده کنید، یا حتی آیتمها را به شکل nextList[0] = "something"
به طور مجزا مقدار دهی کنید.
با این حال، حتی اگر آرایه را کپی کنید، نمیتوانید آیتمهای موجود در داخل آن را به طور مستقیم تغییر دهید. این به این دلیل است که کپی بهصورت سطحی انجام میشود—آرایهٔ جدید همان آیتمهای آرایهٔ اصلی را در بر خواهد داشت. بنابراین، اگر یک object داخل آرایه کپیشده را تغییر دهید، در واقع در حال تغییر state موجود هستید. برای مثال، چنین کدی مشکل ایجاد میکند.
const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);
گرچه nextList
و list
دو آرایهٔ متفاوت هستند، nextList[0]
و list[0]
به یک object اشاره میکنند. بنابراین با تغییر دادن nextList[0].seen
، در واقع list[0].seen
را هم تغییر میدهید. این یک تغییر state است و باید از آن اجتناب کنید! میتوانید این مشکل را مشابه بهروزرسانی object های تودرتو در جاوااسکریپت—بهجای تغییر مستقیم، آیتمهایی را که میخواهید تغییر دهید کپی کنید. در ادامه روش این کار را میبینید.
بهروزرسانی objectهای داخل آرایهها
objectها واقعا در “داخل” آرایهها قرار ندارند. ممکن است در کد چنین به نظر برسد، اما هر object داخل آرایه یک مقدار مستقل است که آرایه به آن “اشاره” میکند. به همین دلیل هنگام تغییر فیلدهای تودرتو مانند list[0]
باید مراقب باشید؛ ممکن است آرایهٔ artwork شخص دیگری به همان آیتم اشاره کند!
هنگام بهروزرسانی stateهای تودرتو، باید از همان نقطهای که میخواهید تغییر دهید تا بالاترین سطح، کپیهایی ایجاد کنید. بیایید ببینیم این کار چگونه انجام میشود.
در این مثال، دو لیست artwork جداگانه state اولیه یکسانی دارند. آنها باید از هم مستقل باشند، اما به دلیل یک تغییر، state آنها بهطور تصادفی مشترک شده است، و انتخاب یک چکباکس در یک لیست بر لیست دیگر نیز تأثیر میگذارد:
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
آیتم artwork اصلی را تغییر میدهد. این آیتم artwork در لیست yourList
نیز وجود دارد، که باعث بروز باگ میشود. در نظر گرفتن چنین باگهایی میتواند دشوار باشد، اما خوشبختانه اگر از mutate کردن state خودداری کنید این باگها بروز پیدا نمیکنند.
شما میتوانید با استفاده از map
یک آیتم قدیمی را بدون mutate کردن با نسخه به روز رسانی شده اش جایگزین کنید.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
در اینجا ...
سینتکس object spread است که برای ایجاد یک کپی از یک object استفاده میشود.
با استفاده از این روش، هیچیک از stateهای موجود تغییر داده نمیشوند و باگ برطرف میشود:
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> ); }
بهطور کلی، باید فقط objectهایی را تغییر دهید که تازه ایجاد شدهاند. اگر بخواهید یک artwork جدید را الحاق کنید، میتوانید آن را تغییر دهید؛ اما اگر با objectی سروکار دارید که از قبل در state وجود دارد، باید ابتدا از آن یک کپی بسازید.
به کمک Immer منطق بهروزرسانی را به شکل مختصر بنویسید
بهروزرسانی آرایههای تودرتو بدون تغییر میتواند کمی تکراری شود. درست مانند objectها:
- بهطور کلی، معمولاً نیازی به بهروزرسانی state در عمقی بیش از چند لایه ندارید. اگر objectهای state شما بسیار عمیق هستند، بهتر است ساختارشان را به گونهای تغییر دهید که مسطح شوند.
- اگر نمیخواهید ساختار state خود را تغییر دهید، میتوانید از Immer استفاده کنید؛ این کتابخانه به شما اجازه میدهد با سینتکس رایج اما تغییردهنده کد بنویسید و در پسزمینه نسخههای کپی را برایتان ایجاد میکند.
در اینجا مثال Art Bucket List با Immer بازنویسی شده است:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
دقت کنید که با استفاده از Immer، عملیاتی مانند artwork.seen = nextSeen
که یک تغییر (mutation) به حساب میآید، اکنون بدون مشکل است:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
دلیل این امر آن است که شما state اصلی را تغییر نمیدهید، بلکه یک draft
(پیشنویس) ویژه که توسط Immer ایجاد شده است را تغییر میدهید. به همین ترتیب، میتوانید متدهای تغییردهندهای مانند push()
و pop()
را روی محتوای draft
اعمال کنید.
در پشت صحنه، Immer همیشه state بعدی را با توجه به تغییراتی که روی draft
اعمال کردهاید از ابتدا میسازد. این کار event handlerهای شما را بسیار مختصر نگه میدارد، بدون آنکه state را تغییر دهید.
Recap
- شما میتوانید آرایهها را درون state قرار دهید، اما نمیتوانید آنها را تغییر دهید.
- بهجای تغییر یک آرایه، یک نسخهٔ جدید از آن ایجاد کنید و state را به آن بهروزرسانی کنید.
- میتوانید از سینتکس spread آرایه
[...arr, newItem]
برای ایجاد آرایهای با آیتمهای جدید استفاده کنید. - میتوانید از
filter()
وmap()
برای ایجاد آرایههای جدید با آیتمهای فیلترشده یا تغییریافته استفاده کنید. - میتوانید از Immer برای مختصر نگهداشتن کد خود استفاده کنید.
Challenge 1 of 4: بهروزرسانی یک آیتم در سبد خرید
منطق handleIncreaseClick
را طوری تکمیل کنید که با فشردن ”+” عدد مربوطه افزایش یابد::
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> ); }