# How GeoLibre’s Store-Driven Architecture with Zustand Works: Core Patterns Explained

> Discover GeoLibre's store-driven architecture with Zustand. Learn how this central store manages front-end state, enabling undo/redo functionality and efficient UI updates for developers.

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

---

**GeoLibre manages all front-end state through a single Zustand store defined in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts), wrapped with zundo for undo/redo, while UI components read and mutate that state via selectors and centralized action methods.**

GeoLibre is an open-source geospatial project by opengeos that implements a store-driven architecture with Zustand to coordinate map data, UI panels, and user preferences. By housing the entire project state in one observable object and forbidding scattered component state, the codebase ensures that every update flows through a single, auditable location.

## The Single Source of Truth in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)

The heart of the system is the `AppState` interface declared in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts). This interface captures every aspect of a project, including layers, map view parameters, UI visibility flags, preferences, and processing history. No React component holds its own copy of this data; every piece of UI consults the same store, making the architecture strictly store-driven.

The store is instantiated with `useAppStore = create<AppState>()(temporal(...))` (lines 43–46). This produces a plain JavaScript object that is instantly observable anywhere it is imported, establishing the single source of truth for the entire application.

## Undo and Redo via Zundo’s Temporal Wrapper

State mutations are automatically snapshotted by **zundo**’s `temporal` enhancer. Each time an action calls `set` to mutate the store, zundo pushes a new snapshot onto the history stack. To prevent unbounded memory growth, the store uses `pruneHistoryBySize` to cap history length.

Because the wrapper records the whole `AppState` object, users can step backward and forward through any project change without manual diffing or command objects.

## Reading State with Selector Functions

Components import `useAppStore` from `@geolibre/core` and subscribe to only the slices they need. For example, [`packages/map/src/MapCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapCanvas.tsx) (lines 974–988) reads `mapView`, `layers`, and `selectedFeatureId` through inline selectors and calls `setMapView` to update the viewport.

To avoid unnecessary re-renders, most selectors run a shallow comparison using `shallow` from `zustand/shallow` (lines 3–4 of [`store.ts`](https://github.com/opengeos/GeoLibre/blob/main/store.ts)). This prevents React from rerendering when a selector returns a new object reference with identical contents.

```tsx
import { useAppStore } from "@geolibre/core";

export function LayerList() {
  const layers = useAppStore((s) => s.layers);
  const mapView = useAppStore((s) => s.mapView);
  const setMapView = useAppStore((s) => s.setMapView);

  return (
    <ul>
      {layers.map((l) => (
        <li key={l.id} onClick={() => setMapView({ center: l.center })}>
          {l.name}
        </li>
      ))}
    </ul>
  );
}

```

## Centralized Action Methods

All mutations travel through action methods defined on the store, such as `setMapView`, `addLayer`, `addGeoJsonLayer`, `setBasemapOpacity`, and `setMapGrid`. Each method receives the current state via closure and returns a new state object, or mutates in place when it is safe to do so.

Centralizing actions inside [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) guarantees that side effects like the `isDirty` flag stay consistent. When any project data changes, the single `isDirty` boolean updates accordingly, giving the UI a reliable saved-state indicator.

```tsx
import { useAppStore } from "@geolibre/core";

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

```

For imperative calls outside of the React render loop, the store exposes its current state and actions through `useAppStore.getState()`:

```ts
const { addGeoJsonLayer } = useAppStore.getState();
const newLayerId = addGeoJsonLayer("My GeoJSON", myFeatureCollection);

```

## UI Flags and Panel Orchestration

Dialog and panel visibility is controlled through a dedicated `ui` branch inside `AppState` (lines 42–85). Flags for processing dialogs, story-map editors, raster tables, and vector tools all live here. This design makes it trivial to open or close panels from any component without prop drilling or event buses.

The [`VectorToolsDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/VectorToolsDialog.tsx) and [`StoryMapPanel.tsx`](https://github.com/opengeos/GeoLibre/blob/main/StoryMapPanel.tsx) components consume these flags directly, demonstrating how the store-driven architecture decouples UI state from component hierarchy.

## Persistence and Project Loading

The store exposes `newProject`, `loadProject`, and `markSaved` (lines 58–70) to handle project-level transitions. These actions replace or patch the full state object while keeping zundo’s history intact and syncing the `isDirty` flag. When a project loads, the map view, layers, and preferences populate instantly because every subscriber reads from the same source.

Helper utilities such as `sameCamera`, `clampGridDim`, and `fitGrid` are also co-located in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) (lines 71–94), keeping geometry logic adjacent to the state it operates on.

## Collaboration Slice Isolation

GeoLibre includes a frozen `DEFAULT_COLLABORATION_STATE` inside the store (lines 85–99). This slice is deliberately excluded from both zundo snapshots and project serialization. Isolating collaboration state in this way enables real-time multi-user features without cluttering the undo history or the serialized project file.

## Summary

- GeoLibre’s state lives in a single Zustand store at [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) that acts as the sole source of truth.
- The `temporal` wrapper from zundo records full-state snapshots for undo/redo and prunes history to bound memory usage.
- Components read state through selectors and mutate it through centralized actions like `setMapView` and `addLayer`.
- Shallow comparison via `zustand/shallow` minimizes React re-renders when selectors return equivalent data.
- UI visibility flags under the `ui` key let any component open or close panels without prop drilling.
- Project persistence actions replace the full state while preserving undo history and the `isDirty` flag.
- A frozen collaboration slice supports live editing without affecting project serialization or undo stacks.

## Frequently Asked Questions

### How does GeoLibre handle undo and redo?

GeoLibre wraps its Zustand store with zundo’s `temporal` enhancer inside [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts). Every mutation pushes a full snapshot onto an undo stack, and `pruneHistoryBySize` keeps that stack bounded. Users can step backward and forward through project changes because the entire `AppState` is recorded automatically.

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

Zustand provides a plain observable store that any component can import and subscribe to selectively. In GeoLibre, this means components read only the slices they need via selectors, avoiding the excessive re-render issues common with React Context. The architecture also centralizes all mutations in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts), which is harder to enforce with dispersed context providers.

### How do GeoLibre components avoid unnecessary re-renders?

Most components use `useAppStore((s) => s.someSlice)` combined with `shallow` from `zustand/shallow`, as seen in [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) (lines 3–4). This shallow comparison checks whether the selected data has actually changed at the object-property level, so React does not rerender when references change but values remain identical.

### What is the purpose of the collaboration slice in the store?

The `DEFAULT_COLLABORATION_STATE` slice sits inside the Zustand store but is excluded from both zundo history and project serialization. This isolation lets GeoLibre support live collaboration features—such as real-time cursor sharing or session presence—without polluting the core geospatial data model or bloating undo snapshots.