# How GeoLibre's Store-Driven Architecture with Zustand Manages Application State

> Discover how GeoLibre leverages a store-driven architecture with Zustand and zundo for unlimited undo/redo. Learn how state mutations flow through actions for efficient application state management.

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

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
// 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.

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapCanvas.tsx) demonstrates this pattern by selecting only `layers`, `mapView`, and basemap-related fields:

```tsx
// 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:

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/.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:

```tsx
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 in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/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 `addLayer` and `setMapView`, 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.ts`](https://github.com/opengeos/GeoLibre/blob/main/layer-sync.ts) and [`map-controller.ts`](https://github.com/opengeos/GeoLibre/blob/main/map-controller.ts), which react to store changes rather than direct UI manipulation.
- **Selective Persistence**: The `partialize` helper 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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/.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.