1

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 markRaw for third-party objects
  • New objects created within a reactive instance are not reactive by default, causing debugging headaches

Making selectively reactive using shallowReactive:

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.
    }
}
3
  • The question is too unspecific and can't be discussed in general terms. It boils down to a specific implementation, which isn't shown. You have to be extremely careful with making third-party objects reactive that were not designed for this, that's for certain. Do you mean that you design custom modules in framework-independent way? Is this justified? If you don't use them with other frameworks then this would add the complexity without benefits. Commented Mar 5, 2024 at 11:32
  • @EstusFlask I added a basic overview of the code structure Commented Mar 5, 2024 at 12:02
  • It's not a good idea overall to use reactive classes, as they have several pitfalls stackoverflow.com/a/76247596/3731501 . In your case StateDefault(this) is a potential mistake. It's unknown what's the reasoning for using pinia store, there may be no need to put everything inside just because you can, some things could be non-reactive singletons. On the other hand, Context, etc look like the extracted parts of a store. The answer has some good points, raw objects can be much faster so it makes sense to keep some non-reactive objects as they are, not to mention reactivity-related bugs Commented Mar 5, 2024 at 12:54

1 Answer 1

4

In my previous company I used to create custom maps for our clients and our systems were catered to work with either Google, Bing or Mapbox based maps.

When working with maps, the data itself can typically be described as "heavy" in terms of size and placing it inside a reactive store, most of the time, does not make sense. Also, note that in most cases, the data entities are designed to work well/seamless/fast with the map, not with a reactive store.

What I found to be the best strategy for dealing with map data is to keep all data entities in flat arrays (or JavaScript Maps, actually - faster than arrays), outside of stores. These collections should be based on entity type and optimised for retrieving/reading items by their unique identifier. The stores should only store the unique identifiers of data instead of the actual data, and the information that actually needs to be reactive, like which items are active, map related options (zoom level, bounds, etc...), volatile/dynamic relations between data types and, obviously, user preferences related to the business logic of your map (e.g: filters, etc...).

Decoupling the map entities from their reactive parts adds an extra layer of complexity to the codebase and requires a shift in how you conceptualise and and use the map data but I guarantee you'll be happy with the results in terms of performance and, in the long run, makes your models more scalable and flexible. Ultimately, you'll find it easier to implement and/or change business logic.

Since you asked a generic question, I could only provide a generic answer, explaining the paradigm. Should you need a more hands-on type of assistance, I suggest providing the shortest code necessary to reproduce the issues you're having and I'll consider providing more help and/or advice.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.