# How GeoLibre's Store-Driven Architecture with Zustand Works

> Discover how GeoLibre's store-driven architecture leverages Zustand for efficient state management, enabling undo/redo and cross-component synchronization. Learn more.

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

---

**GeoLibre uses a centralized Zustand store wrapped with zundo's temporal enhancer to manage all application state—enabling undo/redo, cross-component synchronization, and persistence through a single source of truth defined in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts).**

GeoLibre is an open-source geospatial visualization platform built by opengeos that relies on a store-driven architecture to coordinate complex UI state, map views, and layer management. Unlike component-based state solutions, the entire application state lives in a single Zustand store, allowing any component to read or mutate data through selector functions and action methods.

## Core Store Setup in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)

The foundation of GeoLibre's architecture resides in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts), where the `AppState` interface defines the complete shape of the application.

### Zustand Creation with Temporal Enhancer

The store is instantiated using Zustand's `create<AppState>()` function, immediately wrapped by **zundo**'s `temporal` enhancer to provide automatic undo/redo capabilities:

```typescript
// packages/core/src/store.ts (lines 43-46)
export const useAppStore = create<AppState>()(
  temporal(
    (set, get) => ({
      // ... state and actions
    }),
    {
      limit: 50,
      equality: (a, b) => sameCamera(a.mapView, b.mapView),
    }
  )
);

```

The `temporal` wrapper records snapshots of the entire state whenever `set` is called. The `pruneHistoryBySize` option keeps the history bounded to prevent memory bloat, while the `equality` function uses `sameCamera` to avoid duplicate entries when the map view hasn't meaningfully changed.

### Single Source of Truth

The `AppState` interface captures everything: **layers**, **map view**, **UI flags**, **preferences**, and **processing history**. No component holds local copies of this data; every piece of UI consults this central state through the `useAppStore` hook exported from `@geolibre/core`.

## State Access Patterns and Selectors

Components interact with the store through optimized selector functions to prevent unnecessary re-renders.

### Shallow Selector Optimization

Most components use inline selectors with shallow comparison to extract only the slices they need:

```typescript
// packages/core/src/store.ts (lines 3-4)
import { shallow } from 'zustand/shallow';

// In a component:
const layers = useAppStore((s) => s.layers);
const mapView = useAppStore((s) => s.mapView, shallow);

```

By importing `shallow` from `zustand/shallow`, GeoLibre ensures that components only re-render when their specific slice of state actually changes, not when unrelated parts of the store update.

### Reading Complex State

The [`MapCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/MapCanvas.tsx) component demonstrates reading multiple state slices simultaneously:

```tsx
// packages/map/src/MapCanvas.tsx (lines 974-988)
const mapView = useAppStore((s) => s.mapView);
const layers = useAppStore((s) => s.layers);
const selectedFeatureId = useAppStore((s) => s.selectedFeatureId);
const setMapView = useAppStore((s) => s.setMapView);

```

This pattern keeps components declarative and thin, with all data flowing from the central store.

## Centralized Actions and Mutations

All state mutations flow through action methods defined in the store, ensuring consistent updates and maintaining the `isDirty` flag automatically.

### Action Method Implementation

The store defines mutators like `setMapView`, `addLayer`, and `setBasemapOpacity` that receive the current state via closure and return new state:

```typescript
// Example from store.ts structure
setMapView: (newView) => set((state) => ({
  ...state,
  mapView: newView,
  isDirty: true
})),

```

This centralization guarantees that every mutation updates the dirty flag consistently, letting the application track unsaved changes across the entire project.

### Direct State Access for Events

For event handlers that don't need reactive updates, components can access the store imperatively:

```typescript
// Adding a GeoJSON layer directly
const { addGeoJsonLayer } = useAppStore.getState();
const newLayerId = addGeoJsonLayer("My GeoJSON", myFeatureCollection);

```

Using `useAppStore.getState()` bypasses React's render cycle, making it ideal for one-off actions triggered by user interactions.

## Advanced Store Features

Beyond basic state management, GeoLibre's Zustand implementation handles persistence, collaboration, and UI coordination.

### Undo/Redo with Bounded History

The **zundo** integration automatically snapshots state before each mutation. Users can traverse history through standard undo/redo commands, while `pruneHistoryBySize` ensures the history stack never exceeds configured limits. This works transparently—components don't need to implement any special logic to support time-travel debugging.

### Project Persistence Actions

The store provides explicit lifecycle actions for project management:

- **`newProject()`**: Resets state to defaults while preserving undo history structure
- **`loadProject(projectData)`**: Hydrates the store from a serialized project file
- **`markSaved()`**: Clears the `isDirty` flag after successful saves

These actions live alongside the state in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) (lines 58-70), making project serialization a store-level concern rather than a UI responsibility.

### Collaboration State Isolation

A frozen `DEFAULT_COLLABORATION_STATE` object lives within the store but is explicitly excluded from undo history and project serialization. This separation enables **live collaboration features** without polluting the core data model or creating undo entries for remote user actions.

### Modular UI Flags

All dialog visibility and panel states reside under the `ui` key in `AppState`:

```typescript
// Opening the Vector Tools dialog from any component
const setVectorToolOpen = useAppStore((s) => s.setVectorToolOpen);

function OpenVectorToolButton() {
  return <button onClick={() => setVectorToolOpen("buffer")}>Buffer</button>;
}

```

This modular approach makes it trivial to open panels like [`VectorToolsDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/VectorToolsDialog.tsx) or [`StoryMapPanel.tsx`](https://github.com/opengeos/GeoLibre/blob/main/StoryMapPanel.tsx) from any component without prop drilling.

## Performance and Architectural Benefits

GeoLibre's store-driven architecture eliminates prop drilling and ensures state consistency across the application. By co-locating helper functions like `sameCamera`, `clampGridDim`, and `fitGrid` in [`store.ts`](https://github.com/opengeos/GeoLibre/blob/main/store.ts), the codebase keeps logic close to the state it operates on, improving maintainability.

The architecture also enables powerful debugging capabilities—developers can inspect the entire application state through a single object, track changes through the temporal history, and serialize the complete project state for persistence.

## Summary

- **Single Zustand store**: All state lives in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) with the `AppState` interface defining the complete shape.
- **Temporal undo/redo**: The **zundo** wrapper automatically records state snapshots and provides bounded history management.
- **Optimized selectors**: Components use `useAppStore` with shallow comparison to extract specific slices without excessive re-renders.
- **Centralized mutations**: All state changes flow through action methods (`setMapView`, `addLayer`, etc.) that maintain the `isDirty` flag.
- **Persistence ready**: Built-in actions like `loadProject` and `newProject` handle full state hydration and reset.
- **Collaboration support**: Isolated state slices enable real-time features without affecting undo history or project serialization.

## Frequently Asked Questions

### How does GeoLibre handle undo/redo functionality?

GeoLibre uses the **zundo** library's `temporal` enhancer wrapped around the Zustand store in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts). This automatically captures snapshots of the entire `AppState` before each mutation, allowing users to traverse history with standard undo/redo commands. The history is bounded by `pruneHistoryBySize` to prevent memory issues, and custom equality functions like `sameCamera` prevent redundant history entries when state hasn't meaningfully changed.

### What is the difference between using `useAppStore()` and `useAppStore.getState()`?

`useAppStore(selector)` is the standard React hook that subscribes components to state changes and triggers re-renders when the selected slice updates. `useAppStore.getState()` returns the current state object imperatively without subscribing to updates, making it ideal for event handlers or one-off actions where you don't need reactive behavior—such as calling `addGeoJsonLayer` inside a click handler.

### How does the store track unsaved changes?

Every mutation action in the store automatically sets `isDirty: true` when modifying project data. The `markSaved()` action resets this flag to `false` after successful saves. This centralized approach ensures that the dirty state remains consistent regardless of which component initiated the change—whether adding layers in [`MapCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/MapCanvas.tsx) or adjusting settings in [`VectorToolsDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/VectorToolsDialog.tsx).

### Why does GeoLibre use Zustand instead of React Context or Redux?

Zustand provides a lighter-weight solution with built-in support for **shallow equality checks** and **middleware composition** (like the temporal enhancer) without the boilerplate of Redux or the re-render limitations of Context. The `temporal` middleware for undo/redo integrates seamlessly with Zustand's functional API, while the ability to import `useAppStore` anywhere in the monorepo (via `@geolibre/core`) eliminates the provider wrapping required by Context-based solutions.