# How MapController.syncLayers Reconciles MapLibre Sources and Layers in GeoLibre

> Discover how MapController syncLayers in GeoLibre synchronizes MapLibre sources and layers with application state for a consistent visual map. Learn about the reconciliation algorithm.

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

---

**`MapController.syncLayers` implements a store-driven reconciliation algorithm that synchronizes MapLibre GL JS style layers and sources with an ordered array of GeoLibreLayer objects from the Zustand store, ensuring the visual map state always mirrors the application state.**

GeoLibre adopts a "store-driven" rendering model where the Zustand store maintains the single source of truth for all map layers. When the store emits a new ordered array of `GeoLibreLayer` objects, `MapController.syncLayers` reconciles the current MapLibre style with that array through a precise three-phase process. This method, implemented in [`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts), handles layer removal, source lifecycle management, deterministic ordering, and per-feature filter application.

## The Three-Phase Reconciliation Architecture

The `syncLayers` method orchestrates reconciliation through distinct phases that ensure atomic updates to the MapLibre map instance. Each phase handles specific aspects of the synchronization lifecycle while maintaining referential integrity between store layers and native MapLibre style layers.

### Phase 1: Removing Orphaned Layers and Sources

The controller first identifies and removes any MapLibre style-layer ID that existed in the previous sync (`this.layerIds`) but is absent from the incoming `layers` array. This cleanup prevents visual artifacts and memory leaks from detached sources.

The method iterates through cached layer IDs and invokes `removeLayerFromMap` for stale entries:

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

```

*Source: [MapController.syncLayers implementation](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L1075-L1086)*

**`removeLayerFromMap`**, defined in [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts), performs garbage collection on sources. When the last style layer referencing a source (such as a PMTiles archive or Vector-Tile source) is removed, the function disposes of the underlying source to free memory and network resources.

### Phase 2: Bottom-Up Layer Synchronization

After cleanup, the controller walks the new `layers` array **bottom-up** (from last index to first) to insert or update each store layer. This directionality is critical: because the last array entry represents the visual topmost layer, processing backwards ensures that when a layer creates multiple native style layers (e.g., a GeoJSON layer yielding fill, line, circle, and marker layers), each new style layer anchors **below** the first style layer belonging to the next higher-order store layer.

```typescript
for (let i = layers.length - 1; i >= 0; i--) {
  syncLayer(map, layers[i], this.getBeforeStyleLayerId(layers, i));
}

```

*Source: [MapController.syncLayers implementation](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L1089-L1102)*

The **`beforeId`** parameter determines z-index positioning. The controller calculates this via `getBeforeStyleLayerId`, which searches forward in the sorted store list for the first existing native style layer belonging to a higher-z-index layer. If no such layer exists, it falls back to the explicit `layer.beforeId` property used for custom layers such as Deck.gl-rendered COG rasters.

### Phase 3: State Finalization and UI Updates

Once the synchronization loop completes, the controller updates internal bookkeeping and triggers ancillary UI refreshes:

```typescript
this.layerIds = nextIds;
this.syncedLayers = layers;
this.applyBasemapVisibility();
this.applyBasemapOpacity();
this.publishLayerDisplayNames(layers);
this.refreshLayerControl(layers);
this.syncLayerControlState();

```

*Source: [MapController.syncLayers implementation](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L1102-L1109)*

This phase persists the new layer order to `this.layerIds` and `this.syncedLayers`, synchronizes basemap visibility and opacity, publishes layer display names to the global `window.__GEOLIBRE_LAYER_LABELS__` object for external UI panels, and refreshes the LayerControl component state.

## Source Management and Feature Filtering in syncLayer

The **`syncLayer`** function in [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts) performs the heavy lifting of the reconciliation process. This utility function handles four critical responsibilities:

- **Source creation and updates**: Instantiates MapLibre sources for GeoJSON, PMTiles files, raster URLs, vector-tile endpoints, and other supported formats, registering custom protocols when necessary.
- **Style layer generation**: Creates the necessary native style layers (fill, line, circle, raster, video, image) using IDs generated by `getCandidateStyleLayers`.
- **Filter merging**: Injects time-slider windows, rule-based visibility, and annotation visibility constraints via **`withFeatureFilters`** (lines 1670-1685 in [`layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/layer-sync.ts)), merging these with existing filter expressions.
- **External layer coordination**: Applies the `setExternalDeckLayerOrderHandler` for Deck.gl-rendered COG rasters that require ordering logic outside native MapLibre layers.

The function preserves custom paint properties and opacity settings during updates, ensuring that user modifications to layer styling persist across reconciliation cycles.

## Deterministic Layer Ordering with getBeforeStyleLayerId

Precise z-index control requires calculating the correct insertion point for each new style layer. The **`getBeforeStyleLayerId`** method (lines 2125-2139 in [`map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/map-controller.ts)) implements this logic by:

1. Walking forward in the store's `layers` array from the current index
2. Identifying the first layer that already has native style layers rendered on the map
3. Returning the ID of that layer's first native style layer to serve as the `beforeId` anchor

This approach guarantees that even when layers are reordered or restored from persistence, the visual stack remains identical to the store's ordered array. The explicit `layer.beforeId` property serves as an override for advanced use cases involving custom rendered layers that exist outside the standard GeoLibre layer lifecycle.

## Practical Implementation Examples

### React Component Integration

In a React application, subscribe to the Zustand store and invoke `waitAndSyncLayers` whenever the layers array changes:

```tsx
import { useEffect } from "react";
import { useAppStore } from "@geolibre/core";
import { MapController } from "packages/map/src/map-controller";

export function GeoMap({ mapContainer }: { mapContainer: HTMLDivElement }) {
  const controller = new MapController(mapContainer);
  const layers = useAppStore(state => state.layers);

  useEffect(() => {
    controller.waitAndSyncLayers(layers);
  }, [layers]);

  useEffect(() => () => controller.dispose(), []);
}

```

### Manual Synchronization Outside React

For test suites or vanilla JavaScript contexts, drive synchronization manually:

```typescript
import { MapController } from "packages/map/src/map-controller";
import { GeoLibreLayer } from "@geolibre/core";

const mapDiv = document.getElementById("map")!;
const controller = new MapController(mapDiv);

controller.waitAndSyncLayers(initialLayerArray);

// After adding or reordering layers
controller.waitAndSyncLayers(updatedLayerArray);

```

## Summary

- **`MapController.syncLayers`** in [`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts) implements a three-phase reconciliation process that synchronizes MapLibre style layers with the Zustand store's `layers` array.
- **Stale layer removal** uses `removeLayerFromMap` to clean up style layers and their associated sources when no longer referenced.
- **Bottom-up insertion** (processing the array from highest index to lowest) ensures correct z-index ordering by anchoring new layers below existing higher-order layers.
- **`getBeforeStyleLayerId`** calculates deterministic insertion points by searching forward in the layer stack for the first existing native style layer.
- **`syncLayer`** in [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts) manages source lifecycle, style layer creation, and per-feature filter merging via `withFeatureFilters`.
- **State finalization** updates internal caches (`this.layerIds`, `this.syncedLayers`), publishes display names to `window.__GEOLIBRE_LAYER_LABELS__`, and refreshes UI controls.

## Frequently Asked Questions

### How does GeoLibre handle source cleanup when layers are removed?

When `syncLayers` detects a layer ID in `this.layerIds` that no longer exists in the incoming store array, it calls `removeLayerFromMap`. This function removes the style layer from the MapLibre map and checks if any other layers reference the same source. If the removed layer was the final reference, `removeLayerFromMap` disposes of the source entirely, preventing memory leaks from orphaned PMTiles archives, vector-tile sources, or GeoJSON data.

### Why does MapController.syncLayers iterate through layers backwards?

The method processes the `layers` array from last index to first (bottom-up) because the store array order represents visual stacking, with the last element being the topmost layer. This directionality ensures that when a single store layer generates multiple native style layers (such as separate fill, line, and circle layers for a GeoJSON feature), each generated layer can anchor below the first style layer of the next higher-z-index store layer, maintaining correct visual hierarchy without ordering race conditions.

### What is the purpose of the beforeId parameter in the synchronization logic?

The `beforeId` parameter controls the insertion point of new style layers within the MapLibre style stack. Calculated by `getBeforeStyleLayerId`, it identifies the first existing native style layer belonging to a higher-z-index store layer, allowing GeoLibre to insert new layers immediately below it. This mechanism supports complex reordering operations and enables integration with external rendering systems like Deck.gl through explicit `layer.beforeId` overrides.

### Where does GeoLibre store the layer state that triggers syncLayers?

GeoLibre maintains the canonical layer state in a Zustand store defined in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts), specifically in the `layers: GeoLibreLayer[]` array. When application code modifies this array—adding, removing, or reordering layers—the store emits the new state to subscribed components, which typically invoke `controller.waitAndSyncLayers(layers)` to trigger the reconciliation process in [`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts).