Spec-Zone.ru › React 18

Обновление массивов в состоянии

Массивы изменяемы в JavaScript, но вы должны рассматривать их как неизменяемые, когда храните их в состоянии. Так же, как и с объектами, когда вы хотите обновить массив, хранящийся в состоянии, вам нужно создать новый (или скопировать существующий), а затем установить состояние, используя новый массив.

Вы узнаете

  • Как добавлять, удалять или изменять элементы в массиве в состоянии React
  • Как обновить объект внутри массива
  • Как сделать копирование массивов менее повторяющимся с помощью Immer

Обновление массивов без мутации

В JavaScript массивы — это всего лишь другой тип объектов. Как и с объектами, массивы в состоянии React следует рассматривать как неизменяемые. Это означает, что вы не должны переопределять элементы внутри массива, например arr[0] = 'bird', и вы также не должны использовать методы, которые изменяют массив, такие как push() и pop().

Вместо этого каждый раз, когда вы хотите обновить массив, вы должны передать новый массив в функцию установки состояния. Для этого вы можете создать новый массив из исходного массива в вашем состоянии, вызвав его не изменяющие методы, такие как filter() и map(). Затем вы можете установить ваше состояние на получившийся новый массив.

Вот справочная таблица общих операций с массивами. При работе с массивами внутри состояния React вам нужно будет избегать методов в левой колонке и отдавать предпочтение методам в правой колонке:

избегать (изменяет массив) предпочитать (возвращает новый массив)
добавление push, unshift concat, [...arr] синтаксис spread (пример)
удаление pop, shift, splice filter, slice (пример)
замена splice, arr[i] = ... присвоение map (пример)
сортировка reverse, sort сначала скопируйте массив (пример)

В качестве альтернативы вы можете использовать Immer, который позволяет вам использовать методы из обеих колонок.

Опасность

К сожалению, slice и splice имеют схожие названия, но являются совершенно разными:

  • slice позволяет вам копировать массив или его часть.
  • splice изменяет массив (для вставки или удаления элементов).

В React вы будете использовать slice (без p!) гораздо чаще, так как вам не нужно изменять объекты или массивы в состоянии. Обновление объектов объясняет, что такое мутация и почему она не рекомендуется для состояния.

Добавление в массив

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

Вместо этого создайте новый массив, который содержит существующие элементы и новый элемент в конце. Существует несколько способов сделать это, но самый простой — использовать синтаксис ... распространения массива:

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() по добавлению в начало массива. Попробуйте это в песочнице выше!

Удаление из массива

Самый простой способ удалить элемент из массива — фильтровать его. Другими словами, вы создадите новый массив, который не будет содержать этот элемент. Для этого используйте метод 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>
    </>
  );
}

Нажмите кнопку «Удалить» несколько раз и посмотрите на обработчик её клика.

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

Здесь, artists.filter(a => a.id !== artist.id) означает «создать массив, состоящий из тех artists, чьи идентификаторы отличаются от artist.id». Другими словами, кнопка «Удалить» каждого художника будет фильтровать этого художника из массива, а затем запрашивать повторную отрисовку с полученным массивом. Обратите внимание, что 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>
  );
}

Вставка в массив

Иногда вам может потребоваться вставить элемент в определённую позицию, которая не находится ни в начале, ни в конце. Для этого можно использовать синтаксис spread массива ... вместе с методом 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 и не изменяющими методами, такими как map() и filter() в одиночку. Например, вы можете захотеть перевернуть или отсортировать массив. JavaScript-методы 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] для создания копии исходного массива. Теперь, когда у вас есть копия, вы можете использовать изменяющие методы, такие как nextList.reverse() или nextList.sort(), или даже присвоить отдельные элементы с помощью nextList[0] = "something".

Однако даже если вы копируете массив, вы не можете изменять существующие элементы внутри его напрямую. Это связано с тем, что копирование является поверхностным — новый массив будет содержать те же элементы, что и исходный. Таким образом, если вы измените объект внутри скопированного массива, вы изменяете существующее состояние. Например, код такого типа является проблемой.

const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);

Хотя nextList и list — это два разных массива, nextList[0] и list[0] указывают на один и тот же объект. Поэтому, изменяя nextList[0].seen, вы также изменяете list[0].seen. Это изменение состояния, которого следует избегать! Вы можете решить эту проблему аналогично обновлению вложенных JavaScript-объектов — копируя отдельные элементы, которые вы хотите изменить, вместо их изменения. Вот как.

Обновление объектов внутри массивов

Объекты не находятся «внутри» массивов. Они могут казаться «внутри» в коде, но каждый объект в массиве — это отдельное значение, на которое массив «указывает». Именно поэтому необходимо быть осторожным при изменении вложенных полей, таких как list[0]. Список произведений другого человека может указывать на один и тот же элемент массива!

При обновлении вложенного состояния необходимо создавать копии с точки, где вы хотите обновить, и до самого верхнего уровня. Давайте посмотрим, как это работает.

В этом примере два отдельных списка произведений искусства имеют одинаковое начальное состояние. Они должны быть изолированы, но из-за мутации их состояние случайно разделено, и установка флажка в одном списке влияет на другой:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    const myNextList = [...myList];
    const artwork = myNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setMyList(myNextList);
  }

  function handleToggleYourList(artworkId, nextSeen) {
    const yourNextList = [...yourList];
    const artwork = yourNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setYourList(yourNextList);
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Проблема в коде такого типа:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);

Хотя массив myNextList сам по себе новый, сами элементы такие же, как и в исходном массиве myList. Поэтому изменение artwork.seen изменяет исходный элемент произведений искусства. Этот элемент произведений искусства также находится в yourList, что вызывает ошибку. Ошибки такого типа могут быть сложными для понимания, но к счастью, они исчезают, если вы избегаете изменения состояния.

Вы можете использовать map для замены старого элемента его обновлённой версией без мутации.

setMyList(myList.map(artwork => {
  if (artwork.id === artworkId) {
    // Create a *new* object with changes
    return { ...artwork, seen: nextSeen };
  } else {
    // No changes
    return artwork;
  }
}));

Здесь ... — это синтаксис spread объекта, используемый для создания копии объекта.

С этим подходом ни один из существующих элементов состояния не изменяется, и ошибка устраняется:

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:

{
  "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 , теперь допустимо:

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

Это потому, что вы не изменяете исходное состояние, а изменяете специальный draft объект, предоставляемый Immer. Аналогично, вы можете применять изменяющие методы, такие как push() и pop() к содержимому draft.

Внутри Immer всегда создает следующее состояние с нуля в соответствии с изменениями, которые вы внесли в draft. Это позволяет сохранить ваши обработчики событий очень лаконичными, никогда не изменяя состояние.

Резюме

  • Вы можете помещать массивы в состояние, но не можете их изменять.
  • Вместо изменения массива создайте новую его версию и обновите состояние до нее.
  • Вы можете использовать синтаксис разброса массивов [...arr, newItem] для создания массивов с новыми элементами.
  • Вы можете использовать filter() и map() для создания новых массивов с отфильтрованными или преобразованными элементами.
  • Вы можете использовать Immer для сохранения лаконичности вашего кода.

Попробуйте выполнить некоторые задания

Задание 1 из 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>
  );
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/updating-arrays-in-state

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API