Перейти к содержимому

60. MobX: Observables

Observable — это сердце MobX. Это специальные данные, за изменениями которых MobX следит автоматически. Как датчики в умном доме: поменялась температура — сразу сработала автоматика! 🏠


1. Три способа сделать данные наблюдаемыми 🛠️

Заголовок раздела «1. Три способа сделать данные наблюдаемыми 🛠️»

Самый простой и рекомендуемый способ. MobX сам разберётся что сделать observable, что computed, что action:

import { makeAutoObservable } from 'mobx';
class UserStore {
name = 'Яша'; // 👈 станет observable
age = 25; // 👈 станет observable
items: string[] = []; // 👈 станет observable array
constructor() {
makeAutoObservable(this); // 🔮 магия здесь
}
// Методы автоматически становятся actions
setName(name: string) {
this.name = name;
}
// Геттеры автоматически становятся computed
get greeting() {
return `Привет, ${this.name}! Тебе ${this.age} лет.`;
}
}

Когда нужен полный контроль — явно указываешь что и как:

import { makeObservable, observable, computed, action } from 'mobx';
class CartStore {
items: { name: string; price: number }[] = [];
discount = 0;
constructor() {
makeObservable(this, {
items: observable,
discount: observable,
total: computed, // явно указываем что это computed
addItem: action, // явно указываем что это action
setDiscount: action,
});
}
get total() {
const sum = this.items.reduce((acc, item) => acc + item.price, 0);
return sum * (1 - this.discount);
}
addItem(item: { name: string; price: number }) {
this.items.push(item);
}
setDiscount(discount: number) {
this.discount = discount;
}
}

observable() — Для объектов и массивов напрямую 📦

Заголовок раздела «observable() — Для объектов и массивов напрямую 📦»
import { observable } from 'mobx';
// Наблюдаемый объект
const user = observable({
name: 'Петя',
age: 30,
});
// Наблюдаемый массив
const todos = observable([
{ id: 1, text: 'Купить молоко', done: false },
{ id: 2, text: 'Погулять с собакой', done: true },
]);
// Наблюдаемое число
import { observable as obs } from 'mobx';
// Для примитивов используем observable.box
const count = observable.box(0);
console.log(count.get()); // 0
count.set(5);
console.log(count.get()); // 5

Если используешь декораторы (TypeScript + experimentalDecorators: true):

import { observable, computed, action } from 'mobx';
class TodoStore {
@observable todos: Todo[] = [];
@observable filter: 'all' | 'active' | 'done' = 'all';
@computed get filteredTodos() {
if (this.filter === 'active') return this.todos.filter(t => !t.done);
if (this.filter === 'done') return this.todos.filter(t => t.done);
return this.todos;
}
@action addTodo(text: string) {
this.todos.push({ id: Date.now(), text, done: false });
}
@action toggleTodo(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
}

MobX создаёт специальные массивы, которые отслеживают не только замену, но и мутации:

import { observable, autorun } from 'mobx';
const list = observable(['🍎', '🍊', '🍋']);
autorun(() => {
console.log('Список:', list.join(', '));
});
list.push('🍇'); // ✅ реакция сработает
list.splice(0, 1); // ✅ реакция сработает
list[0] = '🍓'; // ✅ реакция сработает (в MobX 6!)
list.sort(); // ✅ реакция сработает

import { observable, autorun } from 'mobx';
const profile = observable({
name: 'Яша',
contacts: {
phone: '+7 999 123 45 67',
},
tags: ['javascript', 'react'],
});
autorun(() => {
// MobX отслеживает ВСЕ вложенные поля!
console.log(`${profile.name} — ${profile.contacts.email}`);
});
profile.name = 'Яков'; // ✅ реакция сработает
profile.contacts.email = 'new@mail'; // ✅ вложенность тоже работает!
profile.tags.push('mobx'); // ✅ и массивы внутри тоже

Когда ключи объекта заранее неизвестны — используй observable.map():

import { observable, autorun } from 'mobx';
// Обычный объект: MobX не видит ДОБАВЛЕНИЕ новых ключей
const plain = observable({ a: 1 });
// Добавление нового ключа НЕ отслеживается в старом подходе!
// Observable Map: отслеживает добавление/удаление ключей
const map = observable.map<string, number>({
'Яша': 100,
'Петя': 85,
});
autorun(() => {
console.log('Scores:', map.size, [...map.entries()]);
});
map.set('Вася', 92); // ✅ добавление нового ключа
map.delete('Петя'); // ✅ удаление ключа
map.set('Яша', 105); // ✅ изменение значения
// Map удобен для кэшей, хранилищ по ID
const usersById = observable.map<number, User>();
usersById.set(1, { id: 1, name: 'Иван' });

6. Глубокая vs Поверхностная наблюдаемость 🔍

Заголовок раздела «6. Глубокая vs Поверхностная наблюдаемость 🔍»

По умолчанию MobX делает объекты глубоко наблюдаемыми (deep observable). Но иногда нужно отслеживать только изменение ссылки:

import { observable, makeObservable } from 'mobx';
class Store {
// Глубокая наблюдаемость (по умолчанию)
// MobX наблюдает за всеми вложенными объектами
deepData = { nested: { value: 1 } };
// Поверхностная — наблюдает только за самой переменной
// Изменение вложенных объектов не отслеживается
shallowRef = observable.ref({ nested: { value: 1 } });
// Наблюдает за элементами первого уровня (не за вложенностью)
shallowArray = observable.shallow([{ value: 1 }]);
constructor() {
makeObservable(this, {
deepData: observable, // глубокая
shallowRef: observable.ref, // только ссылка
shallowArray: observable.shallow, // первый уровень
});
}
}

СценарийРекомендация
Классы (ООП-стиль)makeAutoObservable(this)
Нужен ручной контрольmakeObservable(this, {...})
Простой объект/массивobservable({...})
Динамические ключиobservable.map()
Только ссылкаobservable.ref
Не нужна вложенностьobservable.shallow