React интеграция

Интеграция CesiumJS в React требует учёта фундаментального различия между моделью декларативного UI и императивным рендерингом WebGL-сцены. React управляет виртуальным DOM и стремится пересоздавать интерфейс на основе состояния, тогда как CesiumJS поддерживает долгоживущую графическую сцену, где объекты изменяются напрямую через API Viewer, Scene, Entities и Primitive.

Наиболее распространённый подход заключается в создании контейнера DOM-элемента и инициализации экземпляра Cesium.Viewer внутри useEffect. Ключевая идея — изолировать Cesium от механизма повторного рендера React.

import { useEffect, useRef } from "react";
import * as Cesium from "cesium";
import "cesium/Build/Cesium/Widgets/widgets.css";

export default function CesiumMap() {
  const containerRef = useRef(null);
  const viewerRef = useRef(null);

  useEffect(() => {
    if (!containerRef.current) return;

    viewerRef.current = new Cesium.Viewer(containerRef.current, {
      animation: false,
      timeline: false,
      terrainProvider: Cesium.createWorldTerrain(),
    });

    return () => {
      if (viewerRef.current && !viewerRef.current.isDestroyed()) {
        viewerRef.current.destroy();
        viewerRef.current = null;
      }
    };
  }, []);

  return <div ref={containerRef} style={{ width: "100%", height: "100vh" }} />;
}

Ключевое правило архитектуры — экземпляр Viewer должен существовать независимо от жизненного цикла React-компонента, за исключением инициализации и уничтожения.

Управление жизненным циклом Viewer

Cesium Viewer является тяжёлым объектом, включающим WebGL-контекст, сцену, камеры, источники данных и слой UI. Повторная инициализация приводит к утечкам памяти и деградации производительности.

Стабильная схема управления включает:

  • создание Viewer только один раз
  • хранение ссылки через useRef
  • обязательный destroy() при размонтировании
  • избегание повторного создания при обновлении state

Дополнительно важно учитывать StrictMode в React 18, который может вызывать двойной монтаж в dev-режиме. Это требует защиты от повторной инициализации:

const initializedRef = useRef(false);

useEffect(() => {
  if (initializedRef.current) return;
  initializedRef.current = true;

  viewerRef.current = new Cesium.Viewer(containerRef.current);
}, []);

Разделение React-состояния и Cesium-сцены

Cesium не должен зависеть от частых обновлений React state. Любое изменение state, приводящее к перерисовке сцены, становится узким местом.

Правильный подход — использовать React только как слой управления конфигурацией, а Cesium как независимый runtime.

Пример: управление объектами через эффекты без пересоздания Viewer.

useEffect(() => {
  const viewer = viewerRef.current;
  if (!viewer) return;

  viewer.entities.add({
    id: "point-1",
    position: Cesium.Cartesian3.fromDegrees(71.4304, 51.1282),
    point: {
      pixelSize: 10,
      color: Cesium.Color.RED,
    },
  });
}, []);

Обновление данных выполняется через imperative API, а не через JSX.

Проблема повторного рендера и оптимизация

React может перерендеривать компонент десятки раз при изменении состояния UI. Cesium при этом не должен реагировать на каждый render.

Для защиты используются:

  • useRef для хранения Cesium объектов
  • мемоизация через useMemo для статических конфигураций
  • разделение компонентов: UI и карта

Пример разделения:

function CesiumContainer({ config }) {
  const containerRef = useRef();

  useEffect(() => {
    const viewer = new Cesium.Viewer(containerRef.current, config);

    return () => viewer.destroy();
  }, []);

  return <div ref={containerRef} />;
}

UI-компонент отдельно управляет config, но не пересоздаёт Viewer при каждом изменении.

Работа с Entities и реактивные обновления

Cesium Entities можно обновлять без пересоздания всей сцены. Это позволяет частично интегрировать реактивность.

useEffect(() => {
  const viewer = viewerRef.current;
  if (!viewer) return;

  const entity = viewer.entities.getById("point-1");

  if (entity) {
    entity.position = Cesium.Cartesian3.fromDegrees(lng, lat);
  }
}, [lng, lat]);

Этот подход ограничивает нагрузку на WebGL и избегает полной перерисовки сцены.

Использование CZML и потоковых данных

Для динамических сценариев используется CZML — формат описания временных данных Cesium.

В React интеграции CZML загружается один раз, а затем обновляется через DataSource.

useEffect(() => {
  const viewer = viewerRef.current;
  if (!viewer) return;

  const czmlDataSource = new Cesium.CzmlDataSource();

  viewer.dataSources.add(czmlDataSource);
  czmlDataSource.load("/data/flight.czml");

  return () => {
    viewer.dataSources.remove(czmlDataSource);
  };
}, []);

Это особенно эффективно при визуализации траекторий, симуляций и трекинга объектов.

Интеграция с React через сторонние обёртки

Существует экосистема обёрток над CesiumJS, где наиболее известной является Resium. Она предоставляет декларативные React-компоненты для Cesium.

Пример использования:

import { Viewer, Entity } from "resium";
import { Cartesian3 } from "cesium";

export default function Map() {
  return (
    <Viewer full>
      <Entity
        position={Cartesian3.fromDegrees(71.4304, 51.1282)}
        point={{ pixelSize: 10, color: Cesium.Color.YELLOW }}
      />
    </Viewer>
  );
}

Подход Resium удобен для декларативной разработки, но имеет ограничения:

  • меньше контроля над lifecycle Viewer
  • сложнее оптимизация при больших сценах
  • накладные абстракции поверх Cesium API

Поэтому в высоконагруженных проектах предпочтительнее прямой доступ к CesiumJS.

Управление камерой через React

Камера Cesium — отдельный объект сцены, который должен контролироваться через imperative API.

function flyToMoscow(viewer) {
  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(37.6173, 55.7558, 10000),
  });
}

В React это обычно оборачивается в useCallback или сервисный слой.

const flyTo = useCallback(() => {
  viewerRef.current?.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(lng, lat, height),
  });
}, [lng, lat, height]);

Работа с terrain и производительностью

Terrain является одним из самых ресурсоёмких компонентов Cesium. При React-интеграции важно избегать повторного создания terrain provider.

const terrainProvider = useMemo(() => {
  return Cesium.createWorldTerrain();
}, []);

Это предотвращает лишние сетевые запросы и пересоздание слоя высот.

Архитектура масштабируемого приложения

В крупных приложениях Cesium обычно отделяется в отдельный слой:

  • CesiumService (инициализация Viewer)
  • MapEngine (управление сценой)
  • React UI (панели, фильтры, формы)

Пример сервисного слоя:

class CesiumService {
  constructor(container) {
    this.viewer = new Cesium.Viewer(container);
  }

  addEntity(entity) {
    return this.viewer.entities.add(entity);
  }

  destroy() {
    this.viewer.destroy();
  }
}

React-компонент становится тонким адаптером:

useEffect(() => {
  const service = new CesiumService(containerRef.current);

  return () => service.destroy();
}, []);

Управление памятью и утечками

Cesium активно использует WebGL ресурсы, поэтому неправильное управление жизненным циклом приводит к утечкам:

  • не уничтоженные Viewer
  • не удалённые DataSource
  • накопленные Entities
  • повторно созданные terrain providers

Рекомендуется:

  • всегда вызывать destroy()
  • очищать viewer.entities.removeAll()
  • удалять DataSource через viewer.dataSources.remove()

Синхронизация UI и сцены

Для синхронизации состояния часто используется промежуточный слой событий:

useEffect(() => {
  const viewer = viewerRef.current;
  if (!viewer) return;

  const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);

  handler.setInputAction((movement) => {
    const picked = viewer.scene.pick(movement.position);
    if (picked) {
      console.log("Selected object", picked);
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

  return () => handler.destroy();
}, []);

Это позволяет отделить UI-логику React от интерактивной модели Cesium.

Работа с большими данными и тайлами

При интеграции с React важно учитывать потоковую природу Cesium 3D Tiles:

useEffect(() => {
  const viewer = viewerRef.current;
  if (!viewer) return;

  const tileset = new Cesium.Cesium3DTileset({
    url: "/tileset.json",
  });

  viewer.scene.primitives.add(tileset);

  return () => {
    viewer.scene.primitives.remove(tileset);
  };
}, []);

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

Разделение ответственности между слоями приложения

React выполняет задачи:

  • управление интерфейсом
  • управление фильтрами и настройками
  • маршрутизация и страницы

Cesium выполняет задачи:

  • рендер сцены
  • обработка геометрии
  • работа с 3D Tiles и terrain
  • управление камерой

Слабая связность между слоями обеспечивает предсказуемость поведения и стабильную производительность при масштабировании сцены до миллионов объектов.