# How Layer Synchronization Works Between Zustand and MapLibre GL JS in GeoLibre

> Discover how GeoLibre synchronizes layers between Zustand and MapLibre GL JS. Learn how the MapController uses a deterministic syncLayers routine to keep maps up-to-date with state changes.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-07-31

---

**GeoLibre manages layer synchronization by maintaining a Zustand store as the single source of truth, with a MapController reacting to state changes to reconcile MapLibre GL JS layers through a deterministic `syncLayers` routine.**

The GeoLibre open-source project implements a store-driven architecture to keep map visualization perfectly aligned with application state. By combining **Zustand** for state management and **MapLibre GL JS** for rendering, the codebase ensures that any change to the layer list immediately reflects on the map without direct API manipulation from UI components. Understanding how this layer synchronization between Zustand and MapLibre GL JS works is essential for developers building custom geospatial applications with GeoLibre.

## The Store-Driven Architecture

### Zustand as the Single Source of Truth

At the core of GeoLibre's synchronization mechanism lies a global **Zustand store** defined in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts). This store maintains the `layers` slice, which holds an array of `GeoLibreLayer` objects representing the complete state of the map's data layers.

When any UI operation occurs—such as adding a layer, removing a layer, or toggling visibility—the store updates directly through dedicated actions. This design ensures that `useAppStore` remains the **only place where layer data is mutated**, preventing any scattered MapLibre API calls throughout the component tree.

## The Synchronization Pipeline

### Reacting to Store Updates

The bridge between the Zustand store and the MapLibre map instance occurs in React components like `MapCanvas` through a `useEffect` hook. When the `layers` array changes in the store, the component retrieves the current state and invokes `controller.waitAndSyncLayers(layers)`.

This method first ensures the map style is fully loaded before proceeding, preventing race conditions during initialization. Once ready, it delegates to the core synchronization logic.

### The Reconciliation Algorithm

The `syncLayers` method in [`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts) (lines 1072-1088) performs a full reconciliation between the Zustand state and the MapLibre map. The algorithm executes three distinct phases:

1. **Removal of obsolete layers**: The controller compares the current `layerIds` cache against the new layer list from the store. Any MapLibre layers whose IDs no longer exist in the store are removed via `removeLayerFromMap`.

2. **Bottom-up insertion**: The controller iterates over the desired layer order **from bottom to top** (using `for (let i = layers.length - 1; i >= 0; i--)`). For each layer, it calls `syncLayer` with `getBeforeStyleLayerId` to determine insertion points, ensuring each layer is placed *below* the first style layer that belongs to a higher stack position. This preserves the correct visual z-order.

3. **Cache updates**: After reconciliation, the controller updates its internal `layerIds` and `syncedLayers` caches, then reapplies basemap visibility, opacity settings, and layer control states.

## Translating State to MapLibre Layers

### The syncLayer Helper

Individual layer translation happens in [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts) (lines 84-99) through the `syncLayer` helper function. This utility creates the appropriate MapLibre sources and style layers for a given `GeoLibreLayer` object, handling special cases such as:

- **PMTiles archives** with custom protocol adapters
- **Raster and vector** source configurations
- Custom adapters for specialized data formats

By centralizing this translation logic, GeoLibre ensures that the Zustand store's abstract layer definitions correctly map to MapLibre's specific source and layer requirements.

## Implementation Examples

When adding a new vector layer from a UI button, components interact only with the store:

```tsx
// Example: Adding a new vector layer from a UI button
import { useAppStore } from "@geolibre/core";
import { pointLayer } from "@geolibre/core"; // helper to build a GeoLibreLayer

function AddLayerButton() {
  const addLayer = useAppStore((s) => s.layers.add);
  return (
    <button onClick={() => addLayer(pointLayer("new-points"))}>
      Add Points Layer
    </button>
  );
}

```

The `MapCanvas` component handles the synchronization side effect without direct MapLibre API calls in the UI:

```tsx
// Example: MapCanvas component that triggers synchronization
import { useEffect } from "react";
import { useAppStore } from "@geolibre/core";
import { MapController } from "@geolibre/map";

export function MapCanvas({ map }) {
  const layers = useAppStore((s) => s.layers);
  const controller = new MapController(map);

  useEffect(() => {
    // Ensure the style is loaded before syncing
    controller.waitAndSyncLayers(layers);
  }, [layers, controller]);
}

```

Inside the `MapController`, the `syncLayers` method implements the reconciliation logic:

```ts
// Inside MapController.syncLayers (simplified)
syncLayers(layers: GeoLibreLayer[]): void {
  if (!this.isStyleReady() || !this.map) return;

  const nextIds = layers.map(l => l.id);
  // Remove stale layers
  for (const id of this.layerIds) {
    if (!nextIds.includes(id)) {
      removeLayerFromMap(this.map, id, this.syncedLayers.find(l => l.id === id));
    }
  }

  // Re‑add / update layers in correct order (bottom‑up)
  for (let i = layers.length - 1; i >= 0; i--) {
    syncLayer(this.map, layers[i], this.getBeforeStyleLayerId(layers, i));
  }

  this.layerIds = nextIds;
  this.syncedLayers = layers;
  // …apply basemap settings, refresh UI controls, etc.
}

```

## Summary

- **Zustand store** ([`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)) maintains the single source of truth for all layer state through the `useAppStore` slice.
- **MapController** ([`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts)) subscribes to store changes and executes `syncLayers` to reconcile differences with MapLibre GL JS.
- **Bottom-up iteration** ensures correct visual z-order when inserting layers into the map style.
- **No direct MapLibre API calls** occur in UI components; all mutations flow through the centralized controller, guaranteeing deterministic state management.
- **Specialized handling** for formats like PMTiles occurs in [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts), translating abstract store objects into concrete MapLibre sources and layers.

## Frequently Asked Questions

### Why does GeoLibre iterate bottom-up when synchronizing layers?

GeoLibre iterates from `layers.length - 1` down to `0` to ensure each layer is inserted **below** the first style layer belonging to any layer higher in the stack. This bottom-up approach in `syncLayers` preserves the intended visual hierarchy and z-order without requiring complex dependency tracking between layers.

### What prevents synchronization from running before the map style is ready?

The `waitAndSyncLayers` method checks `this.isStyleReady()` before executing `syncLayers`. If the MapLibre style is not yet loaded, the controller waits for the style to become available, preventing errors that would occur from attempting to add sources or layers to an uninitialized map canvas.

### How does GeoLibre handle specialized formats like PMTiles during synchronization?

The `syncLayer` helper in [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts) contains specialized logic for detecting and handling PMTiles archives, along with raster and vector sources. It automatically registers the necessary custom protocol adapters and creates the appropriate MapLibre source configuration based on the `GeoLibreLayer` type definition.

### Can UI components call MapLibre API methods directly to update layers?

**No.** GeoLibre's architecture strictly prohibits direct MapLibre API calls from UI components. All layer mutations must go through the Zustand store actions, which then trigger the `MapController.syncLayers` routine. This constraint ensures a single source of truth and prevents state inconsistencies between the application store and the map visualization.