I'm working on a quite significantly sized web application that heavily interacts with Google Maps using Vue3 with composition API, typescript, and Pinia store.
The application is not complicated in it's UI elements - a few buttons, sliders, toggles as inputs and a couple of tables displaying certain properties of the state as numeric/text values.
My app's structure is centered around a state pattern, with a single Context class instance (stored in Pinia) managing different states and various elements related to Google Maps (like overlays, Markers, Polylines). The Context also includes a complicated network-like system with elements similar to nodes and paths, each packed with details (e.g., Markers, Polyline instances).
For example, context.network.lanes[].setpoints[].marker.
It probably doesn't mean much, but the Context class (and its imports) contains ~20 custom modules totaling in a couple thousand lines of code.
One example for which I would like reactivity is displaying the values of the selected Setpoint in a table. The coordinates, type, labels, etc. of context.network.selected_lane.selected_setpoint. And as user selects another setpoint or another lane, the table would automatically react and update its content.
I've run into challenges like google maps markers being duplicated when maps is reactive, strict equality check resulting in false when comparing a Point instance local variable with that same instance as the value of a reactive property, DOM elements not changing when the supposedly reactive value changes, and so on.
What strategies, Vue3 features, or tools could help manage this complexity and depth?
I have tried using markRaw in combination with reactive, shallowReactive, and even thought about not using Vue at all. But I can't come up with a solid approach.
Making everything reactive:
- Increases overhead
- Requires explicit
markRawfor third-party objects - New objects created within a reactive instance are not reactive by default, causing debugging headaches
Making selectively reactive using shallowReactive:
- Increases confusion about what is reactive and what is not
- Not recommended by Vue
Not using Vue's reactivity system at all:
- Would have to manually update DOM elements when values change, and vice-versa.
Ideally, I would not need to use and import Vue libraries in my custom modules.
Broad overview of the current structure:
// Pinia store
export const useContextStore = defineStore('context', {
state: () => {
return { context: reactive(new Context()) }
},
getters: {
state_label: (state) => state.context.state.constructor.name,
node_selected: (state) => state.context.network?.selected_node ? "selected" : "",
lane_selected: (state) => state.context.network?.selected_lane ? "selected" : "",
},
actions: {
set_map(map: google.maps.Map): void { this.context.set_map(map); },
keypress_delete(): void { this.context.keypress_delete(); },
node_on_click_callback(node: network.NetworkNode): void { this.context.node_on_click_callback(node); },
// More user actions including keypresses, mouse-clicks, etc.
}
});
// Context class
export class Context implements ContextInterface
{
state: State;
map_manager: MapManager;
network: network.Network | null;
draft_straight: network.DraftStraight | null;
constructor()
{
this.state = new StateDefault(this);
this.map_manager = shallowReactive(new MapManager());
this.network = null;
this.draft_straight = null;
}
public transition(next_state: State): void
{
this.state!.on_exit();
this.state = next_state;
this.state.on_entry();
}
public set_map(map: google.maps.Map): void { this.state.set_map(map); }
public keypress_delete(): void { this.state!.keypress_delete(); }
public node_on_click_callback(node: network.NetworkNode): void { this.state!.node_on_click_callback(node); }
// More user actions including keypresses, mouse-clicks, etc.
}
// Default state. One of multiple concrete states.
export class StateDefault extends State
{
on_entry(): void { /* ... */ }
on_exit(): void { /* ... */ }
set_map(map: google.maps.Map): void { this.context.map_manager.set_map(map); }
keypress_delete(): void { console.log("Can't delete, nothing selected"); }
node_on_click_callback(node: network.NetworkNode): void
{
this.context.network!.select_node(node);
this.context.transition(new StateNodeSelected(this.context));
}
// More implementations for the user actions in this state.
}
// Network module
export namespace network
{
export class DraftStraight
{
private network_ref: Network;
private setpoints: math.Point[];
// Many more properties.
// Google maps 3rd party library properties.
private map: google.maps.Map;
private markers: visualize.Marker[];
private polyline: visualize.Polyline | null;
// Constructor and methods.
}
export class NetworkNode
{
network_ref: Network;
position: math.Point;
source_lanes: NetworkLane[];
target_lanes: NetworkLane[];
// Google maps 3rd party library properties.
map: google.maps.Map;
marker: visualize.Marker;
// Constructor and methods.
}
// More similar classes: NetworkLane, Straight, Intersection, Roundabout.
export class Network
{
origin: math.geo.Geodetic;
nodes: NetworkNode[];
straights: Straight[];
intersections: Intersection[];
roundabouts: Roundabout[];
selected_node: NetworkNode | null;
selected_lane: NetworkLane | null;
map: google.maps.Map;
origin_marker: visualize.Marker;
// Constructor and methods.
public select_node(node: NetworkNode): void
{
this.selected_node?.remove_highlight();
this.selected_node = node;
this.selected_node.add_highlight();
// ...
}
// More methods.
}
}