Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. JavaScript: ссылки, коллекции и состояние React
js_state_foundations

JavaScript: ссылки, коллекции и состояние React

Ссылочная семантика, поверхностное копирование, неизменяемые операции с массивами и безопасное обновление вложенных данных.

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

JavaScript: ссылки, коллекции и состояние React

Разберём поведение JavaScript, из-за которого изменения состояния в React чаще всего дают неожиданный результат.

#Что сделаем

Возьмём список товаров и добавим выбор одного элемента. По ходу работы выясним, почему копии через spread иногда недостаточно, какие методы массива меняют исходные данные, как замыкание запоминает переменные и зачем React нужны новые ссылки на изменившиеся объекты.

Весь код остаётся на TypeScript, но в этом уроке нас интересует именно поведение JavaScript во время выполнения.

#1. Объект хранится по ссылке

Число или строка копируются как самостоятельные значения. С переменными, которые указывают на объект, всё иначе: две переменные могут вести к одному объекту.

const product = { id: 'p1', selected: false }; const sameProduct = product; sameProduct.selected = true; console.log(product.selected); // true

Нового товара здесь не появилось. Мы изменили единственный объект через вторую ссылку на него.

React сравнивает старое и новое состояние по ссылкам. Если изменить старый объект на месте, предыдущий снимок состояния тоже окажется испорчен, а React не получит надёжного признака изменения.

Проверьте себя. Добавьте console.log(product === sameProduct). Результат true подтверждает, что обе переменные указывают на один объект.

Частая ошибка. Считать const защитой от изменений: он запрещает переприсваивание переменной, но не изменение полей объекта.

#2. Spread делает только поверхностную копию

У товара появилась вложенная настройка:

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.

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

Частая ошибка. Скопировать только внешний объект и изменить вложенное поле — старый снимок изменится вместе с новым.

#3. Методы массива: какие меняют исходник

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(...): сначала создайте новый массив и только затем сортируйте его.

Таблица помогает не запоминать наугад:

Меняют исходный массивВозвращают новый
push, pop, shift, unshiftconcat, slice, map, filter
splice, sort, reverse, filltoSpliced, toSorted, toReversed, with

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

Частая ошибка. Вызвать items.sort(...) для массива из состояния. Порядок изменится и в старом снимке.

#4. Замыкание запоминает переменную, а не значение

Функция, созданная внутри другой функции, продолжает видеть её переменные. Это называется замыканием, и именно оно объясняет «устаревшие» значения в асинхронном коде React.

function createLogger(prefix: string) { let count = 0; return function log(message: string) { count += 1; // видит переменную, а не её копию console.log(`${prefix} #${count}: ${message}`); }; }

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

let selectedId = 'p1'; const show = () => console.log(selectedId); // прочитает значение в момент вызова selectedId = 'p2'; show(); // p2 — не 'p1'

В React каждый вызов компонента создаёт новые локальные переменные для этого вызова. Обработчик, созданный в одном вызове, навсегда видит переменные именно того вызова, — отсюда правило «состояние в обработчике является снимком».

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

Частая ошибка. Ожидать, что функция прочитает значение переменной на момент своего создания, а не вызова.

#5. Равенство: ===, Object.is и поверхностное сравнение

Примитивы сравниваются по значению, объекты — по ссылке. React использует Object.is, который почти совпадает с ===, но иначе обрабатывает два особых случая.

{ id: 'p1' } === { id: 'p1' }; // false: два разных объекта Object.is(NaN, NaN); // true, а NaN === NaN даёт false Object.is(0, -0); // false, а 0 === -0 даёт true

Поэтому «одинаковые по содержимому» объекты для React разные, и наоборот: изменённый на месте объект остаётся тем же самым.

const next = state; next.items.push(item); // содержимое изменилось Object.is(state, next); // true — React не увидит изменения

Отсюда следует практическое правило: чтобы React заметил изменение, нужна новая ссылка на изменившийся объект. Именно это и делает копирование пути из раздела 2.

Проверьте себя. Сравните два объекта с одинаковыми полями через === и убедитесь, что результат false.

Частая ошибка. Изменить массив в состоянии методом push и ждать перерисовки.

#6. Отсутствующие значения: ?., ?? и undefined

Данные из API часто содержат null или не содержат поля вовсе. Опциональная цепочка и оператор ?? позволяют обработать это без длинных проверок.

const city = order.customer?.address?.city ?? 'не указан';

Важно различать ?? и ||: первый подставляет значение только для null и undefined, второй — для любого ложного значения.

const quantity = input.quantity ?? 1; // 0 останется нулём const wrong = input.quantity || 1; // 0 превратится в 1

Для количества, цены и координат разница принципиальна: || молча превращает корректный нуль в значение по умолчанию. Отдельно помните, что ?. не защищает от неверного типа — только от отсутствия свойства.

Проверьте себя. Передайте quantity: 0 и сравните результат ?? и ||.

Частая ошибка. Использовать || для значений по умолчанию у чисел и строк.

#7. Переменная модуля общая для всех потребителей

Код верхнего уровня модуля выполняется один раз при загрузке. Изменяемая переменная такого модуля становится общей для всех компонентов, которые его импортировали.

let selectedId: string | null = null; export function selectProduct(id: string) { selectedId = id; }

Это не локальное состояние компонента. Две открытые карточки будут менять одну переменную, а React не узнает о её изменении и не запустит новую отрисовку.

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

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

Частая ошибка. Хранить выбранный элемент или черновик формы в переменной модуля «чтобы не пробрасывать свойства».

#8. Проверка результата

Для функции select полезны четыре проверки:

  1. выбранный товар получил selected: true;
  2. исходный массив не изменился;
  3. вложенный объект выбранного товара скопирован;
  4. остальные товары не копируются без причины.
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 поведение: старый снимок остаётся целым, изменённая ветвь получает новые ссылки, неизменённая не копируется.

Проверьте себя. Добавьте пятую проверку: повторный вызов select с тем же идентификатором возвращает новый массив, но не меняет исходный.

Частая ошибка. Проверять только итоговое значение поля, не убедившись, что исходные данные остались целыми.

#Что запомнить

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

Следующий урок применит эти правила к данным неизвестной формы, полученным из API.

#Источники для сверки

  • MDN: работа с объектами
  • MDN: методы Array
  • MDN: замыкания
  • React: обновление объектов в состоянии
  • React: обновление массивов в состоянии

Устранение неисправностей

Далее: TypeScript: данные из API без any