# How MapController.syncLayers Reconciles MapLibre Sources and Layers with the Zustand Store in GeoLibre

> Learn how MapController.syncLayers keeps MapLibre sources and layers in sync with the Zustand store. This GeoLibre function ensures data integrity and visual consistency.

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

---

**MapController.syncLayers acts as a deterministic bridge that synchronizes the visual MapLibre map state with the Zustand store by removing stale layers, inserting new ones in bottom-up order, and re-applying basemap settings while keeping the store as the single source of truth.**

GeoLibre implements a unidirectional data flow where the `@geolibre/core` Zustand store maintains the canonical list of `GeoLibreLayer` objects and the `@geolibre/map` package renders those layers to the MapLibre canvas. The `MapController.syncLayers` method is the critical reconciliation engine that translates store updates into native MapLibre style operations without mutating the application state.

## The Six-Step Reconciliation Pipeline

When `syncLayers` receives a new array of layers from the store, it executes a deterministic sequence defined in [`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts) to ensure the visual map matches the store exactly.

### 1. Style Readiness Verification

The method first guards against premature execution by checking `isStyleReady`. If the MapLibre instance or its style has not finished loading, `syncLayers` returns early to prevent errors from invalid style operations.

### 2. Stale Layer Removal

The controller builds a `Set` of incoming layer IDs and iterates over the previously known `layerIds` array. Any ID not present in the new set is purged from the map via `removeLayerFromMap`, which is implemented in [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts) ([source](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L3081-L3085)). This ensures that layers deleted from the Zustand store disappear immediately from the visualization.

### 3. Bottom-Up Layer Insertion

Incoming layers are processed from bottom to top, where the last array entry represents the topmost visual layer. For each layer, `syncLayer` is invoked with the `beforeId` parameter calculated by `getBeforeStyleLayerId` ([source](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L3098-L3100)). This positions native MapLibre style layers *beneath* the first style layer of the layer above it, guaranteeing correct z-order even when style layers are added out-of-band.

### 4. Internal Bookkeeping Updates

After processing all layers, the controller refreshes its internal `layerIds` and `syncedLayers` references to reflect the current state ([source](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L3111-L3112)). This cache enables efficient diffing during the next reconciliation cycle.

### 5. Basemap State Persistence

To ensure that basemap visibility and opacity changes survive the sync operation, the controller re-applies these properties via `applyBasemapVisibility` and `applyBasemapOpacity` ([source](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L3113-L3115)). This step is crucial when toggling basemaps or adjusting transparency through the UI.

### 6. UI Control Synchronization

Finally, the method publishes display names for the sidebar (`publishLayerDisplayNames`), refreshes the layer control panel (`refreshLayerControl`), and synchronizes built-in control states (`syncLayerControlState`) ([source](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts#L3116-L3118)). These calls ensure that React components and DOM controls reflect the new layer stack without requiring additional store subscriptions.

## Source Architecture and Key Files

According to the opengeos/GeoLibre source code, the reconciliation logic spans three primary modules:

- **[`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts)** – Contains the `MapController` class and the `syncLayers` orchestration logic that coordinates the six-step pipeline.
- **[`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts)** – Provides low-level operations including `syncLayer` and `removeLayerFromMap`, which directly manipulate MapLibre's style API.
- **[`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)** – Defines the Zustand store structure holding the `layers` array that serves as the single source of truth for the entire application.

## Practical Implementation Examples

### Triggering a Manual Sync

When integrating with external data sources, you can force a reconciliation by passing the current store snapshot to the controller:

```typescript
import { useAppStore } from '@geolibre/core';
import { createMapController } from '@geolibre/map';

const controller = createMapController();
const layers = useAppStore.getState().layers;
controller.syncLayers(layers);

```

### Adding a Layer Programmatically

Update the Zustand store and immediately sync to render the new layer:

```typescript
import { useAppStore } from '@geolibre/core';

useAppStore.setState(state => ({
  layers: [...state.layers, myNewGeoLibreLayer],
}));
controller.syncLayers(useAppStore.getState().layers);

```

### Removing a Layer via the Store

Filter the store array and let `syncLayers` handle the native cleanup:

```typescript
useAppStore.setState(state => ({
  layers: state.layers.filter(l => l.id !== 'layer-to-remove')
}));
controller.syncLayers(useAppStore.getState().layers);

```

## Summary

- **MapController.syncLayers** in [`packages/map/src/map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts) is the sole authority for translating Zustand store changes into MapLibre visual updates.
- The reconciliation follows a strict six-phase pipeline: readiness check, stale removal, bottom-up insertion, bookkeeping updates, basemap persistence, and UI synchronization.
- Layer ordering is handled by calculating `beforeId` values via `getBeforeStyleLayerId` to ensure correct z-index stacking.
- The store remains immutable; the controller only reads from `@geolibre/core` and writes to the MapLibre style, preventing circular dependencies.

## Frequently Asked Questions

### How does GeoLibre handle layer ordering when syncing?

GeoLibre processes the layers array from bottom to top, inserting each layer's native MapLibre style layers before the first style layer of the layer above it. This is achieved by passing a calculated `beforeId` to `syncLayer`, ensuring that the visual stack matches the array order in the Zustand store regardless of when style layers were created.

### What happens to basemap settings during a sync?

The controller explicitly re-applies `visibility` and `opacity` properties to basemap style layers after completing the main reconciliation loop. This prevents basemap toggles or transparency adjustments from being reset when the layer list changes, as implemented in lines 3113-3115 of [`map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/map-controller.ts).

### Can I call syncLayers before the map style loads?

No. The method includes an `isStyleReady` guard that returns early if the MapLibre style is not yet loaded. Attempting to sync before initialization completes would throw errors when accessing style methods, so the controller waits until the map is fully ready.

### Where is the single source of truth for layer state stored?

The canonical layer state resides in the Zustand store defined in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts). The `MapController` only reads from this store and never writes back to it, maintaining a unidirectional data flow where UI components and the map controller consume the same immutable state.