How GeoLibre's Zustand Store Architecture Manages Layer Synchronization with MapController

GeoLibre uses a single Zustand store as the source of truth for all map layers, with MapController subscribing to the layers slice and calling syncLayers() to reconcile MapLibre-GL state through add, update, remove, and reorder operations.

This architecture guarantees deterministic rendering and eliminates race conditions between the UI and map view. According to the opengeos/GeoLibre source code, layer synchronization is implemented through a unidirectional data flow: UI components mutate the store, the store notifies subscribers, and MapController translates those changes into MapLibre-GL API calls.

Core Architecture: The Global Zustand Store

The application state lives in one central store defined in [packages/core/src/store.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts). The store is created with Zustand's create function and uses shallow equality checking to prevent unnecessary re-renders.

import { create } from "zustand";
import { shallow } from "zustand/shallow";

export const useStore = create<StoreState>((set, get) => ({
  layers: [],
  activeLayerId: null,
  // …additional state fields
  addLayer: (layer) => set((state) => ({ layers: [...state.layers, layer] })),
  removeLayer: (id) => set((state) => ({ layers: state.layers.filter(l => l.id !== id) })),
  updateLayer: (id, updates) => set((state) => ({
    layers: state.layers.map(l => l.id === id ? { ...l, ...updates } : l)
  })),
  setLayerVisibility: (id, visible) => get().updateLayer(id, { visible }),
}));

Layer State Shape

Each entry in the layers array follows the GeoLibreLayer interface:

export interface GeoLibreLayer {
  id: string;
  type: "vector" | "raster" | "group" | "marker" | "geojson";
  source: any;          // MapLibre source definition
  layout?: any;         // MapLibre layout properties
  paint?: any;          // MapLibre paint properties
  visible: boolean;
  metadata?: Record<string, unknown>;
}

The store holds everything needed to reconstruct the map: source definitions, styling, visibility flags, and ordering.

How MapController Subscribes to Layer Changes

In [packages/map/src/MapController.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts), the controller creates a synchronous subscription to the layers slice:

import { useStore } from "@geolibre/core";
import { shallow } from "zustand/shallow";

class MapController {
  constructor() {
    this.unsubscribe = useStore.subscribe(
      (state) => state.layers,           // selector function
      (layers) => this.syncLayers(layers), // listener function
      { equalityFn: shallow }             // prevent deep comparison overhead
    );
  }
  
  destroy() {
    this.unsubscribe();
  }
  // …
}

The shallow comparator ensures the subscription only fires when the array reference or top-level properties change—not on every nested property mutation. This is critical for performance with large layer sets.

The syncLayers Reconciliation Algorithm

The MapController.syncLayers() method implements a full diff-and-patch cycle between the store's layers array and MapLibre-GL's current style state.

Four Core Operations

  1. Add – New layer IDs trigger map.addSource() and map.addLayer()
  2. Update – Existing layers receive source.setData(), map.setLayoutProperty(), or map.setPaintProperty() calls
  3. Remove – Vanishing layer IDs cause map.removeLayer() and map.removeSource() execution
  4. Reorder – Array index changes invoke map.moveLayer() to match z-order
class MapController {
  static syncLayers(layers: GeoLibreLayer[]) {
    const map = getMapInstance();
    const currentLayerIds = new Set(map.getStyle().layers?.map(l => l.id) || []);
    const newLayerIds = new Set(layers.map(l => l.id));

    // Remove stale layers
    currentLayerIds.forEach(id => {
      if (!newLayerIds.has(id)) {
        map.removeLayer(id);
        if (map.getSource(id)) map.removeSource(id);
      }
    });

    // Add or update layers
    layers.forEach((layer, index) => {
      const sourceExists = map.getSource(layer.id);
      
      if (!sourceExists) {
        // Add new source and layer
        map.addSource(layer.id, layer.source);
        map.addLayer({
          id: layer.id,
          type: layer.type,
          source: layer.id,
          layout: layer.layout,
          paint: layer.paint,
        });
      } else {
        // Update existing: data, layout, paint
        const source = map.getSource(layer.id) as GeoJSONSource;
        if (layer.source.data) source.setData(layer.source.data);
        
        Object.entries(layer.layout || {}).forEach(([key, val]) => {
          map.setLayoutProperty(layer.id, key, val);
        });
        Object.entries(layer.paint || {}).forEach(([key, val]) => {
          map.setPaintProperty(layer.id, key, val);
        });
      }

      // Sync visibility
      map.setLayoutProperty(layer.id, "visibility", 
        layer.visible ? "visible" : "none"
      );

      // Enforce ordering
      if (index > 0) {
        const beforeId = layers[index - 1].id;
        map.moveLayer(layer.id, beforeId);
      }
    });
  }
}

Bidirectional Consistency Patterns

Because the store is the single source of truth, any map interaction that should persist must flow back through the store. GeoLibre handles this through callback delegation.

Drag-and-Drop Reordering

When a user reorders layers via the UI:

// LayerPanel component
function onDragEnd(result: DropResult) {
  if (!result.destination) return;
  
  const newOrder = reorderArray(layers, result.source.index, result.destination.index);
  useStore.getState().setLayers(newOrder); // Store update triggers syncLayers
}

// MapController.syncLayers receives the reordered array
// and calls map.moveLayer() to match

Map-Driven Events

If the map itself initiates changes (rare, but possible with some plugins), MapController provides a callback that updates the store rather than modifying the map directly:

class MapController {
  onLayerReorder(mapMovedLayerId: string, beforeId?: string) {
    const layers = useStore.getState().layers;
    const reordered = moveLayerInArray(layers, mapMovedLayerId, beforeId);
    useStore.getState().setLayers(reordered); // Triggers subscription → syncLayers
  }
}

This creates a loop: map event → store update → subscription fired → syncLayers → map updated. The redundancy is safe because syncLayers detects no diff on the second pass.

Handling Edge Cases in Layer Synchronization

Scenario Implementation Detail
Remote GeoParquet loading Store creates a temporary DuckDB-WASM source; syncLayers promotes it to a vector source once parsing completes
Raster opacity animation Store updates paint["raster-opacity"] value; syncLayers applies via map.setPaintProperty with transition options
Removing animating layers Store removal queues; syncLayers first calls source.cancel() or equivalent to prevent stray callbacks
Rapid toggle spam shallow equality batches rapid setLayerVisibility calls; MapLibre receives final state, not intermediate flickers
Source type changes syncLayers detects incompatible source type, removes old source+layer, then adds new configuration

Key Files in the Synchronization Pipeline

File Role
[packages/core/src/store.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) Defines global Zustand store, GeoLibreLayer interface, and mutation helpers (addLayer, removeLayer, setLayerVisibility)
[packages/map/src/MapController.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts) Implements store subscription and syncLayers() reconciliation logic
[apps/geolibre-desktop/src/hooks/useUiProfileBootstrap.ts](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/useUiProfileBootstrap.ts) Bootstraps the application, initializes store, wires MapController subscription
[packages/plugins/src/plugins/maplibre-openaerialmap.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/maplibre-openaerialmap.ts) Example plugin demonstrating direct store reads and layer updates

Practical Integration Examples

Adding a Layer from File Upload

import { useStore } from "@geolibre/core";
import { convertFileToGeoJsonLayer } from "@geolibre/utils";

async function onFileSelected(file: File) {
  const layer = await convertFileToGeoJsonLayer(file);
  useStore.getState().addLayer(layer);
  // MapController.syncLayers() runs automatically via subscription
}

Toggling Visibility in a Layer Panel

function LayerItem({ layer }: { layer: GeoLibreLayer }) {
  const setVisibility = useStore(state => state.setLayerVisibility);
  
  return (
    <button onClick={() => setVisibility(layer.id, !layer.visible)}>
      {layer.visible ? "Hide" : "Show"}
    </button>
  );
}

Plugin-Initiated Source Update

// In a custom raster plugin
const updateOpacity = (layerId: string, opacity: number) => {
  useStore.getState().updateLayer(layerId, {
    paint: { "raster-opacity": opacity }
  });
  // syncLayers applies via map.setPaintProperty
};

Summary

Frequently Asked Questions

How does GeoLibre prevent unnecessary re-renders when layer properties change?

GeoLibre uses Zustand's shallow comparator in the subscription options. This compares array references and primitive values without deep object traversal, so updating a single layer's paint property only triggers syncLayers if the containing layers array is replaced. The subscription in [packages/map/src/MapController.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts) explicitly configures { equalityFn: shallow }.

What happens if a layer is removed while its source is still loading?

The syncLayers implementation queues the removal if it detects an active load state on the source. It first cancels the pending request or waits for the data event, then executes map.removeLayer and map.removeSource. This prevents MapLibre-GL from throwing errors for operations on incomplete sources.

Can plugins bypass the store and manipulate the map directly?

Technically yes, but this breaks synchronization. The architecture expects all layer mutations to flow through useStore. The plugin example at [packages/plugins/src/plugins/maplibre-openaerialmap.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/maplibre-openaerialmap.ts) demonstrates the correct pattern: reading current state from useStore.getState() and dispatching updates through store actions, never calling MapLibre-GL layer methods directly.

How does GeoLibre handle layer ordering compared to MapLibre-GL's beforeId parameter?

The layers array order in the Zustand store defines the z-order. During syncLayers, the method calls map.moveLayer(layer.id, beforeId) for each layer whose index changed, where beforeId is the previous array element's id. This declarative approach lets UI components reorder layers by simply reordering the array and letting the subscription propagate changes.

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 →