Прямая мутация данных — это изменение объекта или массива по существующей ссылке, без создания новой копии. Это одна из трудноуловимых ошибок в экосистеме React, которая может нарушить работу механизмов сравнения и обновления интерфейса. Проблема возникает, когда такие мутации есть в коде и нарушают принцип неизменяемости (immutability): данные не должны меняться напрямую, а любое изменение должно приводить к созданию нового объекта.

В этой статье мы разберем случаи из практики Modus BI, покажем наш подход к решению и правила, которые помогают контролировать появление мутаций в новом коде и устранять их в легаси. Этот гайд поможет вам системно подойти к вопросу и значительно сократить количество сложноотлавливаемых багов.

Архитектура и технологическая основа проекта

Фронтенд Modus BI построен на React и Redux. За десять лет проект вырос до большой экосистемы с десятками визуальных компонентов, форм, конструкторов и пользовательских сценариев.

Для визуализаций мы используем D3.js, amCharts, ECharts, Leaflet.

Для обработки данных — Lodash.

Для сборки — Webpack.

В кодовой базе есть разные поколения решений: старые модули на классовых компонентах и классическом Redux, новые — на Redux Toolkit, TypeScript и функциональных компонентах.

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

Проблема: мутация данных

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

В ранних React-проектах такие практики встречались достаточно часто. По мере роста системы и увеличения количества зависимостей это начало приводить к ряду проблем:

  • непредсказуемое поведение компонентов — ререндер происходит не всегда, когда ожидается;

  • сложность отладки — трудно отследить, где именно изменились данные;

  • рост технического долга — по мере роста системы исправление ошибок занимает все больше времени.

Примеры из практики Modus 

Пример №1. Мутация аргумента функции

Было:

function copyFieldsRef(dataset) {

  const fieldsRef = (dataset  {}).fields_ref  [];

  const fields = (dataset  {}).fields  {};

  _.forEach(fieldsRef, (hierarchy) => {

    _.forEach(hierarchy.ref, (rule, hierarchyIndex) => {

      _.forEach(fields, (field) => {

        if (field.name === rule.field_name) {

          field.hierarchyName = hierarchy.name;

          field.hierarchyIndex = hierarchyIndex + 2;

        }

        if (hierarchyIndex === 0 && field.name === rule.parentFieldName) {

          field.hierarchyName = hierarchy.name;

          field.hierarchyIndex = hierarchyIndex + 1;

        }

      });

    });

  });

  return dataset;

}

Функция напрямую изменяет dataset.fields по существующей ссылке. Это означает, что изменяются свойства внутри объекта dataset, и там, где используется эта же ссылка, данные оказываются измененными. React при этом может не обнаружить изменения, так как ссылка на объект осталась прежней.

Стало:

function copyFieldsRef(datasetData) {

  const dataset = structuredClone(datasetData);

  const fieldsRef = dataset?.fields_ref || [];

  const fields = dataset?.fields || {};

  _.forEach(fieldsRef, (hierarchy) => {

    _.forEach(hierarchy.ref, (rule, hierarchyIndex) => {

      _.forEach(fields, (field) => {

        if (field.name === rule.field_name) {

          field.hierarchyName = hierarchy.name;

          field.hierarchyIndex = hierarchyIndex + 2;

        }

        if (hierarchyIndex === 0 && field.name === rule.parentFieldName) {

          field.hierarchyName = hierarchy.name;

          field.hierarchyIndex = hierarchyIndex + 1;

        }

      });

    });

  });

  return dataset;

}

Теперь функция возвращает новую копию объекта. Это делает ее поведение предсказуемым.

Пример №2. Мутация состояния компонента

Было:

changeDatasetField(fieldName, optionName, val) {

  const { dataset } = this.state;

  if (dataset && _.isArray(dataset.fields)) {

    .set(.find(dataset.fields, ['name', fieldName]), optionName, val);

  }

  this.setState({ dataset });

}

Здесь dataset изменяется напрямую перед вызовом setState, поэтому React может проигнорировать такое обновление.

Стало:

changeDatasetField(fieldName, optionName, val) {

  const { dataset } = this.state;

  const datasetCopy = structuredClone(dataset);

  if (datasetCopy?.fields) {

    const updatedFields = datasetCopy.fields.map((field) =>

      field.name === fieldName ? { ...field, [optionName]: val } : { ...field }

    );

    this.setState({ dataset: { ...datasetCopy, fields: updatedFields } });

  }

}

Теперь состояние обновляется иммутабельно — создается новая структура данных и React корректно отрабатывает ререндер.  

Наш подход к решению

Чтобы системно решить проблему, мы в Modus BI приняли несколько правил:

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

2. Возврат новых структур. Все функции, за исключением тех случаев, где мутация данных необходима или её невозможно избежать, должны возвращать новые объекты или массивы.

3. Используем structuredClone для глубокого копирования, когда это действительно нужно.

4. Явные названия. Если функция по архитектурным причинам все же должна мутировать данные, добавляем суффикс Mutation, например, datasetFieldsMutation.

Что мы получили:

  • повысилась стабильность и предсказуемость интерфейса;

  • сократилось время на отслеживание и исправление неочевидных ошибок;

  • упростился процесс анализа кода и код-ревью.

По сути, неизменяемость данных — это не «просто хорошая практика», а базис предсказуемой архитектуры интерфейса.

Наши планы

Дальше мы продолжаем последовательную эволюцию фронтенда:

  • полный переход на функциональные компоненты;

  • завершение миграции на Redux Toolkit;

  • внедрение TypeScript во все модули;

  • повышение покрытия тестами;

  • обновление зависимостей до актуальных версий и контроль совместимости.

Работа с легаси‑кодом — это марафон. Мы не переписываем все сразу, но каждый день улучшаем систему: убираем мутации, упрощаем функции, добавляем типизацию и обновляем архитектуру. Последовательные шаги делают проект устойчивым, код — поддерживаемым, а платформу Modus BI — готовой к развитию. Если вы сталкиваетесь в своей кодовой базе с «багами из ниоткуда», начните с поиска и исключения прямых мутаций данных. Это даст быстрый эффект и сократит число проблем, которые трудно объяснить и долго чинить.

Комментарии (1)


  1. adminNiochen
    17.11.2025 18:29

    В планах полный переход на функциональные компоненты? Уже почти 26 год, а вы до сих пор классовыми компонентами пользуетесь? Ну тут даже сказать нечего...