How GeoLibre's Store-Driven Architecture with Zustand Manages Application State
GeoLibre uses a single global Zustand store as the single source of truth, wrapped with temporal middleware from zundo to provide unlimited undo/redo, where all state mutations flow through defined actions and UI components subscribe only to specific slices they need.
The open-source GeoLibre project (opengeos/GeoLibre) implements a store-driven architecture that centralizes all application state within a Zustand store defined in packages/core/src/store.ts. This pattern ensures predictable state mutations, efficient UI updates through selective subscriptions, and robust undo/redo functionality while maintaining a strict separation between the UI layer and MapLibre rendering logic.
Centralized Store Definition
Global Store Creation with Temporal Middleware
At the heart of GeoLibre's architecture lies the useAppStore hook, created in packages/core/src/store.ts using Zustand's create<AppState>() function. The store is wrapped with the temporal middleware from zundo to enable unlimited undo and redo capabilities across all state changes.
// packages/core/src/store.ts
export const useAppStore = create<AppState>()(
temporal((set, get) => ({
// state properties and actions
}))
);
The temporal wrapper records every state change in a history stack, exposing undo(), redo(), and history management methods via useAppStore.getState().temporal. History size is automatically trimmed based on feature payload limits using the trimHistoryBySize helper to prevent memory bloat.
Unified Application State Interface
All mutable data—including project metadata, map view configuration, layers, UI panel flags, and collaboration state—are defined in the AppState interface. Key fields include layers, mapView, selectedLayerId, ui, and collaboration, ensuring a strongly-typed, centralized data model.
// AppState interface includes:
// - layers: Layer[]
// - mapView: MapViewState
// - selectedLayerId: string | null
// - ui: { processingOpen, storymapPanelOpen, ... }
// - collaboration: CollaborationState
State Mutation Patterns
Action-Based State Updates
The store exposes a rich set of actions that directly mutate state via Zustand's set function. These actions—such as addLayer, removeLayer, setMapView, setBasemapOpacity, and setCollaboration—serve as the only sanctioned mechanism for state changes.
Each action is a thin wrapper that ensures immutability and marks the project dirty when appropriate. For example, adding a GeoJSON layer triggers addGeoJsonLayer(name, data), which updates the layers array and flags the project as modified.
Selective Subscriptions for Performance
Components subscribe only to the specific state slices they require, preventing unnecessary re-renders. The MapCanvas component in packages/map/src/MapCanvas.tsx demonstrates this pattern by selecting only layers, mapView, and basemap-related fields:
// packages/map/src/MapCanvas.tsx
const layers = useAppStore(state => state.layers);
const mapView = useAppStore(state => state.mapView);
const basemapOpacity = useAppStore(state => state.basemapOpacity);
This selective subscription ensures that UI components update only when their relevant data changes, maintaining 60fps performance during complex map interactions.
Temporal Undo/Redo Capabilities
GeoLibre's integration with zundo provides robust history management without boilerplate. The middleware automatically tracks state changes and exposes control methods through the store:
// Undo last action
useAppStore.getState().temporal.undo();
// Redo previously undone action
useAppStore.getState().temporal.redo();
The system supports coalescing rapid edits (such as continuous slider adjustments) into single history entries, preventing the undo stack from filling with intermediate states. The partialize helper explicitly excludes ephemeral fields like gpsStatus and live-collaboration data from the history stack.
MapLibre Synchronization Layer
Decoupled Rendering Architecture
GeoLibre enforces a strict unidirectional data flow where the UI never manipulates MapLibre directly. Instead, components update the Zustand store, and dedicated synchronization files reconcile changes with the map engine.
The packages/map/src/layer-sync.ts file listens to store changes and applies them to MapLibre sources and layers. Similarly, packages/map/src/map-controller.ts reacts to store updates for map grid changes, basemap switches, and view transitions. This separation allows the core state to remain platform-agnostic while rendering specifics stay isolated in the map package.
State Persistence Strategies
Ephemeral vs. Persistent State
Not all state belongs in saved projects or undo history. GeoLibre uses the partialize helper to exclude transient data—such as gpsStatus and live-collaboration slices—from persistence layers. This keeps transient runtime data out of the .geolibre.json project files and the undo stack.
Project-level libraries for styles, layers, and templates (styleLibrary, layerLibrary, templateLibrary) persist separately via IndexedDB in the desktop application, distinct from the main project serialization.
UI State Namespacing
UI-only flags are grouped under a nested ui object containing fields like processingOpen and storymapPanelOpen. This namespacing keeps interface state separate from domain data, allowing panels to toggle without affecting the core project model or triggering unnecessary map re-renders.
Practical Implementation Examples
The following patterns demonstrate working with GeoLibre's store-driven architecture in application code:
import { useAppStore } from 'packages/core/src/store';
// Reading a slice: list of layers
const layers = useAppStore(state => state.layers);
// Adding a new GeoJSON layer
function addStatesLayer() {
const geojson = /* FeatureCollection fetched elsewhere */;
useAppStore.getState().addGeoJsonLayer('US States', geojson);
}
// Toggling layer visibility
function toggleVisibility(layerId: string) {
const layer = useAppStore.getState().layers.find(l => l.id === layerId);
if (layer) {
useAppStore.getState().setLayerVisibility(layerId, !layer.visible);
}
}
// Managing UI panels
function openProcessing() {
useAppStore.getState().setProcessingOpen(true);
}
// Temporal undo/redo
function undo() {
useAppStore.getState().temporal.undo();
}
function redo() {
useAppStore.getState().temporal.redo();
}
Summary
- Single Source of Truth: All application state resides in one Zustand store (
useAppStore) defined inpackages/core/src/store.ts. - Temporal Middleware: The store uses zundo to provide unlimited undo/redo with automatic history trimming via
trimHistoryBySize. - Action-Based Mutations: State changes occur exclusively through defined actions like
addLayerandsetMapView, ensuring predictable updates. - Selective Subscriptions: Components subscribe only to required state slices (e.g.,
useAppStore(state => state.layers)), preventing unnecessary re-renders. - Sync Layer Pattern: MapLibre updates are handled by
layer-sync.tsandmap-controller.ts, which react to store changes rather than direct UI manipulation. - Selective Persistence: The
partializehelper excludes ephemeral data (GPS status, collaboration) from persistence and undo history.
Frequently Asked Questions
How does GeoLibre handle undo and redo functionality?
GeoLibre wraps its Zustand store with the temporal middleware from zundo, which automatically records every state change in a history stack. Components can call useAppStore.getState().temporal.undo() or redo() to navigate through the history. The system also coalesces rapid edits and trims history based on payload size to manage memory.
What is the difference between the global store and MapLibre's internal state?
The Zustand store serves as the single source of truth for application logic, while MapLibre maintains its own rendering state. GeoLibre bridges these through synchronization files like packages/map/src/layer-sync.ts, which listens to store changes and updates MapLibre sources accordingly. UI components never modify MapLibre directly; they only update the store.
Which state fields are excluded from persistence in GeoLibre?
Fields such as gpsStatus and live-collaboration data are explicitly excluded from persistence using the partialize helper. These ephemeral values exist only during the runtime session and do not appear in saved .geolibre.json files or the undo history stack, ensuring that transient UI state doesn't pollute project saves.
How do components optimize performance when subscribing to the store?
Components use selector functions to subscribe only to specific slices of state (e.g., useAppStore(state => state.layers)). This pattern ensures that components re-render only when their subscribed data changes, rather than on every store update. The MapCanvas component leverages this to maintain smooth performance while tracking complex map 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 →