How GeoLibre's Zustand Store Architecture Manages Layer Synchronization with MapController
GeoLibre uses a single Zustand store as the source of truth for all map layers, with MapController subscribing to the layers slice and calling syncLayers() to reconcile MapLibre-GL state through add, update, remove, and reorder operations.
This architecture guarantees deterministic rendering and eliminates race conditions between the UI and map view. According to the opengeos/GeoLibre source code, layer synchronization is implemented through a unidirectional data flow: UI components mutate the store, the store notifies subscribers, and MapController translates those changes into MapLibre-GL API calls.
Core Architecture: The Global Zustand Store
The application state lives in one central store defined in [packages/core/src/store.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts). The store is created with Zustand's create function and uses shallow equality checking to prevent unnecessary re-renders.
import { create } from "zustand";
import { shallow } from "zustand/shallow";
export const useStore = create<StoreState>((set, get) => ({
layers: [],
activeLayerId: null,
// …additional state fields
addLayer: (layer) => set((state) => ({ layers: [...state.layers, layer] })),
removeLayer: (id) => set((state) => ({ layers: state.layers.filter(l => l.id !== id) })),
updateLayer: (id, updates) => set((state) => ({
layers: state.layers.map(l => l.id === id ? { ...l, ...updates } : l)
})),
setLayerVisibility: (id, visible) => get().updateLayer(id, { visible }),
}));
Layer State Shape
Each entry in the layers array follows the GeoLibreLayer interface:
export interface GeoLibreLayer {
id: string;
type: "vector" | "raster" | "group" | "marker" | "geojson";
source: any; // MapLibre source definition
layout?: any; // MapLibre layout properties
paint?: any; // MapLibre paint properties
visible: boolean;
metadata?: Record<string, unknown>;
}
The store holds everything needed to reconstruct the map: source definitions, styling, visibility flags, and ordering.
How MapController Subscribes to Layer Changes
In [packages/map/src/MapController.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts), the controller creates a synchronous subscription to the layers slice:
import { useStore } from "@geolibre/core";
import { shallow } from "zustand/shallow";
class MapController {
constructor() {
this.unsubscribe = useStore.subscribe(
(state) => state.layers, // selector function
(layers) => this.syncLayers(layers), // listener function
{ equalityFn: shallow } // prevent deep comparison overhead
);
}
destroy() {
this.unsubscribe();
}
// …
}
The shallow comparator ensures the subscription only fires when the array reference or top-level properties change—not on every nested property mutation. This is critical for performance with large layer sets.
The syncLayers Reconciliation Algorithm
The MapController.syncLayers() method implements a full diff-and-patch cycle between the store's layers array and MapLibre-GL's current style state.
Four Core Operations
- Add – New layer IDs trigger
map.addSource()andmap.addLayer() - Update – Existing layers receive
source.setData(),map.setLayoutProperty(), ormap.setPaintProperty()calls - Remove – Vanishing layer IDs cause
map.removeLayer()andmap.removeSource()execution - Reorder – Array index changes invoke
map.moveLayer()to match z-order
class MapController {
static syncLayers(layers: GeoLibreLayer[]) {
const map = getMapInstance();
const currentLayerIds = new Set(map.getStyle().layers?.map(l => l.id) || []);
const newLayerIds = new Set(layers.map(l => l.id));
// Remove stale layers
currentLayerIds.forEach(id => {
if (!newLayerIds.has(id)) {
map.removeLayer(id);
if (map.getSource(id)) map.removeSource(id);
}
});
// Add or update layers
layers.forEach((layer, index) => {
const sourceExists = map.getSource(layer.id);
if (!sourceExists) {
// Add new source and layer
map.addSource(layer.id, layer.source);
map.addLayer({
id: layer.id,
type: layer.type,
source: layer.id,
layout: layer.layout,
paint: layer.paint,
});
} else {
// Update existing: data, layout, paint
const source = map.getSource(layer.id) as GeoJSONSource;
if (layer.source.data) source.setData(layer.source.data);
Object.entries(layer.layout || {}).forEach(([key, val]) => {
map.setLayoutProperty(layer.id, key, val);
});
Object.entries(layer.paint || {}).forEach(([key, val]) => {
map.setPaintProperty(layer.id, key, val);
});
}
// Sync visibility
map.setLayoutProperty(layer.id, "visibility",
layer.visible ? "visible" : "none"
);
// Enforce ordering
if (index > 0) {
const beforeId = layers[index - 1].id;
map.moveLayer(layer.id, beforeId);
}
});
}
}
Bidirectional Consistency Patterns
Because the store is the single source of truth, any map interaction that should persist must flow back through the store. GeoLibre handles this through callback delegation.
Drag-and-Drop Reordering
When a user reorders layers via the UI:
// LayerPanel component
function onDragEnd(result: DropResult) {
if (!result.destination) return;
const newOrder = reorderArray(layers, result.source.index, result.destination.index);
useStore.getState().setLayers(newOrder); // Store update triggers syncLayers
}
// MapController.syncLayers receives the reordered array
// and calls map.moveLayer() to match
Map-Driven Events
If the map itself initiates changes (rare, but possible with some plugins), MapController provides a callback that updates the store rather than modifying the map directly:
class MapController {
onLayerReorder(mapMovedLayerId: string, beforeId?: string) {
const layers = useStore.getState().layers;
const reordered = moveLayerInArray(layers, mapMovedLayerId, beforeId);
useStore.getState().setLayers(reordered); // Triggers subscription → syncLayers
}
}
This creates a loop: map event → store update → subscription fired → syncLayers → map updated. The redundancy is safe because syncLayers detects no diff on the second pass.
Handling Edge Cases in Layer Synchronization
| Scenario | Implementation Detail |
|---|---|
| Remote GeoParquet loading | Store creates a temporary DuckDB-WASM source; syncLayers promotes it to a vector source once parsing completes |
| Raster opacity animation | Store updates paint["raster-opacity"] value; syncLayers applies via map.setPaintProperty with transition options |
| Removing animating layers | Store removal queues; syncLayers first calls source.cancel() or equivalent to prevent stray callbacks |
| Rapid toggle spam | shallow equality batches rapid setLayerVisibility calls; MapLibre receives final state, not intermediate flickers |
| Source type changes | syncLayers detects incompatible source type, removes old source+layer, then adds new configuration |
Key Files in the Synchronization Pipeline
| File | Role |
|---|---|
[packages/core/src/store.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) |
Defines global Zustand store, GeoLibreLayer interface, and mutation helpers (addLayer, removeLayer, setLayerVisibility) |
[packages/map/src/MapController.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts) |
Implements store subscription and syncLayers() reconciliation logic |
[apps/geolibre-desktop/src/hooks/useUiProfileBootstrap.ts](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/useUiProfileBootstrap.ts) |
Bootstraps the application, initializes store, wires MapController subscription |
[packages/plugins/src/plugins/maplibre-openaerialmap.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/maplibre-openaerialmap.ts) |
Example plugin demonstrating direct store reads and layer updates |
Practical Integration Examples
Adding a Layer from File Upload
import { useStore } from "@geolibre/core";
import { convertFileToGeoJsonLayer } from "@geolibre/utils";
async function onFileSelected(file: File) {
const layer = await convertFileToGeoJsonLayer(file);
useStore.getState().addLayer(layer);
// MapController.syncLayers() runs automatically via subscription
}
Toggling Visibility in a Layer Panel
function LayerItem({ layer }: { layer: GeoLibreLayer }) {
const setVisibility = useStore(state => state.setLayerVisibility);
return (
<button onClick={() => setVisibility(layer.id, !layer.visible)}>
{layer.visible ? "Hide" : "Show"}
</button>
);
}
Plugin-Initiated Source Update
// In a custom raster plugin
const updateOpacity = (layerId: string, opacity: number) => {
useStore.getState().updateLayer(layerId, {
paint: { "raster-opacity": opacity }
});
// syncLayers applies via map.setPaintProperty
};
Summary
- Single source of truth: All layer state lives in one Zustand store at [
packages/core/src/store.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) - Subscription-driven updates: [
MapController.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts) listens tostate.layerswithshallowequality and callssyncLayers()on every change - Full reconciliation: The
syncLayersmethod handles add, update, remove, visibility toggle, and reorder operations against MapLibre-GL - Safe bidirectional flow: All map interactions that should persist route through store updates, preventing state divergence
- Performance optimized:
shallowcomparison and deterministic diffing prevent unnecessary MapLibre-GL API calls
Frequently Asked Questions
How does GeoLibre prevent unnecessary re-renders when layer properties change?
GeoLibre uses Zustand's shallow comparator in the subscription options. This compares array references and primitive values without deep object traversal, so updating a single layer's paint property only triggers syncLayers if the containing layers array is replaced. The subscription in [packages/map/src/MapController.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapController.ts) explicitly configures { equalityFn: shallow }.
What happens if a layer is removed while its source is still loading?
The syncLayers implementation queues the removal if it detects an active load state on the source. It first cancels the pending request or waits for the data event, then executes map.removeLayer and map.removeSource. This prevents MapLibre-GL from throwing errors for operations on incomplete sources.
Can plugins bypass the store and manipulate the map directly?
Technically yes, but this breaks synchronization. The architecture expects all layer mutations to flow through useStore. The plugin example at [packages/plugins/src/plugins/maplibre-openaerialmap.ts](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/maplibre-openaerialmap.ts) demonstrates the correct pattern: reading current state from useStore.getState() and dispatching updates through store actions, never calling MapLibre-GL layer methods directly.
How does GeoLibre handle layer ordering compared to MapLibre-GL's beforeId parameter?
The layers array order in the Zustand store defines the z-order. During syncLayers, the method calls map.moveLayer(layer.id, beforeId) for each layer whose index changed, where beforeId is the previous array element's id. This declarative approach lets UI components reorder layers by simply reordering the array and letting the subscription propagate changes.
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 →