# How Attribute Table Selection Syncs with Map Highlighting and Zoom-to-Feature in GeoLibre

> Learn how GeoLibre synchronizes attribute table selection with map highlighting and zoom to feature using its reactive Zustand store for seamless GIS data interaction.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-04

---

**GeoLibre uses a centralized Zustand store to synchronize attribute table selection with map highlighting and zoom-to-feature through reactive state subscriptions and bidirectional updates.**

In the opengeos/GeoLibre codebase, selection state acts as the single source of truth that connects the attribute table UI to the map canvas. When users click rows in the table or features on the map, the same store fields update—triggering automatic visual synchronization and optional viewport fitting. This article explains the complete data flow with source code references from the actual implementation.

## Core Architecture: The Selection Store

GeoLibre's selection system centers on **[`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts)**, which exports a Zustand store containing two critical state fields:

- **`selectedFeatureId`** — the anchor feature (last clicked), or `null`
- **`selectedFeatureIds`** — the complete multi-selection array

The store provides two actions for updating selection:

```ts
// packages/core/src/store.ts – selection actions
selectFeature: (id) => set({ 
  selectedFeatureId: id, 
  selectedFeatureIds: id ? [id] : [] 
}),

selectFeatures: (ids, anchorId) =>
  set({
    selectedFeatureIds: ids,
    selectedFeatureId:
      anchorId != null && ids.includes(anchorId) 
        ? anchorId 
        : (ids.at(-1) ?? null),
  }),

```

These actions ensure consistent state whether the selection originates from a single click, Ctrl/Cmd additive selection, or Shift range selection.

## From Table Click to Store Update

In **[`apps/geolibre-desktop/src/components/panels/AttributeTable.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/components/panels/AttributeTable.tsx)**, row clicks are handled by `computeRowSelection`, a helper that resolves the new selection set based on modifier keys:

```tsx
// apps/geolibre-desktop/src/components/panels/AttributeTable.tsx
const handleRowClick = (featureId: string, event: ReactMouseEvent) => {
  const additive = event.ctrlKey || event.metaKey;   // Ctrl/Cmd toggles
  const range = event.shiftKey;                      // Shift creates range
  const { ids, anchor } = computeRowSelection({
    featureId,
    sortedIds: sorted.map(r => r.featureId),
    selectedIds: selectedFeatureIds,
    anchorId: selectedFeatureId,
    additive,
    range,
  });

  // Revert from "Show Selected" to "Show All" on plain click
  if (!additive && !range && featureView === "selected") {
    setFeatureView("all");
  }

  // Update global selection store
  selectFeatures(ids, anchor);
};

```

The `selectFeatures` call propagates immediately to all subscribers, including the map canvas.

## From Store Change to Map Highlight and Zoom

The **[`packages/map/src/MapCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapCanvas.tsx)** component subscribes to selection state via `useAppStore`. A `useEffect` reacts to every change and coordinates highlighting and zoom behavior:

```tsx
// packages/map/src/MapCanvas.tsx – syncing selection → map
useEffect(() => {
  const layer = layers.find((item) => item.id === selectedLayerId);
  const highlightIds = resolveHighlightIds({ 
    selectedFeatureIds, 
    selectedFeatureId 
  });
  
  const nextKey = selectedLayerId && highlightIds.length > 0
    ? `${selectedLayerId}:${highlightIds.join("\u0000")}`
    : null;
    
  const shouldFit = Boolean(
    zoomToSelectedFeature && 
    nextKey && 
    nextKey !== previousSelectedFeatureKey.current
  );
  previousSelectedFeatureKey.current = nextKey;

  // Highlight (and optionally fit) features
  controller.current?.highlightFeature(layer, highlightIds, { fit: shouldFit });

  // DuckDB layers: fetch bounds separately for zoom
  if (layer && isDuckDBQueryLayer(layer) && shouldFit && selectedFeatureId) {
    const bounds = duckDBBridge()?.getFeatureBounds?.(layer.id, selectedFeatureId);
    if (bounds) controller.current?.fitBounds(bounds);
  }
}, [
  layers,
  selectedLayerId,
  selectedFeatureId,
  selectedFeatureIds,
  zoomToSelectedFeature,
]);

```

The effect performs three critical functions:

1. **`resolveHighlightIds`** — determines which features should be visually emphasized based on anchor and multi-selection state
2. **Stable key generation** — uses `selectedLayerId` plus null-delimited IDs to detect meaningful changes and prevent redundant operations
3. **Conditional zoom** — only fits bounds when `zoomToSelectedFeature` is enabled and the selection has actually changed

## DuckDB Layer Special Handling

For **DuckDB-based query layers**, the standard highlight mechanism may not provide geometry bounds directly. The `MapCanvas` effect detects these layers with `isDuckDBQueryLayer()` and requests bounds through the `duckDBBridge`:

```tsx
if (layer && isDuckDBQueryLayer(layer) && shouldFit && selectedFeatureId) {
  const bounds = duckDBBridge()?.getFeatureBounds?.(layer.id, selectedFeatureId);
  if (bounds) controller.current?.fitBounds(bounds);
}

```

This ensures zoom-to-feature works consistently across both standard vector layers and dynamically queried datasets.

## Bidirectional Synchronization: Map to Table

The synchronization flows both ways. When users click features directly on the map, the map controller calls `selectFeature` or `selectFeatures` on the same store. The attribute table subscribes to these same fields and uses a virtualizer to scroll the selected row into view, maintaining UI coherence regardless of interaction origin.

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) | Zustand store with `selectedFeatureId`, `selectedFeatureIds`, and selection actions |
| [`apps/geolibre-desktop/src/components/panels/AttributeTable.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/components/panels/AttributeTable.tsx) | Row click handling, selection computation, and store updates |
| [`packages/map/src/MapCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/MapCanvas.tsx) | Reactive effect that highlights features and triggers zoom-to-feature |
| [`apps/geolibre-desktop/src/lib/selection-actions.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/selection-actions.ts) | Shared helpers for dialog-driven selections |

## Summary

- **Centralized state** in `@geolibre/core` eliminates synchronization bugs between table and map
- **`selectFeatures`** handles single, additive, and range selection patterns with proper anchor management
- **Reactive subscription** in `MapCanvas` automatically updates highlighting when selection changes
- **Zoom-to-feature** is controlled by the `zoomToSelectedFeature` boolean and guarded by stable key comparison
- **DuckDB layers** receive special bounds-fetching logic to ensure consistent zoom behavior
- **Bidirectional updates** mean any selection change—table or map—propagates to all UI components

## Frequently Asked Questions

### How does GeoLibre prevent unnecessary zoom operations when selection hasn't changed?

The `MapCanvas` effect builds a stable key from `selectedLayerId` and the sorted `highlightIds` array, joined with a null delimiter (`\u0000`). This key is stored in a ref (`previousSelectedFeatureKey`) and compared against the new key. Zoom only occurs when `zoomToSelectedFeature` is enabled, the key is non-null, and the key differs from the previous value.

### Can multiple features be highlighted simultaneously from the attribute table?

Yes. The `selectFeatures` action accepts an array of IDs and manages multi-selection through the `selectedFeatureIds` array. The `handleRowClick` function in [`AttributeTable.tsx`](https://github.com/opengeos/GeoLibre/blob/main/AttributeTable.tsx) supports Ctrl/Cmd for additive selection and Shift for range selection, passing the resulting ID set to the store.

### What happens when zoom-to-feature is enabled for a DuckDB query layer?

For DuckDB layers, the map controller cannot derive bounds directly from highlighted features. Instead, the effect calls `duckDBBridge()?.getFeatureBounds(layer.id, selectedFeatureId)` to fetch geometry bounds asynchronously, then invokes `controller.current?.fitBounds(bounds)` if valid bounds are returned.