← Назад к вопросам

Как используешь MobX кроме Observer?

1.3 Junior🔥 111 комментариев
#State Management

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

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

MobX кроме Observer

MobX — это state management библиотека которая использует reactive programming. Observer — это декоратор для React компонентов, но MobX намного больше. Расскажу как я использую MobX инструменты и особенности.

Основные концепции MobX

MobX работает на трёх концепциях:

  1. Observable — состояние которое MobX отслеживает
  2. Computed — вычисляемые значения (автоматически кешируются)
  3. Reactions — побочные эффекты которые запускаются при изменении состояния

1. Observable: Создание реактивного состояния

import { observable, action, makeObservable } from 'mobx';

class UserStore {
  users: User[] = [];
  selectedUserId: string | null = null;
  isLoading = false;
  
  constructor() {
    makeObservable(this, {
      users: observable,
      selectedUserId: observable,
      isLoading: observable,
      fetchUsers: action,
      selectUser: action,
      addUser: action
    });
  }
  
  // Actions изменяют state
  fetchUsers = async () => {
    this.isLoading = true;
    try {
      const res = await fetch('/api/users');
      this.users = await res.json();
    } finally {
      this.isLoading = false;
    }
  };
  
  selectUser(userId: string) {
    this.selectedUserId = userId;
  }
  
  addUser(user: User) {
    this.users.push(user);
  }
}

Мож использовать декоратор синтаксис если включить experimentalDecorators:

class UserStore {
  @observable users: User[] = [];
  @observable isLoading = false;
  
  @action
  fetchUsers = async () => { /* ... */ };
}

2. Computed: Вычисляемые значения

Это как useMemo но лучше — автоматически кешируется и отслеживает зависимости:

class UserStore {
  @observable users: User[] = [];
  
  @computed
  get totalUsers(): number {
    // Пересчитывается только если users изменился
    return this.users.length;
  }
  
  @computed
  get activeUsers(): User[] {
    // Кешируется автоматически
    return this.users.filter(u => u.isActive);
  }
  
  @computed
  get usersByRole(): Map<Role, User[]> {
    const map = new Map<Role, User[]>();
    this.users.forEach(user => {
      if (!map.has(user.role)) {
        map.set(user.role, []);
      }
      map.get(user.role)!.push(user);
    });
    return map;
  }
  
  // С параметрами (создаёт мемоизированную функцию)
  @computed
  get usersByStatus() {
    return (status: string) => this.users.filter(u => u.status === status);
  }
}

Важное: computed очень эффективно если много компонентов подписаны на один store — пересчитываются только затронутые.

3. Reactions: Побочные эффекты

Это как useEffect но более мощно:

import { reaction, when, autorun } from 'mobx';

class UserStore {
  @observable selectedUserId: string | null = null;
  
  // autorun запускается при каждом изменении зависимостей
  constructor() {
    autorun(() => {
      if (this.selectedUserId) {
        console.log(`Selected user: ${this.selectedUserId}`);
      }
    });
  }
}

// reaction: более контролируемый autorun
const disposer = reaction(
  // Функция отслеживания (что смотреть)
  () => userStore.selectedUserId,
  
  // Effect (что делать когда изменится)
  (userId) => {
    if (userId) {
      loadUserDetails(userId);
    }
  },
  
  // Опции
  {
    delay: 300 // Debounce в 300ms
  }
);

// Очищаем подписку
// disposer();

// when: выполнить когда условие станет true
when(
  () => userStore.users.length > 0,
  () => {
    console.log('Users loaded!');
  }
);

4. runInAction: Для асинхронного кода

Если не можешь использовать @action декоратор, используй runInAction:

fetch('/api/users')
  .then(r => r.json())
  .then(data => {
    runInAction(() => {
      this.users = data;
      this.isLoading = false;
    });
  });

5. Observable Collections: Специальные типы

import { observable, ObservableMap, ObservableSet } from 'mobx';

class CartStore {
  // ObservableMap для ключ-значение
  items = observable.map<string, CartItem>([
    ['item1', { id: 'item1', qty: 2 }],
    ['item2', { id: 'item2', qty: 1 }]
  ]);
  
  @computed
  get totalPrice(): number {
    let total = 0;
    this.items.forEach(item => {
      total += item.qty * item.price;
    });
    return total;
  }
  
  addItem(item: CartItem) {
    this.items.set(item.id, item);
  }
  
  removeItem(itemId: string) {
    this.items.delete(itemId);
  }
  
  // ObservableSet для уникальных значений
  favoriteIds = observable.set<string>();
  
  toggleFavorite(itemId: string) {
    if (this.favoriteIds.has(itemId)) {
      this.favoriteIds.delete(itemId);
    } else {
      this.favoriteIds.add(itemId);
    }
  }
}

6. Использование в React (не только Observer)

import { createContext, useContext } from 'react';

const StoreContext = createContext<UserStore | null>(null);

export function useUserStore() {
  const store = useContext(StoreContext);
  if (!store) throw new Error('Store not provided');
  return store;
}

// Провайдер
export function StoreProvider({ children }) {
  const store = useRef(new UserStore()).current;
  return (
    <StoreContext.Provider value={store}>
      {children}
    </StoreContext.Provider>
  );
}

// Компонент с Observer
import { Observer } from 'mobx-react-lite';

function UserList() {
  const store = useUserStore();
  
  return (
    <Observer>
      {() => (
        <div>
          {store.isLoading ? (
            <p>Loading...</p>
          ) : (
            <ul>
              {store.users.map(user => (
                <li key={user.id}>{user.name}</li>
              ))}
            </ul>
          )}
        </div>
      )}
    </Observer>
  );
}

7. Интеграция с API и состояние загрузки

class UserStore {
  @observable users: User[] = [];
  @observable error: string | null = null;
  @observable state: 'idle' | 'loading' | 'success' | 'error' = 'idle';
  
  @action
  async fetchUsers(query?: string) {
    this.state = 'loading';
    this.error = null;
    
    try {
      const url = query ? `/api/users?q=${query}` : '/api/users';
      const res = await fetch(url);
      
      if (!res.ok) throw new Error('Failed to fetch');
      
      runInAction(() => {
        this.users = await res.json();
        this.state = 'success';
      });
    } catch (err) {
      runInAction(() => {
        this.error = err.message;
        this.state = 'error';
      });
    }
  }
  
  @computed
  get isLoading(): boolean {
    return this.state === 'loading';
  }
  
  @computed
  get isError(): boolean {
    return this.state === 'error';
  }
}

8. Middleware и DevTools

MobX имеет встроенный DevTools для отладки:

import { spy, trace } from 'mobx';

// Логирование всех изменений
spy(event => {
  if (event.type === 'action') {
    console.log(`Action: ${event.name}`);
  }
});

// Трассировка вычисленных значений
class Store {
  @computed
  get computed() {
    trace(); // Выведет стек что вызвало пересчёт
    return this.value * 2;
  }
}

9. Практический пример: Полноценный Store

class TodoStore {
  @observable todos: Todo[] = [];
  @observable filter: 'all' | 'active' | 'completed' = 'all';
  @observable isLoading = false;
  
  constructor() {
    makeObservable(this);
    this.fetchTodos();
    
    // Автосохранение при изменении
    reaction(
      () => this.todos.length,
      () => {
        this.saveTodos();
      },
      { delay: 500 }
    );
  }
  
  @action
  async fetchTodos() {
    this.isLoading = true;
    try {
      const res = await fetch('/api/todos');
      runInAction(() => {
        this.todos = await res.json();
      });
    } finally {
      runInAction(() => {
        this.isLoading = false;
      });
    }
  }
  
  @action
  addTodo(title: string) {
    this.todos.push({ id: Date.now(), title, done: false });
  }
  
  @action
  toggleTodo(id: number) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
  }
  
  @computed
  get filteredTodos(): Todo[] {
    switch (this.filter) {
      case 'active':
        return this.todos.filter(t => !t.done);
      case 'completed':
        return this.todos.filter(t => t.done);
      default:
        return this.todos;
    }
  }
  
  @computed
  get completedCount(): number {
    return this.todos.filter(t => t.done).length;
  }
  
  @action
  saveTodos() {
    fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(this.todos)
    });
  }
}

Когда использовать MobX

  • Сложное состояние с много взаимосвязей
  • Много computed значений
  • Нужны асинхронные reactions
  • Большие приложения с много stores
  • Когда Redux кажется слишком многословным

Когда NOT использовать

  • Маленькие приложения (useState хватит)
  • Если team не знает MobX (Redux более распространён)
  • Если нужен time-travel debugging (Redux лучше)

Чем MobX лучше Redux

  • Меньше boilerplate кода
  • Автоматическое отслеживание зависимостей (не нужно писать dependency arrays)
  • Более интуитивный API
  • Мутирующий стиль вместо pure функций (иногда проще)

MobX даёт больше свободы и требует меньше кода чем Redux, но требует понимания reactive programming.

Как используешь MobX кроме Observer? | PrepBro