How MapController.syncLayers Reconciles MapLibre Sources and Layers in GeoLibre

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, 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:

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

Source: MapController.syncLayers implementation

removeLayerFromMap, defined in 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.

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

Source: MapController.syncLayers implementation

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:

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

Source: MapController.syncLayers implementation

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 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), 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) 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:

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:

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 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 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, 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.

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 →