State management
Geogirafe uses a custom state manager called Brain based on Javascript Proxy objects. It's main features are:
- Lightweight and fast.
- Supports circular references. You can use nested objects without losing reactivity or causing infinite recursion.
- Allows batch updates, allowing multiple state modifications to be grouped and processed together in a single operation. This reduces redundant callbacks to reduce redundant state updates
- Selective deep cloning, to avoid uneccessary copies for large objects and improve perfomance.
Any component that extends the <GirafeHTMLElement> base class automatically inherits access to Geogirafe's state manager (via this.state which references this.context.stateManager.state). By convention, Geogirafe uses two locations to store a component's state variables:
this.statefor core components, e.g.this.state.themes,state.basemaps,state.mouseCoordinates, defined insrc/tools/state/state.ts.this.state.extendedStatefor custom/addon components, e.g.this.state.extendedState.helloworld, defined directly in custom components.
Observing state changes
Observing changes on a state variable is done via the this.subscribe() method:
this.subscribe(
"path to variable in state manager or regular expression",
(_oldValue: any, _newValue: any, _parent: parentType) => {
// callback function executed when a change on the observed variable is detected
}
);
When a callback is triggered:
- The new value (
newValue) is always a Proxy, ensuring further modifications remain reactive. - The old value (
oldValue) is a deep-cloned and frozen snapshot. This guarantees it cannot be modified unintentionally.
In a class, private properties starting with _ are ignored (changes are not observed).
For example, to observe changes in the mouse coordinates variable in the core state (this.state.mouseCoordinates), we use:
this.subscribe(
"mouseCoordinates",
(_oldCoordinates: number[], _newCoordinates: number[]) => {
console.log(_newCoordinates);
}
);
The state manager can also detect deep changes in properties (i.e. multiple levels of nesting). If we wanted to monitor change only on a specific property of an an object in an array, you can use a regular expression with wildcards:
// Note that we use a regular expression with wildcards (*) here
this.subscribe(
/extendedState\.helloworld\.markers\..*\.myproperty/,
(_oldPropVal: boolean, _newPropVal: boolean, _parent: MyObjectType) => {
// callback function executed when a change on the observed variable is detected
console.log(
`myproperty on parent object ${_parent.id} changed from ${_oldPropVal} to ${_newPropVal}`
);
}
);
To observe changes on the length of an array (i.e. e.g. when an item is added or deleted), you can use:
// Note that we use a string containing the path to the marker property here
this.subscribe(
"extendedState.helloworld.myarray",
(
_oldPropVal: MyObjectType[],
_newPropVal: MyObjectType[],
_parent: HelloState
) => {
// callback function exectuted when a change on the observed variable is detected
}
);
Batch updates
To reduce redundant callbacks and improve performance for operations that trigger many small updates, the state manager can also do batch updates with multiple state modifications grouped and processed together.
Use the batchChanges() method from the context’s state manager:
this.state.batchChanges(() => {
// Multiple state modifications here
this.state.language = "en";
this.state.projection = "EPSG:4326";
});
Selective deep cloning
You can mark specific properties to be ignored during cloning using the @BrainIgnoreClone decorator:
class MyLayer {
visible: boolean;
@BrainIgnoreClone
public olLayer: any; // skip deep cloning for OpenLayers layer objects
}
To use decorators, ensure experimentalDecorators is enabled in tsconfig.json.