به روز رسانی آرایه‌ها در 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, unshiftconcat, [...arr] spread syntax (مثال)
حذف کردنpop, shift, splicefilter, slice (example)
جایگزین کردنsplice, arr[i] = ... assignmentmap (example)
مرتب سازیreverse, sortابتدا از آرایه کپی بگیرید (example)

به‌عنوان جایگزین می‌توانید از Immer استفاده کنید که امکان استفاده از متدهای هر دو ستون را فراهم می‌کند.

Pitfall

متأسفانه، slice و splice، با وجود نام‌گذاری مشابه، تفاوت زیادی با یکدیگر دارند:

  • slice به شما امکان می‌دهد یک آرایه یا بخشی از آن را کپی کنید.
  • splice آرایه را تغییر می‌دهد (برای افزودن یا حذف آیتم‌ها).

در ری‌اکت، از slice (بدون حرف p!) بسیار بیشتر استفاده می‌کنید زیرا نمی‌خواهید object‌ها یا آرایه‌های موجود در state را تغییر دهید. به‌روزرسانی object‌ها توضیح می‌دهد که تغییر (mutation) چیست و چرا برای state توصیه نمی‌شود.

افزودن به آرایه

متد 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>
    </>
  );
}

در اینجا، ابتدا با استفاده از سینتکس spread [...list] یک کپی از آرایهٔ اصلی ایجاد می‌کنید. حالا که یک کپی دارید، می‌توانید از متدهای 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 نیز وجود دارد و همین باعث بروز باگ می‌شود. درک چنین باگ‌هایی می‌تواند دشوار باشد، اما اگر از تغییر state خودداری کنید، این باگ‌ها از بین می‌روند.

می‌توانید با استفاده از map، یک آیتم قدیمی را بدون تغییر state با نسخهٔ به‌روزرسانی‌شدهٔ آن جایگزین کنید.

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>
  );
}