Привет, меня зовут Дмитрий, я Middle-React-разработчик с замашками сеньора, поднимающийся с самых низов без мам, пап и ипотек. В последнее время я частенько вижу ситуацию: при использовании MobX в больших проектах у людей появляются сложности с количеством перерисовок или наоборот не обновлением данных со стора. Также могут проявляться проблемы с производительностью в том числе и из-за этого. Я решил поделиться отладочными инструментами MobX, ведь это может кому пригодиться.
Реактивное программирование и состояние в MobX
Немного справочной информации про концепцию реактивного программирования. Реактивное программирование — это концепция, где данные и действия синхронизируются автоматически. Если что-то меняется в одном месте, это изменение каскадно влияет на все связные элементы приложения. MobX помогает превратить объекты JS в структуры данных, которые отслеживают изменения и обновляют приложение в реальном времени.
Основными инструментами MobX являются:
Observable (наблюдаемые): свойства, которые реагируют на изменения данных.
Computed (вычисляемые): зависимости, которые пересчитываются только при необходимости.
Reactions (реакции): побочные эффекты, выполняемые в ответ на изменения в наблюдаемых данных.
Используя эти элементы, MobX создает реактивные цепочки. Изменения в observables автоматически вызывают пересчет зависимых computed и реакций, что позволяет минимизировать ручное управление состоянием.
Но когда зависимостей и переменных становится много, бывает тяжело понять, откуда что обновилось и что вызвало перерендер. Для решения такой проблемы существуют инструменты отладки, о которых я постараюсь рассказать.
Под капотом
Одной из ключевых особенностей MobX, начиная с версии 5, является использование прокси-объектов, которые позволяют эффективно перехватывать изменения в состоянии и управлять зависимостями между данными и реакциями.
MobX создает реактивные объекты через прокси, что позволяет «перехватывать» каждое обращение к свойствам этих объектов. Этот механизм дает возможность MobX отслеживать зависимости между данными в реальном времени, как только мы обращаемся к какому-либо свойству. Функции MobX:
Создание наблюдаемых свойств: MobX оборачивает объекты в прокси, отслеживая доступ ко всем их свойствам и регистрируя зависимости. Например, если в реакции используется свойство объекта, MobX автоматически добавляет это свойство в зависимости реакции.
Инвалидация при изменениях: Прокси позволяют MobX сразу же узнавать, когда какое-то наблюдаемое значение изменяется. Если в MobX-объекте обновляется свойство, то через прокси MobX «инвалидирует» все вычисляемые значения и реакции, которые зависят от этого свойства, и автоматически пересчитывает их.
Простота управления состоянием: Благодаря прокси, MobX не требует дополнительных обёрток для каждого свойства. Все новые свойства, которые добавляются к объектам, также сразу становятся реактивными, что делает MobX простым в использовании.
Использование прокси позволило MobX упростить и оптимизировать реактивность, так как только зависимости, затронутые изменениями, пересчитываются. Это увеличивает производительность и снижает потребление ресурсов, обеспечивая плавное обновление интерфейса при изменении данных.
Зачем нужны инструменты для отладки и мониторинга?
MobX требует контроля со стороны разработчика: необходимо убедиться, что реактивные связи и состояния обновляются только тогда, когда это действительно необходимо. Если зависимости не настроены корректно, это может привести к ненужным пересчетам и обновлениям, что может негативно сказаться на производительности. Кроме того, ошибки, связанные с реактивностью, не всегда очевидны и могут проявляться в неожиданном поведении.
С помощью таких инструментов, как trace, introspection и spy, разработчик получает возможность следить за всем, что происходит в реактивных недрах проекта, и использовать MobX максимально эффективно.
Отладка может быть сложной задачей, т. к. требуется не только понять, какие изменения происходят, но и выяснить, что именно инициирует их. Давайте начнем с инструмента trace().
Что такое trace и как он работает?
Trace() в MobX — это встроенный метод для отслеживания реактивных связей. Когда мы вызываем trace() внутри функции autorun или computed, MobX выводит детальную информацию о том, какие именно зависимости вызвали её пересчёт. Также trace() можно вызвать и просто в компоненте и если запустим приложение, то в момент обновления переменной получим точку дебага в браузере. Это полезно для ситуаций, когда вы пытаетесь понять, почему какое-то вычисляемое значение или реакция обновляется чаще, чем ожидается.
Trace() позволяет увидеть, какие observable свойства участвуют в вычислении. Это помогает идентифицировать потенциальные проблемы с лишними перерисовками. С помощью этой информации можно более эффективно управлять состоянием и устранять неочевидные ошибки в реактивной логике.
Примеры использования trace
Рассмотрим пример, где trace помогает разобраться в работе autorun и computed. Предположим, у нас есть класс Store с наблюдаемыми свойствами a и b, а также вычисляемое свойство sum.
import { autorun, trace, makeAutoObservable } from "mobx";
class Store0 {
a = 10;
b = 20;
constructor() {
makeAutoObservable(this);
}
// Сеттер для свойства a
setA(value) {
this.a = value;
}
// Вычисляемое свойство sum
get sum() {
trace(); // Включаем trace внутри computed
return this.a + this.b;
}
}
const store0 = new Store0();
// Создаем autorun для автоматического обновления при изменении sum
autorun(() => {
trace(); // Включаем trace внутри autorun
console.log("Сумма:", store0.sum);
});
export default store0;
А также я создал компонент для вывода и изменения переменных.
import React from "react";
import { observer } from "mobx-react-lite";
import store0 from "./Store0";
const Store0Component = observer(() => {
const handleAChange = (event) => {
const newA = parseInt(event.target.value, 10);
store0.setA(isNaN(newA) ? 0 : newA);
};
return (
<div>
<h2>Store0 Variables</h2>
<p>Value of a: {store0.a}</p>
<p>Value of b: {store0.b}</p>
<p>Sum (a + b): {store0.sum}</p>
<h3>Update Value of a</h3>
<label>
a:
<input type="number" value={store0.a} onChange={handleAChange} />
</label>
</div>
);
});
export default Store0Component;
В этом примере, когда значения a или b изменяются, computed sum- свойство пересчитывается, и autorun вызывается для вывода нового значения суммы. Благодаря trace() MobX будет выводить информацию о том, какие зависимости (в данном случае a и b) привели к пересчету.
Теперь представим, что мы изменяем значение a:
Вот что выведет в консоль.
Сообщения, которые выводит spy
и trace
, помогают понять последовательность изменений и реакций в Store0
. Давайте разберём каждое событие:
Spy event — action:
Spy event: {type: 'action', name: 'setA', object: Store0, arguments: Array(1), spyReportStart: true}
Это означает, что был вызван метод setA
(названный "action" в MobX). Он изменяет значение a
и запустит отслеживание, когда его вызвали. Сообщение spyReportStart: true
указывает на начало выполнения setA
.
Spy event — update (observable a):
Spy event: {type: 'update', observableKind: 'object', debugObjectName: 'Store0@5', object: Store0, oldValue: 10, newValue: 11}
После вызова setA
, MobX зафиксировал изменение в a
: старое значение (oldValue
) было 10
, а новое значение (newValue
) стало 11
. Это событие обновления наблюдаемого объекта a
.
MobX trace — computed sum:
[mobx.trace] 'Store0@5.sum' is invalidated due to a change in: 'Store0@5.a'
Здесь trace
показывает, что вычисляемое значение sum
стало "невалидным" из-за изменения a
. Это означает, что MobX
понимает, что sum
нужно пересчитать, поскольку оно зависит от a
.
Spy event — computed update (sum):
Spy event: {observableKind: 'computed', debugObjectName: 'Store0@5.sum', object: Store0, type: 'update', oldValue: 30, newValue: 31}
После пересчета sum
его значение изменилось с 30
на 31
, и это зафиксировано как обновление вычисляемого свойства.
MobX trace — autorun:
[mobx.trace] 'Autorun@6' is invalidated due to a change in: 'Store0@5.sum'
trace
показывает, что autorun
был запущен снова, поскольку sum
обновился. autorun
пересчитывается каждый раз, когда sum
(или любое наблюдаемое свойство внутри него) изменяется.
Spy event — reaction (autorun):
Spy event: {name: 'Autorun@6', type: 'reaction', spyReportStart: true}
MobX зафиксировал, что autorun
запускается как реакция на обновление sum
, и реактивный код выполняется снова.
Console output — Updated sum:
Сумма: 31
Это вывод из autorun
, который печатает новое значение sum
в консоль.
Когда использовать trace?
trace полезен в следующих случаях:
Отладка неожиданных обновлений и улучшение производительности. Когда какое-то computed свойство или autorun обновляется чаще, чем нужно, trace помогает понять, какая зависимость инициирует эти обновления.
Анализ сложных зависимостей. В больших приложениях с множеством взаимозависимых observable и computed свойств trace помогает визуализировать связи, что упрощает понимание для разработчика.
С помощью trace можно быстро выявлять и устранять лишние перерендеры и получать полное представление о том, как именно MobX управляет реактивными зависимостями в приложении.
Introspection в MobX
Есть еще один инструмент для анализа. Introspection дает разработчику возможность заглянуть внутрь реактивных объектов и получить информацию о структуре зависимостей, типах свойств и текущем состоянии каждого из них.
Что такое introspection и какие методы предоставляет MobX?
Introspection— это набор методов, которые позволяют анализировать и получать доступ к информации о реактивных объектах, таких как observable свойства и computed значения. Эти методы помогают выяснить, является ли объект наблюдаемым, получить список его зависимостей и определить, какие элементы участвуют в вычислениях. Возможность использовать introspection важна для более глубокого понимания структуры реактивных цепочек и диагностики проблем в больших приложениях.
Основные методы introspection в MobX:
isObservable — проверяет, является ли объект или его свойство наблюдаемым.
isComputedProp — определяет, является ли свойство вычисляемым (computed).
getDependencyTree — отображает структуру зависимостей для реактивного объекта, что позволяет понять, какие observable или computed свойства влияют на его значение.
getObserverTree — показывает, кто «подписан» на данный объект, т. е. какие реакции или вычисления зависят от него.
Эти методы дают более полное представление о том, как устроены и взаимодействуют друг с другом различные элементы реактивного состояния.
Примеры использования introspection в сложных структурах
В качестве примера я придумал более сложный стор, чем в предыдущем примере. У него есть вложенные объекты, несколько уровней наблюдаемых свойств и вычисляемых значений. Версия MobX в данном примере "mobx-react-lite": "^4.0.7". Обращайте на это внимание, потому что синтаксис со старыми версиями, где были декораторы отличается.
import { makeAutoObservable } from "mobx";
class Store {
user = {
name: "Alice",
age: 30,
settings: {
theme: "dark",
notifications: true,
},
};
activities = [
{ title: "Jogging", duration: 30 },
{ title: "Coding", duration: 120 },
];
constructor() {
makeAutoObservable(this);
}
get totalActivityDuration() {
return this.activities.reduce(
(sum, activity) => sum + activity.duration,
0
);
}
get userInfo() {
return `${this.user.name}, Age: ${this.user.age}`;
}
}
const store = new Store();
export default store;
Также я создал компонент MyComponent, в котором мы хотим проверить, что от чего зависит и наблюдается ли.
import React, { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { isObservable, isComputedProp, getDependencyTree } from "mobx";
import store from "./Store";
const MyComponent = observer(() => {
useEffect(() => {
// Проверка, является ли user наблюдаемым объектом
console.log("user is observable:", isObservable(store.user)); // true
console.log(
"user.settings is observable:",
isObservable(store.user.settings)
); // true
// Проверка, является ли totalActivityDuration вычисляемым
console.log(
"totalActivityDuration is computed:",
isComputedProp(store, "totalActivityDuration")
); // true
// Получение дерева зависимостей для userInfo
console.log(
"Dependency tree for userInfo:",
getDependencyTree(store, "userInfo")
);
// Получение дерева зависимостей для totalActivityDuration
console.log(
"Dependency tree for totalActivityDuration:",
getDependencyTree(store, "totalActivityDuration")
);
}, []);
return (
<div>
<h1>User Info: {store.userInfo}</h1>
<p>Total Activity Duration: {store.totalActivityDuration}</p>
</div>
);
});
export default MyComponent;
Тогда при запуске MyComponent в консоль выведется следующее:
По выводу мы сразу понимаем, что наблюдаемо, а также видим структуру. Думаю, комментарии излишни.
Spy в MobX
Иногда для полноценного понимания работы реактивного состояния требуется видеть все события, происходящие в MobX, в реальном времени. Именно для этого и служит инструмент spy. Это отладочный инструмент, который отслеживает все изменения в MobX и выводит их в консоль. Использование spy позволяет буквально заглянуть «под капот» реактивной системы и увидеть, какие действия происходят в каждый момент времени, будь то изменение observable, запуск computed функции или выполнение эффекта.
Что такое spy и как он работает?
spy — это метод, который позволяет подписаться на все события, происходящие в MobX. С помощью spy можно отслеживать любые изменения состояния и действий, таких как обновления наблюдаемых значений, пересчёты вычисляемых свойств и срабатывание реакций.
Каждое событие, отслеживаемое spy, включает тип события (например, «update» для изменения значения observable или «compute» для пересчета computed), а также дополнительную информацию, такую как старое и новое значения для observable свойств. Это позволяет видеть полную картину изменений в приложении и обнаруживать неожиданные действия или неэффективные пересчёты.
Примеры использования spy
Изменим наш предыдущий store, добавив геттеры и сеттеры, чтобы можно было менять значение из компонента.
import { makeAutoObservable } from "mobx";
class Store2 {
user = {
name: "Alice",
points: 100,
status: "active",
};
levelMultiplier = 2;
constructor() {
makeAutoObservable(this);
}
// Сеттер для user.points
setUserPoints(points) {
this.user.points = points;
}
// Сеттер для levelMultiplier
setLevelMultiplier(multiplier) {
this.levelMultiplier = multiplier;
}
// Вычисляемое свойство: базовый уровень
get baseLevel() {
return Math.floor(this.user.points / 100);
}
// Вычисляемое свойство: уровень с учетом множителя
get adjustedLevel() {
return this.baseLevel * this.levelMultiplier;
}
// Вычисляемое свойство: статус игрока в зависимости от уровня
get userStatus() {
return this.adjustedLevel > 5 ? "VIP" : this.user.status;
}
}
const store2 = new Store2();
export default store2;
Сделаем новый компонент для примера StoreComponent. Который будет отображать и изменять наши переменные в Store2. И подключим spy, чтобы увидеть изменения в реактивной цепочке, которая включает наблюдаемые и вычисляемые свойства.
import React from "react";
import { observer } from "mobx-react-lite";
import store2 from "./Store2";
import { spy } from "mobx";
const StoreComponent = observer(() => {
spy((event) => {
console.log('Spy event:', event);
});
const handlePointsChange = (event) => {
const points = parseInt(event.target.value, 10);
store2.setUserPoints(isNaN(points) ? 0 : points);
};
const handleMultiplierChange = (event) => {
const multiplier = parseInt(event.target.value, 10);
store2.setLevelMultiplier(isNaN(multiplier) ? 1 : multiplier);
};
return (
<div>
<h2>User Info</h2>
<p>Name: {store2.user.name}</p>
<p>Points: {store2.user.points}</p>
<p>Status: {store2.user.status}</p>
<h2>Levels</h2>
<p>Base Level: {store2.baseLevel}</p>
<p>Adjusted Level: {store2.adjustedLevel}</p>
<p>User Status: {store2.userStatus}</p>
<h2>Adjust Points and Level Multiplier</h2>
<div>
<label>
Points:
<input
type="number"
value={store2.user.points}
onChange={handlePointsChange}
/>
</label>
</div>
<div>
<label>
Level Multiplier:
<input
type="number"
value={store2.levelMultiplier}
onChange={handleMultiplierChange}
/>
</label>
</div>
</div>
);
});
export default StoreComponent;
Запускаем и прям из интерфейса давайте поменяем значение points или levelMultiplier и посмотрим, какие события будут зафиксированы при изменении points на 101 и levelMultiplier, например на 3;
В консоле мы увидим для store.user.points структуру логов вида:
Строк много и поначалу, кажется, что ничего не понятно. Спешу вас разубедить:) В целом, по этим логам можно будет понять, что было событие update для user.points, которое, изменило значение с 100 на 101. Это изменение инициирует пересчет baseLevel, так как baseLevel зависит от user.points. Затем adjustedLevel также пересчитывается, поскольку он зависит от baseLevel и levelMultiplier. Наконец, userStatus пересчитывается, поскольку его значение зависит от adjustedLevel.
И для store.levelMultiplier будет похожая структура, по которой будет видно изменение значения levelMultiplier с 2 на 3. Это изменение инициирует пересчет adjustedLevel, т.к. он зависит от levelMultiplier. После этого userStatus пересчитывается, так как его значение зависит от adjustedLevel.
Но будьте осторожны, в консоль может высыпаться столько, что все повиснет.
Когда использовать spy?
Инструмент spy оказывается особенно полезен в следующих ситуациях:
Поиск неожиданных изменений состояния.
Оптимизация реактивных цепочек.
Обнаружение побочных эффектов.
Включение spy может дать разработчику полное представление о том, что происходит внутри MobX, позволяя не только находить ошибки, но и выявлять и устранять возможные проблемы с производительностью.
Заключение
MobX предоставляет разработчикам инструменты для отладки и мониторинга, такие как trace
, introspection
и spy
. Каждый из этих инструментов выполняет свою роль и помогает понять, как работает реактивное состояние и какие зависимости его формируют. Так чем же они отличаются и в каких ситуациях их использовать?
Сравнение trace, introspection и spy
Trace: фокусируется на реактивных зависимостях. Этот инструмент помогает видеть, какие observable свойства вызывают пересчёт
computed
значений и реакций. Лучше всего подходит для анализа и оптимизации конкретных цепочек реактивных обновлений, когда нужно понять, что вызывает пересчёт и почему.Introspection: служит для анализа структуры реактивных объектов. С его помощью можно определить, является ли свойство observable или computed, а также получить дерево зависимостей или наблюдателей для определённых значений. Инструмент особенно полезен для детального анализа состояния и выявления зависимостей в крупных проектах.
Spy: глобальный инструмент для мониторинга всех событий в MobX. Отслеживает любые изменения в observable, computed пересчёты и реакции в реальном времени, выводя в консоль каждое событие. Полезен для получения полной картины состояния и поиска неожиданных изменений или избыточных пересчётов.
Примеры, в каких случаях какой инструмент предпочтительнее использовать
Если вы хотите понять, почему конкретный
computed
значение пересчитывается, используйтеtrace
. Он покажет, какие зависимости инициировали пересчёт и поможет устранить лишние вызовы.Если необходимо узнать структуру объекта и его зависимости, используйте
introspection
. Это поможет вам увидеть взаимосвязи внутри реактивного состояния и, возможно, оптимизировать его структуру.Если нужно отследить все изменения в приложении в реальном времени или найти источник неожиданных изменений, используйте
spy
. Он полезен для отладки сложных систем и помогает идентифицировать потенциальные узкие места или побочные эффекты.
Из опыта:
Переходя на новый проект, в период знакомства и погружения я, периодически, использую trace, если проект несильно замудреный и реже, скорее, прям очень редко, приходится юзать spy, так быстрее погружаешься в проект и понимаешь все зависимости и цепочки. Сейчас в моей разработке большой и сложный проект с кучей зависимостей в виде табличных данных, табличных фильтров, общих фильтров и сортировок. К сожалению, я не могу привести вам конкретный пример, потому что это коммерческая тайна. По своему опыту я не видел, чтобы разработчики пользовались этими инструментами. Но мне кажется, что иметь в арсенале своих скиллов эти инструменты определенно будет плюсом. Всем работающего кода, пока!