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

> Discover how GeoLibre's Zustand store architecture synchronizes map layers with MapController. Learn about add, update, remove, and reorder operations for seamless map state management.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: architecture
- Published: 2026-08-03

---

**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)](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.

```typescript
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:

```typescript
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)](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts), the controller creates a **synchronous subscription** to the layers slice:

```typescript
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

```typescript
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:

```typescript
// 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:

```typescript
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)](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)](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)](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)](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

```typescript
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

```typescript
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

```typescript
// 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

- **Single source of truth**: All layer state lives in one Zustand store at [[`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)
- **Subscription-driven updates**: [[`MapController.ts`](https://github.com/opengeos/GeoLibre/blob/main/MapController.ts)](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts) listens to `state.layers` with `shallow` equality and calls `syncLayers()` on every change
- **Full reconciliation**: The `syncLayers` method handles add, update, remove, visibility toggle, and reorder operations against MapLibre-GL
- **Safe bidirectional flow**: All map interactions that should persist route through store updates, preventing state divergence
- **Performance optimized**: `shallow` comparison and deterministic diffing prevent unnecessary MapLibre-GL API calls

## 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)](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)](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.