# GeoLibre Zustand Store Architecture for State Management: A Deep Dive into the Monolithic Store Pattern

> Explore GeoLibre's monolithic Zustand store architecture for state management. Learn how this pattern, using temporal middleware, enables immutable state with undo redo. Optimize your app's state.

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

---

**GeoLibre implements a single, monolithic Zustand store exported as `useAppStore` from [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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`, `styleLibrary` for all vector and raster sources
- **UI panels**: `ui.processingOpen`, `ui.attributeTableOpen`, `selectedLayerId`, `selectedFeatureIds`
- **Collaboration**: `collaboration` state, 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:

```tsx
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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/history.ts):

1. **Coalescing**: Rapid successive changes (such as map dragging operations) are coalesced into single history entries using `getHistoryCoalesceMs` to prevent history pollution
2. **Memory bounding**: The `trimHistoryBySize` function (lines 96-101) automatically prunes the oldest snapshots when the combined feature payload exceeds `MAX_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 for `addGeoJsonLayer`)
- **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 edits
- **`subscribeProjectRestoreHistory`**: 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:

1. **Predictability**: A single immutable snapshot represents the entire application state at any moment, simplifying debugging and state inspection
2. **Universal undo/redo**: The `temporal` wrapper captures any change without requiring special instrumentation of individual UI actions
3. **Cross-panel coordination**: When a layer visibility toggle occurs, both the Layers panel and MapCanvas react automatically because they subscribe to the same `layers` slice 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:

```tsx
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()`:

```ts
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:

```ts
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:

```tsx
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 `useAppStore` from [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)
- The store uses the `temporal` middleware 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 `set` function
- Components optimize performance by using selectors with `shallow` equality 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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.