How MapController.syncLayers Reconciles MapLibre Sources and Layers with the Zustand Store in GeoLibre
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 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 (source). 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). 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). 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). 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). 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– Contains theMapControllerclass and thesyncLayersorchestration logic that coordinates the six-step pipeline.packages/map/src/layer-sync.ts– Provides low-level operations includingsyncLayerandremoveLayerFromMap, which directly manipulate MapLibre's style API.packages/core/src/store.ts– Defines the Zustand store structure holding thelayersarray 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:
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:
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:
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.tsis 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
beforeIdvalues viagetBeforeStyleLayerIdto ensure correct z-index stacking. - The store remains immutable; the controller only reads from
@geolibre/coreand 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.
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. 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →