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

mohsen4 هفته قبل
ارسال شده در
react/docs/v19
فهرست صفحه
  1. به‌روزرسانی آرایه‌ها بدون تغییر مستقیم
    1. اضافه کردن به یک آرایه
    2. حذف از یک آرایه
    3. تغییر دادن یک آرایه
    4. جایگزینی آیتم‌ها در یک آرایه
    5. درج در یک آرایه
    6. ایجاد تغییرات دیگر در یک آرایه
  2. به‌روزرسانی اشیا داخل آرایه‌ها
    1. نوشتن منطق به‌روزرسانی مختصر با Immer
  3. جمع بندی
  4. چالش ها
    1. 1. به‌روزرسانی یک آیتم در سبد خرید
    2. 2. حذف یک آیتم از سبد خرید
    3. 3. رفع تغییرات مستقیم با استفاده از متدهای غیرقابل تغییر
    4. 4. رفع تغییرات مستقیم با استفاده از Immer
  5. راه حل چالش ها
    1. چالش اول
    2. چالش دوم
    3. چالش سوم
    4. چالش چهارم

آرایه‌ها در جاوااسکریپت قابل تغییر (mutable) هستند، اما هنگام ذخیره‌سازی آن‌ها در وضعیت (state) باید آن‌ها را به‌عنوان غیرقابل تغییر (immutable) در نظر بگیرید. درست مانند اشیا، وقتی می‌خواهید آرایه‌ای که در وضعیت ذخیره شده را به‌روزرسانی کنید، باید یک آرایه جدید ایجاد کنید (یا یک کپی از یک آرایه موجود بسازید) و سپس وضعیت را طوری تنظیم کنید که از آرایه جدید استفاده کند.

به‌روزرسانی آرایه‌ها بدون تغییر مستقیم

در جاوااسکریپت، آرایه‌ها نوع دیگری از اشیا هستند. مانند اشیا، شما باید آرایه‌های وضعیت را به صورت فقط خواندنی در نظر بگیرید. این بدان معناست که نباید آیتم‌های آرایه را مثل arr[0] = 'bird' مقدار دهی کنید و همچنین نباید از متدهایی استفاده کنید که آرایه را تغییر می‌دهند، مانند push() و pop().

به‌جای این کار، هر بار که می‌خواهید یک آرایه را به‌روزرسانی کنید، باید یک آرایه جدید به تابع تنظیم وضعیت ارسال کنید. برای انجام این کار، می‌توانید با فراخوانی متدهای غیرقابل تغییر خود مانند filter() و map() از آرایه اصلی یک آرایه جدید بسازید. سپس می‌توانید وضعیت را با آرایه جدید مقدار دهی کنید.

در اینجا یک جدول مرجع برای عملیات رایج آرایه‌ها آورده ایم. وقتی با آرایه‌های وضعیت در React کار می‌کنید، باید از متدهای ستون چپ پرهیز کنید و به‌جای آن متدهای ستون راست را ترجیح دهید:

پرهیز (تغییرات در آرایه)ترجیح (بازگشت آرایه جدید)
اضافه کردنpush, unshiftconcat و [...arr] سینتکس گسترش
حذفpop, shift, splicefilter و 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] احتیاط کنید. لیست آثار هنری یک شخص ممکن است به همان عنصر آرایه اشاره کند!

هنگام به‌روزرسانی وضعیت تو در تو، باید از نقطه‌ای که می‌خواهید به‌روزرسانی کنید و تا سطح بالا کپی ایجاد کنید. بیایید ببینیم این چگونه کار می‌کند.

در این مثال، دو لیست آثار هنری جداگانه دارای وضعیت اولیه مشابه هستند. آن‌ها باید جداسازی شده باشند، اما به دلیل یک تغییر، وضعیت آن‌ها به‌طور تصادفی به اشتراک گذاشته می‌شود و تیک زدن آیتم در یکی از لیست‌ها بر روی لیست دیگر تأثیر می‌گذارد:

نتیجه اجرای کد در 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) {
    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 برای ایجاد یک آرایه جدید استفاده کنید و سپس از سینتکس گسترش ... شی برای ایجاد کپی از شی تغییر یافته برای آرایه جدید استفاده کنید:

مشاهده راه حل در 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>
        </li>
      ))}
    </ul>
  );
}
    

چالش دوم

شما می‌توانید ابتدا از map برای تولید یک آرایه جدید استفاده کنید و سپس از filter برای حذف محصولات با count برابر با 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;
      }
    }))
  }

  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 یک آرایه جدید ایجاد کنید. اکنون لیست به درستی کار می‌کند:

مشاهده جواب در 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) {
    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 به شما می‌دهد را تغییر دهید. در اینجا، همه تغییرات روی پیش نویش انجام می‌شود و بنابراین کد کار می‌کند:

مشاهده راه حل در 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(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، می‌توانید سبک مورد نظر خود را برای هر مورد جداگانه انتخاب کنید.

رای
0
ارسال نظر
مرتب سازی:
اولین نفری باشید که نظر می دهید!