Stores и reactivity

В экосистеме интерактивных карт Mapbox GL JS ключевую роль в архитектуре приложений играет разделение состояния (state) и реактивного обновления визуализации. Понятия stores и reactivity формируют слой абстракции между данными приложения и рендерингом карты, позволяя строить масштабируемые интерфейсы поверх WebGL-движка.

Store в контексте Mapbox GL JS не является встроенной сущностью библиотеки, но представляет собой архитектурный паттерн, применяемый для управления состоянием карты и связанных данных. Store выступает единым источником истины для:

  • координат центра карты
  • уровня масштабирования
  • параметров наклона и поворота
  • набора активных слоёв
  • фильтров и стилей
  • динамических источников данных

Типовая структура store может быть описана как объект состояния:

const mapStore = {
  center: [37.6173, 55.7558],
  zoom: 10,
  bearing: 0,
  pitch: 0,
  style: 'mapbox://styles/mapbox/streets-v12'
};

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

Реактивность как модель обновления карты

Реактивность в Mapbox GL JS выражается через событийную модель и императивные методы обновления состояния визуализации. Изменение данных store должно приводить к синхронному или асинхронному обновлению карты без ручного пересоздания компонентов.

Ключевые механизмы реактивности:

  • событийные обновления (move, zoom, rotate, pitch)
  • изменение источников данных (setData)
  • динамическое обновление стилей (setPaintProperty, setLayoutProperty)
  • полная замена стиля (setStyle)

Каркас реактивного связывания:

map.on('move', () => {
  mapStore.center = map.getCenter().toArray();
  mapStore.zoom = map.getZoom();
});

В обратную сторону изменения store должны отражаться на карте:

function updateMapFromStore(map, store) {
  map.setCenter(store.center);
  map.setZoom(store.zoom);
  map.setBearing(store.bearing);
  map.setPitch(store.pitch);
}

Sources как реактивные контейнеры данных

Одним из наиболее важных элементов реактивной архитектуры являются источники данных (sources). GeoJSON-источники в Mapbox GL JS представляют собой mutable-структуры, обновляемые без пересоздания слоя.

map.addSource('points', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: []
  }
});

Реактивное обновление данных осуществляется через setData, что позволяет реализовать store-подобное поведение:

const pointsStore = {
  data: { type: 'FeatureCollection', features: [] },
  setFeatures(features) {
    this.data.features = features;
    map.getSource('points').setData(this.data);
  }
};

Таким образом источник данных становится реактивным адаптером между состоянием приложения и рендерингом WebGL-слоя.

Реактивность слоёв и стилевой модели

Стилевая система Mapbox GL JS построена на декларативной модели слоёв, где изменения параметров могут быть реактивно применены без пересборки всего стиля.

Динамическое изменение свойств слоя

map.setPaintProperty('cities-layer', 'circle-radius', [
  'interpolate',
  ['linear'],
  ['get', 'population'],
  1000, 4,
  1000000, 12
]);

Store может инкапсулировать параметры визуализации:

const styleStore = {
  radius: 6,
  color: '#ff0000'
};

function applyStyle(map, store) {
  map.setPaintProperty('cities-layer', 'circle-radius', store.radius);
  map.setPaintProperty('cities-layer', 'circle-color', store.color);
}

При изменении значений store происходит точечное обновление слоя без пересоздания карты.

Подписки и событийная модель

Реактивность в Mapbox GL JS опирается на событийную систему WebGL-карты. Основные события, используемые для синхронизации store:

  • move — изменение центра, масштаба, вращения
  • render — цикл отрисовки кадра
  • idle — завершение загрузки и отрисовки
  • data — обновление источников

Пример синхронизации состояния:

map.on('move', syncStore);

function syncStore() {
  mapStore.center = map.getCenter().toArray();
  mapStore.zoom = map.getZoom();
  mapStore.bearing = map.getBearing();
  mapStore.pitch = map.getPitch();
}

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

Derived state и вычисляемые значения

Поверх базового store строятся производные состояния (derived state), которые вычисляются на основе первичных параметров.

Пример:

function getVisibleRadius(store) {
  return store.zoom > 12 ? 10 : 5;
}

Или более сложная модель:

  • плотность точек зависит от zoom
  • фильтрация данных зависит от bbox
  • визуальные параметры зависят от состояния слоя

Mapbox GL JS поддерживает выражения (expressions), которые частично заменяют необходимость внешних derived state:

['step',
  ['get', 'mag'],
  '#00ff00',
  5, '#ffff00',
  7, '#ff0000'
]

Двусторонняя реактивность (two-way binding)

При построении store-подхода часто требуется синхронизация в обе стороны:

  1. изменения store → обновляют карту
  2. изменения карты → обновляют store

Пример реализации:

let isSyncing = false;

map.on('move', () => {
  if (isSyncing) return;
  isSyncing = true;

  mapStore.center = map.getCenter().toArray();
  mapStore.zoom = map.getZoom();

  isSyncing = false;
});

function setCenter(center) {
  mapStore.center = center;
  map.setCenter(center);
}

Без защиты от рекурсии возникает зацикливание обновлений.

Интеграция с внешними реактивными системами

Store-подход часто реализуется через внешние библиотеки реактивности:

  • Redux-подобные хранилища
  • RxJS streams
  • MobX observables
  • Svelte stores

Пример RxJS-подхода:

const center$ = new BehaviorSubject([0, 0]);

center$.subscribe(center => {
  map.setCenter(center);
});

map.on('move', () => {
  center$.next(map.getCenter().toArray());
});

Такая модель превращает карту в реактивный поток данных.

Производительность и контроль обновлений

Реактивные обновления карты требуют аккуратного управления частотой изменений. Основные проблемы:

  • избыточные вызовы setData
  • частые изменения paint/layout свойств
  • синхронизация на каждом move событии

Оптимизационные стратегии:

  • батчинг изменений store
  • использование requestAnimationFrame
  • мемоизация вычисляемых значений
  • разделение частых и редких обновлений

Пример батчинга:

let pending = null;

function scheduleUpdate(state) {
  pending = state;

  requestAnimationFrame(() => {
    applyMapState(pending);
    pending = null;
  });
}

Реактивная модель источников данных как поток изменений

GeoJSON source в Mapbox GL JS может рассматриваться как потоковое состояние. При обновлении данных:

  • старый feature collection заменяется новым
  • слой автоматически пересчитывает геометрию
  • WebGL pipeline переиспользует буферы

Это делает источники основным кандидатом для store-абстракции:

class GeoJsonStore {
  constructor(map, sourceId) {
    this.map = map;
    this.sourceId = sourceId;
    this.state = { type: 'FeatureCollection', features: [] };
  }

  set(features) {
    this.state.features = features;
    this.map.getSource(this.sourceId).setData(this.state);
  }
}

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

Реактивность стиля как декларативный слой управления визуализацией

Стиль карты в Mapbox GL JS сам по себе является декларативной системой, которая частично заменяет необходимость внешних store. Однако в сложных приложениях стиль становится лишь отражением более высокого уровня состояния.

Пример соответствия store → style:

  • theme → набор цветов слоя
  • mode (day/night) → style URL
  • data state → filters
  • zoom level → visibility rules
function applyTheme(map, theme) {
  map.setStyle(theme === 'dark'
    ? 'mapbox://styles/mapbox/dark-v11'
    : 'mapbox://styles/mapbox/light-v11'
  );
}

Каждая смена темы становится реактивным событием верхнего уровня.

Композиция store-слоёв

В сложных приложениях store часто разбивается на независимые части:

  • mapStateStore
  • dataStore
  • uiStore
  • styleStore

Композиция позволяет локализовать реактивные обновления:

const appStore = {
  map: mapStore,
  data: dataStore,
  ui: uiStore
};

Каждый store отвечает за свою область реактивности, минимизируя каскадные обновления.

Итоговая модель реактивного контура

Реактивная система вокруг Mapbox GL JS формируется как цикл:

  • store хранит состояние
  • изменения store инициируют обновления карты
  • события карты обновляют store
  • источники данных синхронизируются через setData
  • стили реагируют на изменения параметров
  • derived state вычисляет производные значения для визуализации