Store Wars: different state management in Vue.js

I’m currently working on a Vue.js application that I started building in Vue 2 before starting to use the Composition API, and then moved it to Vue 3.

In the original version, I was using Vuex for state management within the application, and interacting with Vuex directly within my Vue components - calling getters and dispatch to retrieve and update data.

As part of moving to Vue 3, I wanted to evaluate any new options, like Pinia which is now the default state management library for Vue.

But because I was integrating with Vuex directly, switching to an alternative would mean changing code within my components.

Defining a Store interface

This is a situation that often occurs in back-end development - where you may need to switch to a different type of database or a different payment provider in an eCommerce application.

In that situation, you need a generic interface that can be used by different implementations. Because they have consistent methods, one implementation can be replaced with another or multiple can be added at the same time. This is called the Strategy design pattern, and related to the open-closed principle in SOLID.

This is what I did by adding a Store interface:

export default interface Store {
  actions: {
    addRow(): void;
    init(): void;
    removeRow(index: Number): void;
  };

  state: {
    isLoading: boolean;
    selection: {
      items: [];
    };
  };
}

Any store that I want to work with needs to have these defined actions and state values, so I can use them within my components knowing that they will always be available.

Creating a native Vue store

This is one implementation of the Store interface, using just Vue’s reactive function from the Composition API:

let state = reactive({
  isLoading: false,
  selection: {
    items: [],
  },
});

let actions = {
  addRow(): void {
    state.selection.items.push({
      // ...
    });
  },

  init(): void {
    state.isLoading = true;

    // ...
  },

  removeRow(index: number): void {
    state.selection.items.splice(index, 1);
  },
};

const vueStore: Store = {
  actions,
  state: readonly(state),
};

export default vueStore;

If I needed to add a Pinia version or another library, I can create another implementation that complies with same interface.

Each implementation being responsible for any specifics for that library - extracting that logic from the component code making it more flexible and reusable.