У меня есть массив объектов в качестве переменной состояния. Я пытаюсь написать функцию, которая изменяет поле в одном из объектов.
interface IItem{
id: string,
selected: boolean
}
const [array, setArray] = useState<IItem[]>([])
const toggleItem = (id : string) => {
const item = array.find(i => i.id === id)
if (item) {
const copy = {...item, selected: !!item.selected}
setArray(array.map(i => i.id === id ? copy : i))
}
}
Код работает, но выглядит слишком сложным для этого. Есть ли способ упростить? (Обратите внимание, что порядок элементов в массиве имеет значение).
«это выглядит слишком сложным для этого» React полагается на дешевую семантику сравнения (т. е. равенство ссылок) для эффективного различия изменений в состоянии, в противном случае ему пришлось бы выполнять рекурсивную проверку для каждого объекта состояния (обратите внимание, что для этого потребуется много выделений и операций), что было бы слишком медленно. Но это требование неизменности плохо сочетается с семантикой Javascript. Ваши единственные реальные варианты — написать код так, как вы написали, использовать библиотеку неизменяемых структур данных или не использовать React.
Вы можете упростить функцию следующим образом: (same thing, just reduced lines!
)
const toggleItem = (id: string) => {
setArray(array.map(i => (i.id === id) ? {...i, selected: !i.selected} : i));
};
В основном это просто сжимает его. Единственное упрощение, которое вы сделали, помимо удаления пробелов или локальных значений, заключается в том, что теперь вы безоговорочно повторяете массив и устанавливаете состояние (которое запускает повторную отрисовку), которое может не иметь приемлемых характеристик производительности или соответствовать потребностям OP. Я не говорю, что это плохой ответ, но если вы собираетесь это сделать, то хотя бы оставьте описание изменений.
Если вы хотите упростить изменение состояния, я бы рекомендовал ознакомиться с пакетом Immer. С помощью Immer вы получаете записываемый черновик состояния, который можно изменять. Код может быть очень длинным, но если вы работаете с еще более глубоко вложенными объектами, это очень полезно. Я также думаю, что в вашем случае это очень помогает с читабельностью.
import { useState } from "react";
import { produce } from "immer";
interface IItem {
id: string;
selected: boolean;
}
const [array, setArray] = useState<IItem[]>([]);
const toggleItem = (id: string) => {
setArray(
produce((draft) => {
const item = draft.find((el) => el.id === id);
item.selected = !item.selected;
}),
);
};
РЕДАКТИРОВАТЬ
Если вы не хотите вводить пакет для локального состояния, я не думаю, что есть очевидный способ изменить ваш код, чтобы сделать его менее сложным. По крайней мере, вот альтернатива, которую, я бы сказал, немного легче читать.
interface IItem {
id: string;
selected: boolean;
}
const [array, setArray] = useState<IItem[]>([]);
const toggleItem = (id: string) => {
setArray((previous) =>
previous.map((item) => {
if (item.id === id) {
return {
...item,
selected: true,
};
}
return item;
}),
);
};
Спасибо — это действительно было бы хорошим решением, но я не хочу вводить immer для местного государственного управления.
Я понимаю, я добавил альтернативу, но это не сильно отличается от вашего текущего кода. Возможно, небольшое улучшение читабельности.
Скройте это в специальном хуке:
interface IItem {
id: string,
selected: boolean
}
function useSelect<T extends IItem>({ initialItems = [] }) {
const [items, setItems] = useState<T[]>(initialItems);
const toggleItem = (id: string) => {
const item = items.find(i => i.id === id);
if (item) {
const copy = { ...item, selected: !!item.selected }
setItems(items.map(i => i.id === id ? copy : i))
}
return { items, toggleItem };
}
}
и используйте его как:
function MyView() {
const { items, toggleItem } = useSelect({ initialItems: [...] });
return (
<>
{items.map(({ id, selected }) => (
<div
key = {id}
onClick = {() => toggleItem(id)}>
{id} {selected ? '(selected)' : ''}
</div>
))}
</>
)
}
Вы можете перейти на использование Immer, см. эту среднюю статью: medium.com/@agarwalsahil97/…