How GeoLibre's Store-Driven Architecture Synchronizes with MapLibre Using Zustand

GeoLibre maintains a single Zustand store as the application's source of truth, where the MapController subscribes to state changes and executes a reconciliation algorithm via syncLayers() to keep the MapLibre GL viewport perfectly in sync with the store.

GeoLibre implements a store-driven architecture that centralizes all application state in a Zustand store defined in @geolibre/core. This design pattern ensures that UI components never manipulate the map directly; instead, they update the store, which triggers a deterministic synchronization process that updates MapLibre GL layers, sources, and styling.

The Single Source of Truth Pattern

Centralized State Management

All application state—including layers, project settings, and selection—lives in packages/core/src/store.ts. The store is exported as useStore and provides slices like layers that components subscribe to using Zustand hooks.

Immutable State Updates

When users interact with the UI—such as adding a GeoJSON file or toggling layer visibility—components call useStore.setState() or helper functions like setLayers. These updates trigger subscriptions across the application, including the critical subscription held by the map controller.

The Synchronization Loop

Store Subscription in MapCanvas

The MapCanvas component (packages/map/src/MapCanvas.tsx) subscribes to the layers slice of the Zustand store. When the layers array changes, it invokes controller.syncLayers(layers) to initiate reconciliation.

The Reconciliation Algorithm

The MapController.syncLayers() method (packages/map/src/map-controller.ts) executes a three-step reconciliation process:

  1. Creates new MapLibre sources and style layers for any added GeoLibreLayer objects.
  2. Updates existing MapLibre layers when properties like visibility, opacity, or styling change.
  3. Removes MapLibre resources that correspond to layers deleted from the store.

This logic delegates to helper functions in packages/map/src/layer-sync.ts that translate GeoLibre layer definitions into MapLibre-compatible sources and style layers.

Practical Implementation Examples

Adding a GeoJSON Layer

When users import data through a file dialog, the application calls core helpers that update the store:

import { useStore, addGeoJsonLayer } from '@geolibre/core';

const handleFile = async (geojson: GeoJSON.FeatureCollection) => {
  // Updates Zustand store; triggers MapController.syncLayers automatically
  addGeoJsonLayer(geojson, {
    name: 'My uploaded data',
    visible: true,
  });
};

Toggling Layer Visibility

Components read from and write to the store using Zustand selectors:

import { useStore } from '@geolibre/core';

function VisibilityToggle({ layerId }: { layerId: string }) {
  const visible = useStore(state => 
    state.layers.find(l => l.id === layerId)?.visible ?? false
  );

  const toggle = () => {
    useStore.setState(state => ({
      layers: state.layers.map(l =>
        l.id === layerId ? { ...l, visible: !l.visible } : l
      ),
    }));
  };

  return <button onClick={toggle}>{visible ? 'Hide' : 'Show'}</button>;
}

When the button updates the store, syncLayers adjusts the corresponding MapLibre layer's layout.visibility property without requiring direct map access from the UI component.

Supporting Alternative Renderers

The store-driven architecture enables alternative renderers to subscribe to the same state. The CesiumLayerSync class (packages/map/src/cesium-layer-sync.ts) demonstrates this by implementing its own synchronization logic:

import { useStore } from '@geolibre/core';
import { CesiumLayerSync } from '@geolibre/map/cesium-layer-sync';

const cesiumSync = new CesiumLayerSync();

useStore.subscribe(state => state.layers, (layers) => {
  cesiumSync.syncLayers(layers); // Applies same state to 3D globe
});

Key Implementation Files

Summary

  • GeoLibre uses a single Zustand store (useStore) as the exclusive source of truth for all application state.
  • The MapController subscribes to store changes and executes syncLayers() to reconcile state with MapLibre GL.
  • UI components never mutate MapLibre directly; they update the store via useStore.setState() or helper functions.
  • The reconciliation algorithm handles creation, updates, and removal of MapLibre sources and layers deterministically.
  • Alternative renderers like Cesium can subscribe to the same store, making the architecture extensible and renderer-agnostic.

Frequently Asked Questions

Why does GeoLibre use Zustand instead of React Context for state management?

Zustand provides better performance characteristics for high-frequency updates and eliminates the provider nesting required by React Context. The store-driven architecture in GeoLibre benefits from Zustand's subscription model, which allows the MapController to listen specifically to the layers slice without re-rendering unrelated UI components when map data changes.

How does GeoLibre handle performance when syncing hundreds of layers?

The syncLayers() method in packages/map/src/map-controller.ts implements a diffing algorithm that only creates, updates, or removes MapLibre resources that have actually changed. By comparing the previous and current layers arrays from the Zustand store, the controller avoids redundant API calls to MapLibre GL, ensuring efficient synchronization even with complex layer hierarchies.

Can I use GeoLibre's store without the MapLibre renderer?

Yes. The @geolibre/core package is designed to be renderer-agnostic. You can import useStore and the layer management helpers without importing the map package. The CesiumLayerSync implementation in packages/map/src/cesium-layer-sync.ts demonstrates how to create custom synchronization logic for alternative rendering engines while using the same Zustand store.

What happens if MapLibre GL throws an error during synchronization?

The syncLayers() method includes error boundaries that catch MapLibre GL exceptions during layer creation or updates. When an error occurs, the controller rolls back the specific MapLibre changes while preserving the Zustand store state, allowing the application to display user-friendly error messages without crashing the map instance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →