GeoLibre Zustand Store Architecture for State Management: A Deep Dive into the Monolithic Store Pattern
GeoLibre implements a single, monolithic Zustand store exported as useAppStore from packages/core/src/store.ts, wrapped with the temporal middleware from zundo to provide immutable state management with comprehensive undo/redo capabilities across the entire application.
GeoLibre manages all client-side state through a centralized Zustand architecture defined in packages/core/src/store.ts. This monolithic approach establishes one source of truth for every UI component, handling map views, layer collections, UI panel visibility, and collaboration data through a strongly-typed interface. By funneling all mutations through this single store, GeoLibre ensures predictable state transitions while enabling complex features like temporal history and cross-panel synchronization.
Monolithic Store Structure in packages/core/src/store.ts
The entire application state lives in a single Zustand store created using create<AppState>() and exported as useAppStore. This store initialization occurs in packages/core/src/store.ts and serves as the exclusive dependency for all React components requiring state access.
The store utilizes Zustand's create function with a TypeScript generic AppState that enumerates every state slice from project metadata to GPS coordinates. The implementation wraps the store creator with temporal from the zundo library, which intercepts all set calls to record immutable snapshots for undo/redo functionality.
export const useAppStore = create<AppState>()(
temporal(
(set, get) => ({
projectName: DEFAULT_PROJECT_NAME,
projectPath: null,
mapView: createDefaultMapView(),
basemapStyleUrl: DEFAULT_BASEMAP,
layers: [],
layerGroups: [],
ui: { /* panel states */ },
addLayer: (layer, beforeLayerId) => { /* implementation */ },
setMapView: (view, markDirty?) => { /* implementation */ },
// ... additional state and actions
})
)
);
The temporal wrapper requires a partialize function (defined elsewhere in the codebase) to determine which state fields should be tracked in the history stack. This configuration ensures that transient UI states can be excluded from undo history while core data mutations remain reversible.
State Slices and Selector Patterns
The AppState interface (defined in packages/core/src/types.ts, lines 108-179) organizes data into logical slices that cover every aspect of the geospatial application:
- Project & file handling:
projectName,projectPath,isDirty,recentProjects - Map view:
mapView,basemapStyleUrl,basemapVisible,basemapOpacity, 2-D/3-D mode flags - Layers:
layers,layerGroups,layerLibrary,styleLibraryfor all vector and raster sources - UI panels:
ui.processingOpen,ui.attributeTableOpen,selectedLayerId,selectedFeatureIds - Collaboration:
collaborationstate, chat messages, and presence indicators - Presentation modes:
storymap,printLayout,widgets,dashboardColumns - Auxiliary data:
gpsStatus,pointerCoords,comments
All state values are plain JavaScript objects without nested observables. Components subscribe to specific slices using selector functions to minimize re-renders:
import { shallow } from 'zustand/shallow';
import { useAppStore } from '@geolibre/core';
const { projectName, isDirty } = useAppStore(
state => ({
projectName: state.projectName,
isDirty: state.isDirty
}),
shallow
);
The shallow equality check (imported on line 4 of store.ts) prevents re-renders when unrelated state properties change, ensuring optimal performance even as the monolithic store grows.
Temporal History and Undo/Redo Implementation
The temporal wrapper from zundo provides the architecture for GeoLibre's undo/redo system. Each mutation passes through this middleware layer, which captures snapshots of relevant state according to the partialize configuration.
History management includes two critical optimizations defined in packages/core/src/history.ts:
- Coalescing: Rapid successive changes (such as map dragging operations) are coalesced into single history entries using
getHistoryCoalesceMsto prevent history pollution - Memory bounding: The
trimHistoryBySizefunction (lines 96-101) automatically prunes the oldest snapshots when the combined feature payload exceedsMAX_HISTORY_FEATURE_COUNT, preventing memory exhaustion with large datasets
These mechanisms ensure that the undo stack remains performant and memory-safe regardless of dataset size or user interaction velocity.
Mutation Patterns and Action Methods
Every state modification occurs through methods defined on the store itself rather than direct mutation. These actions use Zustand's set and get functions to produce new immutable state objects.
Key mutation categories include:
- Layer CRUD:
addLayer,removeLayer,updateLayer,moveLayer(lines 644-650 foraddGeoJsonLayer) - Map manipulation:
setMapView,setMapGrid,setSecondaryMapView - UI toggles:
setProcessingOpen,setStorymapPanelOpen,setCollaborateDialogOpen - Collaboration:
addCollaborationChat,updateCollaborationPresence
All methods follow the pattern of receiving parameters, calling get() to access current state if needed, and calling set() to merge updates. This immutable approach ensures React components receive fresh references only when watched data actually changes.
Project-Level State Persistence and History
Beyond field-level temporal tracking, GeoLibre implements project-level undo capabilities through specialized functions:
registerProjectRestoreHistory(lines 978-986): Treats entire project loads and saves as single undoable transactions, distinct from granular field editssubscribeProjectRestoreHistory: Enables UI components to react to the availability of project-level undo/redo operations
This architecture allows users to undo a complete project restoration while maintaining separate history stacks for individual editing operations within that project.
Why GeoLibre Uses a Single Store Architecture
GeoLibre adopts a store-driven SPA pattern where components read state exclusively from useAppStore and never mutate MapLibre GL directly. This architecture provides three distinct advantages:
- Predictability: A single immutable snapshot represents the entire application state at any moment, simplifying debugging and state inspection
- Universal undo/redo: The
temporalwrapper captures any change without requiring special instrumentation of individual UI actions - Cross-panel coordination: When a layer visibility toggle occurs, both the Layers panel and MapCanvas react automatically because they subscribe to the same
layersslice in the unified store
Practical Implementation Examples
Subscribing to State in React Components
Components should select only the specific fields they need to minimize re-renders:
import { useAppStore } from '@geolibre/core';
export function ProjectHeader() {
const { projectName, isDirty, setProjectName } = useAppStore(state => ({
projectName: state.projectName,
isDirty: state.isDirty,
setProjectName: state.setProjectName,
}));
return (
<header className="flex justify-between items-center">
<h1>{projectName}{isDirty && ' *'}</h1>
<button onClick={() => setProjectName('Untitled')}>Reset</button>
</header>
);
}
Programmatic Layer Import
Access store methods outside React components using getState():
import { useAppStore } from '@geolibre/core';
import type { FeatureCollection } from 'geojson';
function importGeoJson(name: string, fc: FeatureCollection) {
useAppStore.getState().addGeoJsonLayer(name, fc);
}
importGeoJson('Countries', worldGeoJson);
The addGeoJsonLayer method creates a new GeoLibreLayer object, inserts it into the layers array, and automatically marks the project as dirty.
Implementing Undo/Redo Controls
The temporal sub-store exposes undo/redo methods globally:
import { useAppStore } from '@geolibre/core';
// Undo the last action
useAppStore.getState().temporal.undo();
// Redo the next action
useAppStore.getState().temporal.redo();
Managing UI Panel Visibility
UI state lives in the same store, enabling persistent panel states across sessions:
import { useAppStore } from '@geolibre/core';
export function ProcessingButton() {
const open = useAppStore(state => state.ui.processingOpen);
const setOpen = useAppStore(state => state.setProcessingOpen);
return (
<button onClick={() => setOpen(!open)}>
{open ? 'Close' : 'Open'} Processing
</button>
);
}
Summary
- GeoLibre centralizes all state in a single Zustand store exported as
useAppStorefrompackages/core/src/store.ts - The store uses the
temporalmiddleware from zundo to provide automatic undo/redo functionality with configurable coalescing and memory limits - State is organized into typed slices covering project data, map views, layers, UI panels, and collaboration features
- All mutations occur through store methods that use immutable updates via Zustand's
setfunction - Components optimize performance by using selectors with
shallowequality checks to subscribe only to relevant state slices - Project-level history management operates separately from field-level temporal tracking through
registerProjectRestoreHistory
Frequently Asked Questions
What is the GeoLibre Zustand store architecture?
The GeoLibre Zustand store architecture is a monolithic state management pattern that consolidates all application state—including map views, layers, UI panels, and project metadata—into a single store exported as useAppStore from packages/core/src/store.ts. According to the GeoLibre source code, this store is wrapped with the temporal middleware to provide built-in undo/redo capabilities while maintaining type safety through the AppState TypeScript interface.
How does GeoLibre handle undo/redo functionality?
GeoLibre implements undo/redo through the zundo library's temporal wrapper, which records snapshots of state changes defined by a partialize function. The system coalesces rapid changes using getHistoryCoalesceMs and bounds memory usage via trimHistoryBySize with MAX_HISTORY_FEATURE_COUNT. Users can call useAppStore.getState().temporal.undo() or .redo() from anywhere in the application to navigate the history stack.
Can I use multiple stores in GeoLibre instead of the monolithic approach?
While Zustand supports multiple stores, GeoLibre's architecture is intentionally designed around a single store pattern to enable cross-panel coordination and universal undo/redo. The AppState interface in packages/core/src/types.ts defines all possible state fields, and components expect to access related data (such as layers and selected features) from the same store instance. Deviating from this pattern would break the temporal history integration and require significant refactoring of the mutation helpers.
How does the store handle performance with large datasets?
The store maintains performance through several mechanisms: the shallow equality function prevents unnecessary re-renders when unrelated state changes, the trimHistoryBySize function prunes old snapshots when feature counts exceed MAX_HISTORY_FEATURE_COUNT, and selectors allow components to subscribe only to specific fields rather than the entire state object. Additionally, history coalescing prevents rapid interactions (like continuous map zooming) from flooding the undo stack.
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 →