Audience: GeoView core developers
We use Zustand store for our state management. The store is split into slices (map, layer, UI, etc.) with three access patterns: selector hooks for React components, getter/setter functions for controllers, and controller methods for mutations.
For access to store values from within the browser you should use React Developer Tools to inspect React components.
The store is active when (1) running in dev environment or (2) the local storage GEOVIEW_DEVTOOLS key is set.
useStore hook for variable linked to UI and can be updated from another component.
useState hook for variable linked to UI and only referenced in your component.
useEffect hook
Each store state file in src/core/stores/states/ exports three types of functions:
useStore*) — React components onlyFine-grained hooks that trigger re-renders when the selected state changes.
// Pattern: useStore[SliceName][PropertyName]
export const useStoreMapZoom = (): number =>
useStore(useGeoViewStore(), (state) => state.mapState.zoom);
export const useStoreMapClickMarker = (): TypeClickMarker | undefined =>
useStore(useGeoViewStore(), (state) => state.mapState.clickMarker);
getStore*) — controllers and non-React codePoint-in-time snapshots. Takes mapId as first parameter.
export const getStoreMapZoom = (mapId: string): number =>
getStoreMapState(mapId).zoom;
export const getStoreMapOrderedLayerInfo = (
mapId: string,
): TypeOrderedLayerInfo[] => getStoreMapState(mapId).orderedLayerInfo;
setStore*) — controllers onlyMutate state via internal actions. Takes mapId as first parameter. Never call these from React components.
export const setStoreMapZoom = (mapId: string, zoom: number): void => {
getStoreMapState(mapId).actions.setZoom(zoom);
};
export const setStoreMapClickMarker = (
mapId: string,
marker: TypeClickMarker,
): void => {
getStoreMapState(mapId).actions.showClickMarker(marker);
};
Components read state through useStore* selector hooks and mutate through controller methods. Never call setStore* functions directly from components.
import { useStoreMapZoom, useStoreMapClickMarker } from '@/core/stores/states/map-state';
import { useMapController } from '@/core/controllers/use-controllers';
export function MyComponent(): JSX.Element {
// Read state — re-renders when values change
const zoom = useStoreMapZoom();
const clickMarker = useStoreMapClickMarker();
// Get controller for mutations
const mapController = useMapController();
const handleZoom = useCallback((): void => {
mapController.zoomToExtent(extent);
}, [mapController]);
return <Box onClick={handleZoom}>Zoom: {zoom}</Box>;
}
Controllers import getStore* and setStore* functions directly. They contain the validation and business logic.
import {
getStoreMapZoom,
setStoreMapZoom,
setStoreMapProjection,
} from "@/core/stores/states/map-state";
export class MapController extends AbstractMapViewerController {
zoomToLevel(zoom: number): void {
const mapId = this.getMapId();
const currentZoom = getStoreMapZoom(mapId);
// Business logic / validation
if (zoom !== currentZoom) {
setStoreMapZoom(mapId, zoom);
}
}
}
Inside a controller, access other controllers through this.getControllersRegistry():
// Inside a controller method
this.getControllersRegistry().layerSetController.triggerResetFeatureInfo(layerPath);
this.getControllersRegistry().uiController.setCircularProgress(true);
mapViewer.controllers// In a plugin
const layerPaths =
this.mapViewer.controllers.layerController.getLayerEntryLayerPaths();
this.mapViewer.controllers.timeSliderController?.checkInitTimeSliderLayerAndApplyFilters(
layer,
config,
);
| Context | Read State | Mutate State |
|---|---|---|
| React component | useStore* hooks |
Controller methods via useMapController(), etc. |
| Inside controller | getStore* getters |
setStore* setters |
| Plugin / non-React class | this.mapViewer.controllers.* |
this.mapViewer.controllers.* methods |
| Test suite | this.getControllersRegistry().* |
Controller methods |
Key rule: Components never call setStore* directly — mutations always go through controller methods.