Cập nhật mảng trong State

Array (mảng) là các cấu trúc dữ liệu có thể biến đổi (mutable) trong JavaScript, tuy nhiên bạn nên xem chúng như là không thể biến đổi (immutable) khi lưu trữ chúng trong state. Giống như objects, khi bạn muốn cập nhật một array được lưu trữ trong state, bạn cần tạo mới (hoặc tạo một bản sao của array sẵn có), và sau đó cập nhật state để sử dụng array mới.

Bạn sẽ được học

  • Cách thêm, sửa và xoá items trong một array trong React state
  • Cách để update một object bên trong một array
  • Cách để tạo ra bản sao của array ít lặp lại code với Immer

Cập nhật arrays mà không thay đổi chúng

Trong Javascript, array chỉ là một loại khác của object. Giống như objects, Bạn nên xem array trong React state như là chỉ đọc (read-only). Điều này có nghĩa là bạn không nên gán lại items bên trong một array như là arr[0] = 'bird', và bạn cũng không nên sử dụng những methods làm thay đổi array, giống như push()pop().

Thay vì vậy, mỗi lần bạn muốn cập nhật một array, bạn sẽ muốn truyền một array mới tới hàm đặt state. Để làm điều này, bạn có thể tạo một array mới từ array ban đầu trong state của bạn bằng cách gọi những methods không làm thay đổi array như filter()map(). Sau đó bạn có thể cập nhật state của bạn với array mới đó.

Dưới đây là một bảng tham khảo tới những hoạt động xử lý array thông thường. Khi làm việc với array bên trong React state, bạn sẽ cần phải tránh những methods ở cột bên trái, và hãy sử dụng những methods ở cột bên phải:

Tránh (thay đổi array)Ưa chuộng (trả về array mới)
Thêmpush, unshiftconcat, [...arr] cú pháp spread (Ví dụ)
Xoápop, shift, splicefilter, slice (Ví dụ)
Thay thếsplice, arr[i] = ... Gán giá trịmap (Ví dụ)
Sắp xếpreverse, sortSao chép array trước (Ví dụ)

Bạn cũng có thể sử dụng Immer - thư viện cho phép bạn sử dụng những methods từ cả hai cột.

Chú Ý

Thật không may, slicesplice có tên khá giống nhau nhưng chúng hoàn toàn khác nhau:

  • slice cho phép bạn sao chép array hoặc một phần của nó.
  • splice thay đổi array (dùng để thêm hoặc xoá items).

Trong React, bạn có lẽ sẽ thường sử dụng slice (không phải splice!) rất nhiều bởi vì bạn không muốn thay đổi object hoặc array bên trong state. Cập nhật object giải thích mutation là gì và tại sao nó lại không được khuyến khích cho state.

Thêm item vào một array

push() sẽ thay đổi một array, điều mà bạn sẽ không mong muốn:

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

Thay vì vậy, hãy tạo một array mới chứa những items sẵn có một item mới ở cuối. Có khá nhiều cách để thực hiện điều này, nhưng cách dễ nhất là sử dụng cú pháp ... array spread:

setArtists( // Thay thế state
[ // với một array mới
...artists, // array đó chứa tất cả các items cũ
{ id: nextId++, name: name } // và một item mới ở phía cuối
]
);

Bây giờ thì nó hoạt động đúng:

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

Cú pháp array spread cũng cho phép bạn chèn một item vào array bằng cách đặt nó trước array ban đầu ...artists:

setArtists([
{ id: nextId++, name: name },
...artists // Đặt những item cũ ở đằng sau
]);

Theo cách này, spread có thể làm công việc của cả push() bằng cách thêm vào phía cuối của một array và unshift() bằng cách thêm vào phía đầu của một array. Hãy thử nó ở code sandbox phía trên!

Xoá item khỏi một array

Cách dễ nhất để xoá một item khỏi một array là sàng lọc nó ra. Nói cách khác, bạn sẽ tạo ra một array mới không chứa item đó. Để thực hiện điều này, hãy sử dụng phương thức filter, ví dụ:

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

Click vào “Delete” button một vài lần, và nhìn vào hàm xử lý click của nó.

setArtists(
artists.filter(a => a.id !== artist.id)
);

Ở đây, artists.filter(a => a.id !== artist.id) có nghĩa là “tạo một array chứa những artists mà IDs của họ khác với artist.id”. Nói cách khác, “Delete” button của mỗi artist sẽ lọc artist đó ra khỏi array, và sau đó yêu cầu một lần re-render với array vừa mới tạo. Chú ý rằng filter không thay đổi array ban đầu.

Biến đổi một array

Nếu bạn muốn thay đổi một vài hoặc tất cả các items bên trong array, bạn có thể sử dụng map() để tạo một array mới. Hàm callback bạn truyền vào map sẽ quyết định làm gì với từng item, dựa vào data hoặc index của nó (hoặc cả hai).

Trong ví dụ này, một array giữ toạ độ của hai hình tròn và một hình vuông. Khi bạn nhấn vào button, nó chỉ di chuyển những hình tròn xuống dưới khoảng cách 50px. Nó làm vậy bằng cách tạo một array mới với 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') {
        // Không thay đổi
        return shape;
      } else {
        // Trả về một hình tròn mới ở phía dưới hình tròn cũ 50px
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Re-render với array mới
    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,
        }} />
      ))}
    </>
  );
}

Thay thế items trong một array

Đây là một việc làm phổ biến khi bạn muốn thay thế một hoặc nhiều item trong array. Cách gán như arr[0] = 'bird' sẽ thay đổi array gốc, vì vậy bạn nên sử dụng map để thực hiện điều này.

Để thay thế một item, hãy tạo một array mới với map. Bên trong việc gọi hàm map của bạn, bạn sẽ nhận được index của item như là đối số thứ hai. Sử dụng nó để quyết định liệu bạn sẽ trả lại item gốc (đối số đầu tiên) hay một cái gì khác:

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) {
        // Tăng giá trị của counter khi được click vào
        return c + 1;
      } else {
        // Phần còn lại không thay đổi
        return c;
      }
    });
    setCounters(nextCounters);
  }

  return (
    <ul>
      {counters.map((counter, i) => (
        <li key={i}>
          {counter}
          <button onClick={() => {
            handleIncrementClick(i);
          }}>+1</button>
        </li>
      ))}
    </ul>
  );
}

Chèn một item vào trong một array

Đôi khi, bạn muốn chèn một item vào một vị trí cụ thể, không phải ở đầu array hay cuối array. Để thực hiện điều này, bạn có thể sử dụng cú pháp ... array spread kết hợp với phương thức slice(). Phương thức slice() cho phép bạn cắt một “miếng” trong array. Để chèn một item, bạn sẽ tạo ra một array bằng cách sử dụng phần cắt từ đầu đến trước vị trí chèn, sau đó tới item mới, rồi tiếp đến là phần còn lại của array gốc.

Trong ví dụ này, Insert button luôn chèn item mới ở index 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; // Có thể là bất kỳ index nào
    const nextArtists = [
      // Những items trước điểm chèn
      ...artists.slice(0, insertAt),
      // Item mới
      { id: nextId++, name: name },
      // Những items sau điểm chèn
      ...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>
    </>
  );
}

Thực hiện những thay đổi khác tới một array

Có một số thao tác bạn không thể thực hiện chỉ với cú pháp spread và các phương thức không làm thay đổi array như map()filter(). Ví dụ, bạn có thể muốn đảo ngược hoặc sắp xếp một array. Phương thức reverse()sort() trong JavaScript làm thay đổi array gốc, vì vậy bạn không thể sử dụng chúng trực tiếp.

Tuy nhiên, bạn có thể sao chép array trước tiên, sau đó thực hiện các thay đổi trên array sao chép đó. Ví dụ:

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

Ở đây, việc đầu tiên là bạn sử dụng cú pháp [...list] để tạo một bản sao của array ban đầu. Bây giờ bạn đã có một bản sao, bạn có thể sử dụng các phương thức làm thay đổi array như nextList.reverse() hoặc nextList.sort(), hoặc thậm chí gán các items riêng lẻ với nextList[0] = "something".

Tuy nhiên, ngay cả khi bạn sao chép một array, bạn không thể thay đổi trực tiếp các items hiện có bên trong nó. Điều này là do sao chép là nông - array mới sẽ chứa các items giống như array ban đầu. Vì vậy, nếu bạn sửa đổi một đối tượng bên trong array đã sao chép, bạn đang thay đổi state hiện tại. Ví dụ, mã như thế này gây ra vấn đề.

const nextList = [...list];
nextList[0].seen = true; // Vấn đề: thay đổi list[0]
setList(nextList);

Mặc dù nextListlist là hai array khác nhau, nextList[0]list[0] trỏ vào cùng một đối tượng. Do đó, bằng việc thay đổi nextList[0].seen, bạn cũng đang thay đổi list[0].seen. Điều này là một sự biến đổi state, mà bạn nên tránh! Bạn có thể giải quyết vấn đề này một cách tương tự như cập nhật một nested object trong JavaScript—bằng cách sao chép các items riêng lẻ mà bạn muốn thay đổi thay vì biến đổi chúng. Dưới đây là cách làm.

Cập nhật objects bên trong array

Các đối tượng thực sự không được đặt “bên trong” các array. Chúng có thể xuất hiện là “bên trong” trong code, nhưng mỗi đối tượng trong một array là một giá trị riêng biệt, mà array “trỏ” đến. Đây là lý do tại sao bạn cần phải cẩn thận khi thay đổi các nested fields như list[0]. Danh sách artwork của một người khác có thể trỏ vào cùng một phần tử của array!

Khi cập nhật nested state, bạn cần tạo bản sao từ điểm bạn muốn cập nhật, và cho đến top level. Hãy xem cách điều này hoạt động.

Trong ví dụ này, hai danh sách artwork riêng biệt có cùng state ban đầu. Chúng được xem như là tách biệt, nhưng do một sự biến đổi, state của chúng được chia sẻ một cách tình cờ, và việc check vào một ô trong một list ảnh hưởng đến list khác:

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

Đây là vấn đề trong đoạn code:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Vấn đề: biến đổi item hiện có
setMyList(myNextList);

Mặc dù myNextList là array mới, các items trong nó vẫn giống như trong array myList ban đầu. Vì vậy, việc thay đổi artwork.seen thay đổi item artwork gốc. Mục artwork đó cũng có trong yourList, điều này gây ra lỗi. Các lỗi như thế này có thể khó nghĩ tới, nhưng may mắn là chúng biến mất nếu bạn tránh biến đổi state.

Bạn có thể sử dụng map để thay thế một item cũ bằng phiên bản đã được cập nhật mà không làm biến đổi.

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Tạo ra một object mới với những thay đổi
return { ...artwork, seen: nextSeen };
} else {
// Không có sự thay đổi
return artwork;
}
}));

Ở đây, ... là cú pháp object spread được sử dụng để tạo ra bản sao của một object.

Với phương pháp này, không có state items hiện có nào bị biến đổi, và lỗi đã được sửa:

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) {
        // Tạo một object mới cùng với những thay đổi
        return { ...artwork, seen: nextSeen };
      } else {
        // không có sự thay đổi
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Tạo một object mới cùng với những thay đổi
        return { ...artwork, seen: nextSeen };
      } else {
        // không có sự thay đổi
        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>
  );
}

Nhìn chung, bạn chỉ nên biến đổi các object mà bạn vừa tạo mới. Nếu bạn đang chèn một artwork mới, bạn có thể biến đổi nó, nhưng nếu bạn đang xử lý cái gì đó đã có trong state, bạn cần tạo một bản sao.

Viết logic cập nhật gọn gàng với Immer

Cập nhật các array lồng nhau mà không làm thay đổi có thể trở nên hơi lặp đi lặp lại. Giống như với objects:

  • Nói chung, bạn không nên cần phải cập nhật state sâu hơn một hoặc hai cấp độ. Nếu các state object của bạn lồng ghép sâu, bạn có lẽ muốn tái cấu trúc chúng khác đi để chúng trở nên phẳng hơn.
  • Nếu bạn không muốn thay đổi cấu trúc state của mình, bạn có thể sử dụng Immer, thư viện cho phép bạn viết bằng cú pháp thuận tiện nhưng có biến đổi và chịu trách nhiệm về việc tạo bản sao cho bạn.

Dưới đây là ví dụ về Art Bucket List được viết lại bằng 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": {}
}

Lưu ý rằng với Immer, biến đổi như artwork.seen = nextSeen hiện đã được chấp nhận:

updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});

Điều này bởi vì bạn không biến đổi state gốc, mà bạn đang biến đổi một object draft đặc biệt được cung cấp bởi Immer. Tương tự, bạn cũng có thể áp dụng các phương thức biến đổi như push()pop() vào nội dung của draft.

Đằng sau hậu trường, Immer luôn xây dựng state tiếp theo từ ban đầu, dựa trên các thay đổi mà bạn đã thực hiện với draft. Điều này giữ cho các hàm xử lý sự kiện của bạn rất gọn gàng mà không bao giờ biến đổi state.

Tóm tắt

  • Bạn có thể đặt các array vào state, nhưng bạn không thể thay đổi chúng.
  • Thay vì biến đổi một array trực tiếp, hãy tạo một bản sao mới của nó và cập nhật state thành nó.
  • Bạn có thể sử dụng cú pháp array spread [...arr, newItem] để tạo arrays với các items mới.
  • Bạn có thể sử dụng filter()map() để tạo các arrays mới với các items đã được lọc hoặc biến đổi.
  • Bạn có thể sử dụng Immer để giữ code của bạn gọn gàng.

Challenge 1 of 4:
Cập nhật một item trong giỏ mua hàng

Hoàn thành handleIncreaseClick logic để khi nhấn ”+”, số tương ứng tăng lên:

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