Ссылочная семантика, поверхностное копирование, неизменяемые операции с массивами и безопасное обновление вложенных данных.
Разберём поведение JavaScript, из-за которого изменения состояния в React чаще всего дают неожиданный результат.
Возьмём список товаров и добавим выбор одного элемента. По ходу работы выясним, почему копии через spread иногда недостаточно, какие методы массива меняют исходные данные и зачем React нужны новые ссылки на изменившиеся объекты.
Весь код остаётся на TypeScript, но в этом уроке нас интересует именно поведение JavaScript во время выполнения.
Число или строка копируются как самостоятельные значения. С переменными, которые указывают на объект, всё иначе: две переменные могут вести к одному объекту.
const product = { id: 'p1', selected: false };
const sameProduct = product;
sameProduct.selected = true;
console.log(product.selected); // trueНового товара здесь не появилось. Мы изменили единственный объект через вторую ссылку на него.
React сравнивает старое и новое состояние по ссылкам. Если изменить старый объект на месте, предыдущий снимок состояния тоже окажется испорчен, а React не получит надёжного признака изменения.
Проверьте себя. Добавьте console.log(product === sameProduct). Результат true подтверждает, что обе переменные указывают на один объект.
У товара появилась вложенная настройка:
type Item = {
id: string;
meta: { selected: boolean };
};
const items: Item[] = [
{ id: 'p1', meta: { selected: false } },
];
const copy = [...items];
copy[0].meta.selected = true;Массив copy новый, но copy[0] и items[0] всё ещё указывают на один товар. Вложенный meta также остался общим. Поэтому items[0].meta.selected неожиданно стал true.
Правило простое: копируйте каждый уровень, который собираетесь изменить.
function select(items: readonly Item[], id: string): Item[] {
return items.map((item) =>
item.id === id
? { ...item, meta: { ...item.meta, selected: true } }
: item,
);
}Неизменившиеся товары можно оставить прежними. Новый объект нужен только выбранному товару и его изменившемуся meta.
Проверьте себя. Убедитесь тестом, что новый массив отличается от исходного, выбранный товар получил новую ссылку, а остальные товары сохранили прежние ссылки.
map, filter, slice и toSorted возвращают новый массив. push, splice, sort и reverse меняют существующий.
const visible = items.filter((item) => item.meta.selected);
const ordered = items.toSorted((a, b) => a.id.localeCompare(b.id));visible и ordered можно вычислить из исходных данных во время отрисовки. Не стоит записывать их в отдельное состояние: иначе придётся вручную синхронизировать несколько копий одной информации.
Если старый браузер не поддерживает toSorted, используйте [...items].sort(...): сначала создайте новый массив и только затем сортируйте его.
Частая ошибка. Вызвать items.sort(...) для массива из состояния. Порядок изменится и в старом снимке.
Код верхнего уровня модуля выполняется один раз при загрузке. Изменяемая переменная такого модуля становится общей для всех компонентов, которые его импортировали.
let selectedId: string | null = null;
export function selectProduct(id: string) {
selectedId = id;
}Это не локальное состояние компонента. Две открытые карточки будут менять одну переменную, а React не узнает о её изменении и не запустит новую отрисовку.
В модуле безопасно хранить неизменяемые настройки и чистые функции. Пользовательское состояние должно принадлежать компоненту, внешнему хранилищу с подпиской или серверу — выбор зависит от области его жизни.
Для функции select полезны четыре проверки:
selected: true;const source: Item[] = [
{ id: 'p1', meta: { selected: false } },
{ id: 'p2', meta: { selected: false } },
];
const result = select(source, 'p1');
expect(result).not.toBe(source);
expect(result[0]).not.toBe(source[0]);
expect(result[0].meta).not.toBe(source[0].meta);
expect(result[1]).toBe(source[1]);
expect(source[0].meta.selected).toBe(false);Здесь проверяется не способ написания функции, а важное для React поведение: старый снимок остаётся целым, изменённая ветвь получает новые ссылки, неизменённая не копируется.
Следующий урок применит эти правила к данным неизвестной формы, полученным из API.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: TypeScript: данные из API без any