Управление состоянием

Зачем нужно

Состояние (state) — это данные, от которых зависит интерфейс. В SPA состояние хранится на клиенте: введённый текст, список товаров, авторизован ли пользователь. Когда приложение растёт, управление состоянием становится главной архитектурной задачей: нужно понимать, где хранить данные, как их обновлять и как доставлять в нужные компоненты.

Где используется

  • Формы — значения полей, валидация, состояние отправки
  • Списки — данные с сервера, фильтрация, пагинация
  • Авторизация — токен, данные пользователя, права доступа
  • UI-состояние — открытые модалки, активные табы, тема оформления
  • Корзина — выбранные товары, количество, стоимость

Проблема состояния в SPA

      Компонент A (Header)
      ├─ показывает имя пользователя
      ├─ показывает количество товаров в корзине
      │
      Компонент B (Sidebar)
      ├─ показывает меню для авторизованных
      │
      Компонент C (ProductList)
      ├─ добавляет товар в корзину
      │   → должен обновить Header (счётчик)
      │   → должен обновить CartPage (список)
      │
      Компонент D (CartPage)
      ├─ показывает товары
      ├─ удаляет товар
          → должен обновить Header (счётчик)

Проблема: данные нужны в разных местах, и изменение в одном месте должно отразиться в других.

Паттерны управления состоянием

1. Локальное состояние (Component State)

Данные живут внутри одного компонента:

// Простейшее локальное состояние
class Counter {
  constructor(element) {
    this.element = element;
    this.state = { count: 0 };
    this.render;
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.render; // Перерисовка при изменении
  }

  increment {
    this.setState({ count: this.state.count + 1 });
  }

  render {
    this.element.innerHTML = `
      <span>${this.state.count}</span>
      <button id="inc">+</button>
    `;
    this.element.querySelector('#inc')
      .addEventListener('click', () => this.increment);
  }
}

new Counter(document.getElementById('app'));

Когда использовать: данные нужны только внутри одного компонента (открыто/закрыто модальное окно, значение поля ввода).

2. Props drilling (передача через пропсы)

Данные передаются от родителя к потомкам через параметры:

// Родительский компонент владеет состоянием
class App {
  constructor {
    this.state = { user: null, cart:  };
  }

  render {
    // Передаём состояние в дочерние компоненты
    const header = new Header({
      user: this.state.user,
      cartCount: this.state.cart.length,
    });

    const productList = new ProductList({
      onAddToCart: (product) => this.addToCart(product),
    });
  }

  addToCart(product) {
    this.state.cart = [...this.state.cart, product];
    this.render; // Перерисовка всего дерева
  }
}

Проблема prop drilling: если компоненту E нужны данные от A, они пробрасываются через B, C, D — которым эти данные не нужны.

3. Глобальное хранилище (Store)

Централизованное хранилище, доступное из любого компонента:

// === Паттерн Store (упрощённый Redux) ===

class Store {
  constructor(reducer, initialState) {
    this.state = initialState;
    this.reducer = reducer;
    this.listeners = ;
  }

  // Получить текущее состояние
  getState {
    return this.state;
  }

  // Отправить действие (action)
  dispatch(action) {
    // Reducer создаёт НОВОЕ состояние на основе действия
    this.state = this.reducer(this.state, action);
    // Уведомляем подписчиков
    this.listeners.forEach(fn => fn(this.state));
  }

  // Подписаться на изменения
  subscribe(listener) {
    this.listeners.push(listener);
    // Возвращаем функцию отписки
    return  => {
      this.listeners = this.listeners.filter(fn => fn !== listener);
    };
  }
}

// === Reducer — чистая функция ===
// (текущее состояние, действие) → новое состояние
function appReducer(state, action) {
  switch (action.type) {
    case 'ADD_TO_CART':
      return {
        ...state,
        cart: [...state.cart, action.payload],
      };

    case 'REMOVE_FROM_CART':
      return {
        ...state,
        cart: state.cart.filter(item => item.id !== action.payload),
      };

    case 'SET_USER':
      return {
        ...state,
        user: action.payload,
      };

    default:
      return state;
  }
}

// === Использование ===
const store = new Store(appReducer, {
  user: null,
  cart: ,
});

// Подписка — Header обновляется при изменении
store.subscribe((state) => {
  document.getElementById('cart-count').textContent = state.cart.length;
  document.getElementById('user-name').textContent =
    state.user?.name || 'Гость';
});

// Действия из любого места приложения
store.dispatch({
  type: 'ADD_TO_CART',
  payload: { id: 1, name: 'Ноутбук', price: 50000 },
});

store.dispatch({
  type: 'SET_USER',
  payload: { name: 'Антон', email: 'anton@mail.ru' },
});

4. Паттерн Observer (Event Bus)

Простейший механизм подписки/публикации:

class EventBus {
  constructor {
    this.events = {};
  }

  on(event, callback) {
    if (!this.events[event]) this.events[event] = ;
    this.events[event].push(callback);
  }

  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }

  emit(event, data) {
    if (!this.events[event]) return;
    this.events[event].forEach(cb => cb(data));
  }
}

// Глобальная шина событий
const bus = new EventBus();

// Header подписывается
bus.on('cart:updated', (cart) => {
  document.getElementById('cart-count').textContent = cart.length;
});

// ProductList публикует
function addToCart(product) {
  cart.push(product);
  bus.emit('cart:updated', cart);
}

Иммутабельность (Immutability)

Ключевой принцип управления состоянием — никогда не мутировать данные напрямую:

// ❌ ПЛОХО — мутация
const state = { user: 'Антон', cart: [1, 2, 3] };
state.cart.push(4); // Изменили оригинал

// ✅ ХОРОШО — создаём новый объект
const newState = {
  ...state,
  cart: [...state.cart, 4],
};

// === Иммутабельные операции ===

// Добавить в массив
const added = [...array, newItem];

// Удалить из массива
const removed = array.filter(item => item.id !== targetId);

// Обновить элемент массива
const updated = array.map(item =>
  item.id === targetId ? { ...item, name: 'Новое имя' } : item
);

// Обновить вложенный объект
const newState = {
  ...state,
  user: {
    ...state.user,
    settings: {
      ...state.user.settings,
      theme: 'dark',
    },
  },
};

Зачем иммутабельность:

  • Простое сравнение: oldState !== newState — значит были изменения
  • Предсказуемость: функция-reducer возвращает новый объект, не трогая старый
  • Отладка: можно хранить историю всех состояний (time-travel debugging)
  • Безопасность: защита от случайных мутаций в других частях кода

Сравнение подходов

Подход Сложность Масштаб Отладка Когда
Локальное состояние Низкая Один компонент Простая Формы, UI-переключатели
Prop drilling Низкая 2-3 уровня Средняя Маленькие приложения
Event Bus Средняя Любой Сложная Слабосвязанные компоненты
Store (Redux-like) Высокая Любой Лучшая Большие приложения

Частые ошибки

  1. Мутация состоянияstate.items.push(item) вместо [...state.items, item] — компоненты не обновятся
  2. Всё в глобальный store — локальное UI-состояние (открыта модалка?) не нужно хранить глобально
  3. Нет нормализации — вложенные данные дублируются, обновлять больно
  4. Подписка без отписки — при удалении компонента слушатель остаётся → утечка памяти
  5. Асинхронность в reducer — reducer должен быть чистой функцией, async-логика выносится наружу
  6. Слишком глубокая вложенностьstate.a.b.c.d.e = x — лучше нормализовать

Практика

  1. Реализовать класс Store с методами getState, dispatch, subscribe
  2. Написать reducer для TODO-приложения (добавить, удалить, переключить статус)
  3. Подключить 3 независимых компонента к одному store
  4. Добавить middleware для логирования каждого action
  5. Реализовать undo/redo через хранение истории состояний

Связанные темы

  • Что такое SPA — почему состояние — проблема именно в SPA
  • Компонентный подход — компоненты как потребители состояния
  • Virtual DOM — как изменение состояния вызывает перерисовку
  • MVC — MVC как подход к организации данных

Ресурсы